use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use std::rc::Rc; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, KeyboardEvent, MouseEvent, WebGl2RenderingContext, WebGlBuffer, WebGlProgram, WebGlShader, WebGlUniformLocation, wasm_bindgen::{JsCast, JsValue} }; use crate::{ AppState, assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; // --- color --- const COLOR_SIZE: usize = 3; type ColorWithOpacity = [f32; COLOR_SIZE + 1]; fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { let mut color_with_opacity = [0.0; COLOR_SIZE + 1]; color_with_opacity[..COLOR_SIZE].copy_from_slice(&color); color_with_opacity[COLOR_SIZE] = opacity; color_with_opacity } // --- scene data --- struct SceneSpheres { representations: Vec>, colors_with_opacity: Vec, highlights: Vec } impl SceneSpheres { fn new() -> SceneSpheres{ SceneSpheres { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new() } } fn len_i32(&self) -> i32 { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32) { self.representations.push(representation); self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); } } struct ScenePoints { representations: Vec>, colors_with_opacity: Vec, highlights: Vec, selections: Vec } impl ScenePoints { fn new() -> ScenePoints { ScenePoints { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), selections: Vec::new() } } fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { self.representations.push(representation); self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); self.selections.push(if selected { 1.0 } else { 0.0 }); } } pub struct Scene { spheres: SceneSpheres, points: ScenePoints } impl Scene { fn new() -> Scene { Scene { spheres: SceneSpheres::new(), points: ScenePoints::new() } } } pub trait DisplayItem { fn show(&self, scene: &mut Scene, selected: bool); // the smallest positive depth, represented as a multiple of `dir`, where // the line generated by `dir` hits the element. returns `None` if the line // misses the element fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; } impl DisplayItem for Sphere { fn show(&self, scene: &mut Scene, selected: bool) { /* SCAFFOLDING */ const DEFAULT_OPACITY: f32 = 0.5; const GHOST_OPACITY: f32 = 0.2; const HIGHLIGHT: f32 = 0.2; let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; scene.spheres.push(representation, color, opacity, highlight); } // this method should be kept synchronized with `sphere_cast` in // `spheres.frag`, which does essentially the same thing on the GPU side fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { // if `a/b` is less than this threshold, we approximate // `a*u^2 + b*u + c` by the linear function `b*u + c` const DEG_THRESHOLD: f64 = 1e-9; let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); let a = -rep[3] * dir.norm_squared(); let b = rep.rows_range(..3).dot(&dir); let c = -rep[4]; let adjust = 4.0*a*c/(b*b); if adjust < 1.0 { // as long as `b` is non-zero, the linear approximation of // // a*u^2 + b*u + c // // at `u = 0` will reach zero at a finite depth `u_lin`. the root of // the quadratic adjacent to `u_lin` is stored in `lin_root`. if // both roots have the same sign, `lin_root` will be the one closer // to `u = 0` let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); let lin_root = -(2.0*c)/b / square_rect_ratio; if a.abs() > DEG_THRESHOLD * b.abs() { if lin_root > 0.0 { Some(lin_root) } else { let other_root = -b/(2.*a) * square_rect_ratio; (other_root > 0.0).then_some(other_root) } } else { (lin_root > 0.0).then_some(lin_root) } } else { // the line through `dir` misses the sphere completely None } } } impl DisplayItem for Point { fn show(&self, scene: &mut Scene, selected: bool) { /* SCAFFOLDING */ const GHOST_OPACITY: f32 = 0.4; const HIGHLIGHT: f32 = 0.5; let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; scene.points.push(representation, color, opacity, highlight, selected); } /* SCAFFOLDING */ fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); if rep[2] < 0.0 { // this constant should be kept synchronized with `point.frag` const POINT_RADIUS_PX: f64 = 4.0; // find the radius of the point in screen projection units let point_radius_proj = POINT_RADIUS_PX * pixel_size; // find the squared distance between the screen projections of the // ray and the point let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; let dist_sq = (dir_proj - rep_proj).norm_squared(); // if the ray hits the point, return its depth if dist_sq < point_radius_proj * point_radius_proj { Some(rep[2] / dir[2]) } else { None } } else { None } } } // --- WebGL utilities --- fn compile_shader( context: &WebGl2RenderingContext, 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 set_up_program( context: &WebGl2RenderingContext, vertex_shader_source: &str, fragment_shader_source: &str ) -> WebGlProgram { // compile the shaders let vertex_shader = compile_shader( &context, WebGl2RenderingContext::VERTEX_SHADER, vertex_shader_source, ); let fragment_shader = compile_shader( &context, WebGl2RenderingContext::FRAGMENT_SHADER, fragment_shader_source, ); // create the program and attach the shaders let program = context.create_program().unwrap(); context.attach_shader(&program, &vertex_shader); context.attach_shader(&program, &fragment_shader); context.link_program(&program); /* DEBUG */ // report whether linking succeeded let link_status = context .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) .as_bool() .unwrap(); let link_msg = if link_status { "Linked successfully" } else { "Linking failed" }; console::log_1(&JsValue::from(link_msg)); program } fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, 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()) }) } // bind the given vertex buffer object to the given vertex attribute fn bind_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, buffer: &Option ) { context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.vertex_attrib_pointer_with_i32( attr_index, attr_size, WebGl2RenderingContext::FLOAT, false, // don't normalize 0, // zero stride 0, // zero offset ); } // load the given data into a new vertex buffer object fn load_new_buffer( context: &WebGl2RenderingContext, data: &[f32] ) -> Option { // create a buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer(); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); // load the given data into the buffer. this block is unsafe because // `Float32Array::view` creates a raw view into our module's // `WebAssembly.Memory` buffer. allocating more memory will change the // buffer, invalidating the view, so we have to make sure we don't allocate // any memory until the view is dropped. we're okay here because the view is // used as soon as it's created unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, &js_sys::Float32Array::view(&data), WebGl2RenderingContext::STATIC_DRAW, ); } buffer } fn bind_new_buffer_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, data: &[f32] ) { let buffer = load_new_buffer(context, data); bind_to_attribute(context, attr_index, attr_size, &buffer); } // the direction in camera space that a mouse event is pointing along fn event_dir(event: &MouseEvent) -> (Vector3, f64) { let target: web_sys::Element = event.target().unwrap().unchecked_into(); let rect = target.get_bounding_client_rect(); let width = rect.width(); let height = rect.height(); let shortdim = width.min(height); // this constant should be kept synchronized with `spheres.frag` and // `point.vert` const FOCAL_SLOPE: f64 = 0.3; ( Vector3::new( FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, -1.0 ), FOCAL_SLOPE * 2.0 / shortdim ) } // --- display component --- #[component] pub fn Display() -> View { let state = use_context::(); // canvas let display = create_node_ref(); // viewpoint let assembly_to_world = create_signal(DMatrix::::identity(5, 5)); // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); let yaw_right = create_signal(0.0); let yaw_left = create_signal(0.0); let roll_ccw = create_signal(0.0); let roll_cw = create_signal(0.0); let zoom_in = create_signal(0.0); let zoom_out = create_signal(0.0); let turntable = create_signal(false); /* BENCHMARKING */ // manipulation let translate_neg_x = create_signal(0.0); let translate_pos_x = create_signal(0.0); let translate_neg_y = create_signal(0.0); let translate_pos_y = create_signal(0.0); let translate_neg_z = create_signal(0.0); let translate_pos_z = create_signal(0.0); let shrink_neg = create_signal(0.0); let shrink_pos = create_signal(0.0); // change listener let scene_changed = create_signal(true); create_effect(move || { state.assembly.elements.with(|elts| { for elt in elts { elt.representation().track(); elt.ghost().track(); } }); state.selection.track(); scene_changed.set(true); }); /* INSTRUMENTS */ const SAMPLE_PERIOD: i32 = 60; let mut last_sample_time = 0.0; let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); let assembly_for_raf = state.assembly.clone(); on_mount(move || { // timing let mut last_time = 0.0; // viewpoint const ROT_SPEED: f64 = 0.4; // in radians per second const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */ let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::identity(5, 5); let mut location_z: f64 = 5.0; // manipulation const TRANSLATION_SPEED: f64 = 0.15; // in length units per second const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters const 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(); // disable depth testing ctx.disable(WebGl2RenderingContext::DEPTH_TEST); // set blend mode ctx.enable(WebGl2RenderingContext::BLEND); ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); // set up the sphere rendering program let sphere_program = set_up_program( &ctx, include_str!("identity.vert"), include_str!("spheres.frag") ); // set up the point rendering program let point_program = set_up_program( &ctx, include_str!("point.vert"), include_str!("point.frag") ); /* 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 the sphere program's vertex attribute let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; // find the sphere program's uniforms const SPHERE_MAX: usize = 200; let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( &ctx, &sphere_program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( &ctx, &sphere_program, "sphere_list", Some("lt") ); let sphere_color_locs = get_uniform_array_locations::( &ctx, &sphere_program, "color_list", None ); let sphere_highlight_locs = get_uniform_array_locations::( &ctx, &sphere_program, "highlight_list", None ); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; let viewport_positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -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 ]; let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); // find the point program's vertex attributes let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { // get the time step let time = performance.now(); let time_step = 0.001*(time - last_time); last_time = time; // get the navigation state let pitch_up_val = pitch_up.get(); let pitch_down_val = pitch_down.get(); let yaw_right_val = yaw_right.get(); let yaw_left_val = yaw_left.get(); let roll_ccw_val = roll_ccw.get(); let roll_cw_val = roll_cw.get(); let zoom_in_val = zoom_in.get(); let zoom_out_val = zoom_out.get(); let turntable_val = turntable.get(); /* BENCHMARKING */ // get the manipulation state let translate_neg_x_val = translate_neg_x.get(); let translate_pos_x_val = translate_pos_x.get(); let translate_neg_y_val = translate_neg_y.get(); let translate_pos_y_val = translate_pos_y.get(); let translate_neg_z_val = translate_neg_z.get(); let translate_pos_z_val = translate_pos_z.get(); let shrink_neg_val = shrink_neg.get(); let shrink_pos_val = shrink_pos.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() } } /* BENCHMARKING */ + if turntable_val { Vector3::new(0.0, TURNTABLE_SPEED, 0.0) } else { Vector3::zeros() }; let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); rotation_sp.copy_from( Rotation3::from_scaled_axis(time_step * ang_vel).matrix() ); orientation = &rotation * &orientation; // update the assembly's location let zoom = zoom_out_val - zoom_in_val; location_z *= (time_step * ZOOM_SPEED * zoom).exp(); // manipulate the assembly if state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( |sel| sel.into_iter().next().unwrap().clone() ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; let translate_z = translate_pos_z_val - translate_neg_z_val; let shrink = shrink_pos_val - shrink_neg_val; let translating = translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0; if translating || shrink != 0.0 { let elt_motion = { let u = if translating { TRANSLATION_SPEED * Vector3::new( translate_x, translate_y, translate_z ).normalize() } else { Vector3::zeros() }; time_step * DVector::from_column_slice( &[u[0], u[1], u[2], SHRINKING_SPEED * shrink] ) }; assembly_for_raf.deform( vec![ ElementMotion { element: sel, velocity: elt_motion.as_view() } ] ); scene_changed.set(true); } } if scene_changed.get() { const SPACE_DIM: usize = 3; const COLOR_SIZE: usize = 3; /* 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; } // --- get the assembly --- let mut scene = Scene::new(); // 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 asm_to_world = &location * &orientation; // set up the scene state.assembly.elements.with_untracked( |elts| for elt in elts { let selected = state.selection.with(|sel| sel.contains(elt)); elt.show(&mut scene, selected); } ); let sphere_cnt = scene.spheres.len_i32(); // --- draw the spheres --- // use the sphere rendering program ctx.use_program(Some(&sphere_program)); // enable the sphere program's vertex attribute ctx.enable_vertex_attrib_array(viewport_position_attr); // write the spheres in world coordinates let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( |rep| (&asm_to_world * rep).cast::() ).collect(); // set the resolution let width = canvas.width() as f32; 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 scene data ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); for n in 0..sphere_reps_world.len() { let v = &sphere_reps_world[n]; ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), v.rows(0, 3).as_slice() ); ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), v.rows(3, 2).as_slice() ); ctx.uniform4fv_with_f32_array( sphere_color_locs[n].as_ref(), &scene.spheres.colors_with_opacity[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), scene.spheres.highlights[n] ); } // pass the display parameters ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); // bind the viewport vertex position buffer to the position // attribute in the vertex shader bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); // disable the sphere program's vertex attribute ctx.disable_vertex_attrib_array(viewport_position_attr); // --- draw the points --- if !scene.points.representations.is_empty() { // use the point rendering program ctx.use_program(Some(&point_program)); // enable the point program's vertex attributes ctx.enable_vertex_attrib_array(point_position_attr); ctx.enable_vertex_attrib_array(point_color_attr); ctx.enable_vertex_attrib_array(point_highlight_attr); ctx.enable_vertex_attrib_array(point_selection_attr); // write the points in world coordinates let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); let point_positions = DMatrix::from_columns( &scene.points.representations.into_iter().map( |rep| &asm_to_world_sp * rep ).collect::>().as_slice() ).cast::(); // load the point positions and colors into new buffers and // bind them to the corresponding attributes in the vertex // shader bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); // disable the point program's vertex attributes ctx.disable_vertex_attrib_array(point_position_attr); ctx.disable_vertex_attrib_array(point_color_attr); ctx.disable_vertex_attrib_array(point_highlight_attr); ctx.disable_vertex_attrib_array(point_selection_attr); } // --- update the display state --- // update the viewpoint assembly_to_world.set(asm_to_world); // clear the scene change flag scene_changed.set( pitch_up_val != 0.0 || pitch_down_val != 0.0 || yaw_left_val != 0.0 || yaw_right_val != 0.0 || roll_cw_val != 0.0 || roll_ccw_val != 0.0 || zoom_in_val != 0.0 || zoom_out_val != 0.0 || turntable_val /* BENCHMARKING */ ); } else { frames_since_last_sample = 0; mean_frame_interval.set(-1.0); } }); start_animation_loop(); }); let set_nav_signal = move |event: &KeyboardEvent, value: f64| { let mut navigating = true; let shift = event.shift_key(); match event.key().as_str() { "ArrowUp" if shift => zoom_in.set(value), "ArrowDown" if shift => zoom_out.set(value), "ArrowUp" => pitch_up.set(value), "ArrowDown" => pitch_down.set(value), "ArrowRight" if shift => roll_cw.set(value), "ArrowLeft" if shift => roll_ccw.set(value), "ArrowRight" => yaw_right.set(value), "ArrowLeft" => yaw_left.set(value), _ => navigating = false }; if navigating { scene_changed.set(true); event.prevent_default(); } }; let set_manip_signal = move |event: &KeyboardEvent, value: f64| { let mut manipulating = true; let shift = event.shift_key(); match event.key().as_str() { "d" | "D" => translate_pos_x.set(value), "a" | "A" => translate_neg_x.set(value), "w" | "W" if shift => translate_neg_z.set(value), "s" | "S" if shift => translate_pos_z.set(value), "w" | "W" => translate_pos_y.set(value), "s" | "S" => translate_neg_y.set(value), "]" | "}" => shrink_neg.set(value), "[" | "{" => shrink_pos.set(value), _ => manipulating = false }; if manipulating { event.prevent_default(); } }; view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible // again canvas( ref=display, width="600", height="600", tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs 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); // swap manipulation inputs translate_pos_z.set(translate_neg_y.get()); translate_neg_z.set(translate_pos_y.get()); translate_pos_y.set(0.0); translate_neg_y.set(0.0); } else { if event.key() == "Enter" { /* BENCHMARKING */ turntable.set_fn(|turn| !turn); scene_changed.set(true); } set_nav_signal(&event, 1.0); set_manip_signal(&event, 1.0); } }, on:keyup=move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs yaw_right.set(roll_cw.get()); yaw_left.set(roll_ccw.get()); pitch_up.set(zoom_in.get()); 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); // swap manipulation inputs translate_pos_y.set(translate_neg_z.get()); translate_neg_y.set(translate_pos_z.get()); translate_pos_z.set(0.0); translate_neg_z.set(0.0); } else { set_nav_signal(&event, 0.0); set_manip_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); }, on:click=move |event: MouseEvent| { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(Rc, f64)> = None; let tangible_elts = state.assembly.elements .get_clone_untracked() .into_iter() .filter(|elt| !elt.ghost().get()); for elt in tangible_elts { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { clicked = Some((elt, depth)) } }, None => clicked = Some((elt, depth)) } None => () }; } // if we clicked something, select it match clicked { Some((elt, _)) => state.select(&elt, event.shift_key()), None => state.selection.update(|sel| sel.clear()) }; } ) } }