forked from StudioInfinity/dyna3

For now, this is just a thin wrapper around the old element structure, which was renamed to `Sphere` in the previous commit. The biggest organizational change is moving `cast` into the `DisplayItem` trait.
805 lines
No EOL
30 KiB
Rust
805 lines
No EOL
30 KiB
Rust
use core::array;
|
|
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
|
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::{ElementKey, ElementColor, ElementMotion, Sphere}};
|
|
|
|
// --- scene data ---
|
|
|
|
struct SceneSpheres {
|
|
representations: Vec<DVector<f64>>,
|
|
colors: Vec<ElementColor>,
|
|
highlights: Vec<f32>
|
|
}
|
|
|
|
impl SceneSpheres {
|
|
fn new() -> SceneSpheres{
|
|
SceneSpheres {
|
|
representations: Vec::new(),
|
|
colors: Vec::new(),
|
|
highlights: Vec::new()
|
|
}
|
|
}
|
|
|
|
fn len_i32(&self) -> i32 {
|
|
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
|
}
|
|
|
|
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32) {
|
|
self.representations.push(representation);
|
|
self.colors.push(color);
|
|
self.highlights.push(highlight);
|
|
}
|
|
}
|
|
|
|
struct ScenePoints {
|
|
representations: Vec<DVector<f64>>
|
|
}
|
|
|
|
impl ScenePoints {
|
|
fn new() -> ScenePoints {
|
|
ScenePoints {
|
|
representations: Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
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<f64>, assembly_to_world: &DMatrix<f64>) -> Option<f64>;
|
|
}
|
|
|
|
impl DisplayItem for Sphere {
|
|
fn show(&self, scene: &mut Scene, selected: bool) {
|
|
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
|
let representation = self.representation.get_clone_untracked();
|
|
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
|
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
|
scene.spheres.push(representation, color, highlight);
|
|
}
|
|
|
|
// this method should be kept synchronized with `sphere_cast` in
|
|
// `spheres.frag`, which does essentially the same thing on the GPU side
|
|
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>) -> Option<f64> {
|
|
// if `a/b` is less than this threshold, we approximate
|
|
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
|
const DEG_THRESHOLD: f64 = 1e-9;
|
|
|
|
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
|
let a = -rep[3] * dir.norm_squared();
|
|
let b = rep.rows_range(..3).dot(&dir);
|
|
let c = -rep[4];
|
|
|
|
let adjust = 4.0*a*c/(b*b);
|
|
if adjust < 1.0 {
|
|
// as long as `b` is non-zero, the linear approximation of
|
|
//
|
|
// a*u^2 + b*u + c
|
|
//
|
|
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
|
|
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
|
|
// both roots have the same sign, `lin_root` will be the one closer
|
|
// to `u = 0`
|
|
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
|
|
let lin_root = -(2.0*c)/b / square_rect_ratio;
|
|
if a.abs() > DEG_THRESHOLD * b.abs() {
|
|
if lin_root > 0.0 {
|
|
Some(lin_root)
|
|
} else {
|
|
let other_root = -b/(2.*a) * square_rect_ratio;
|
|
(other_root > 0.0).then_some(other_root)
|
|
}
|
|
} else {
|
|
(lin_root > 0.0).then_some(lin_root)
|
|
}
|
|
} else {
|
|
// the line through `dir` misses the sphere completely
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- WebGL utilities ---
|
|
|
|
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<const N: usize>(
|
|
context: &WebGl2RenderingContext,
|
|
program: &WebGlProgram,
|
|
var_name: &str,
|
|
member_name_opt: Option<&str>
|
|
) -> [Option<WebGlUniformLocation>; 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())
|
|
})
|
|
}
|
|
|
|
// find the vertex attribute called `attr_name` in the given program. enable it
|
|
// and return its index
|
|
fn find_and_enable_attribute(
|
|
context: &WebGl2RenderingContext,
|
|
program: &WebGlProgram,
|
|
attr_name: &str
|
|
) -> u32 {
|
|
let attr_index = context.get_attrib_location(program, attr_name) as u32;
|
|
context.enable_vertex_attrib_array(attr_index);
|
|
attr_index
|
|
}
|
|
|
|
// bind the given vertex buffer object to the given vertex attribute
|
|
fn bind_to_attribute(
|
|
context: &WebGl2RenderingContext,
|
|
attr_index: u32,
|
|
attr_size: i32,
|
|
buffer: &Option<WebGlBuffer>
|
|
) {
|
|
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<WebGlBuffer> {
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
)
|
|
}
|
|
|
|
// --- display component ---
|
|
|
|
#[component]
|
|
pub fn Display() -> View {
|
|
let state = use_context::<AppState>();
|
|
|
|
// canvas
|
|
let display = create_node_ref();
|
|
|
|
// viewpoint
|
|
let assembly_to_world = create_signal(DMatrix::<f64>::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();
|
|
}
|
|
});
|
|
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::<f64>::identity(5, 5);
|
|
let mut rotation = DMatrix::<f64>::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 OPACITY: f32 = 0.5; /* 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::<web_sys::HtmlCanvasElement>();
|
|
let ctx = canvas
|
|
.get_context("webgl2")
|
|
.unwrap()
|
|
.unwrap()
|
|
.dyn_into::<WebGl2RenderingContext>()
|
|
.unwrap();
|
|
|
|
// disable depth testing
|
|
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
|
|
|
// set up the sphere rendering program
|
|
let sphere_program = set_up_program(
|
|
&ctx,
|
|
include_str!("identity.vert"),
|
|
include_str!("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 and enable the sphere program's sole vertex attribute
|
|
let viewport_position_attr = find_and_enable_attribute(&ctx, &sphere_program, "position");
|
|
|
|
// find the sphere program's uniforms
|
|
const SPHERE_MAX: usize = 200;
|
|
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
|
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
&ctx, &sphere_program, "sphere_list", Some("sp")
|
|
);
|
|
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
&ctx, &sphere_program, "sphere_list", Some("lt")
|
|
);
|
|
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
&ctx, &sphere_program, "color_list", None
|
|
);
|
|
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
&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 opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity");
|
|
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
|
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
|
|
|
// 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 and enable the point program's sole vertex attribute
|
|
let point_position_attr = find_and_enable_attribute(&ctx, &point_program, "position");
|
|
|
|
// set up a repainting routine
|
|
let (_, start_animation_loop, _) = create_raf(move || {
|
|
// 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()
|
|
);
|
|
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 {
|
|
key: sel,
|
|
velocity: elt_motion.as_view()
|
|
}
|
|
]
|
|
);
|
|
scene_changed.set(true);
|
|
}
|
|
}
|
|
|
|
if scene_changed.get() {
|
|
const SPACE_DIM: 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;
|
|
|
|
// get the spheres
|
|
state.assembly.elements.with_untracked(
|
|
|elts| for (key, elt) in elts {
|
|
let selected = state.selection.with(|sel| sel.contains(&key));
|
|
elt.show(&mut scene, selected);
|
|
}
|
|
);
|
|
let sphere_cnt = scene.spheres.len_i32();
|
|
|
|
// write the spheres in world coordinates
|
|
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
|
|rep| (&asm_to_world * rep).cast::<f32>()
|
|
).collect();
|
|
|
|
/* SCAFFOLDING */
|
|
// get the points
|
|
scene.points.representations.append({
|
|
use crate::engine::point;
|
|
&mut vec![
|
|
point(0.0, 0.0, 0.0),
|
|
point(0.5, 0.5, 0.0),
|
|
point(-0.5, -0.5, 0.0),
|
|
point(-0.5, 0.5, 0.0),
|
|
point(0.5, -0.5, 0.0),
|
|
point(0.0, 0.15, 1.0),
|
|
point(0.0, -0.15, -1.0)
|
|
]
|
|
});
|
|
|
|
// write the points in world coordinates
|
|
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
|
let point_positions = DMatrix::from_columns(
|
|
&scene.points.representations.into_iter().map(
|
|
|rep| &asm_to_world_sp * rep
|
|
).collect::<Vec<_>>().as_slice()
|
|
).cast::<f32>();
|
|
|
|
// --- draw the spheres ---
|
|
|
|
// use the sphere rendering program
|
|
ctx.use_program(Some(&sphere_program));
|
|
|
|
// set the resolution
|
|
let width = canvas.width() as f32;
|
|
let height = canvas.height() as f32;
|
|
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
|
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
|
|
|
// pass the 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.uniform3fv_with_f32_array(
|
|
sphere_color_locs[n].as_ref(),
|
|
&scene.spheres.colors[n]
|
|
);
|
|
ctx.uniform1f(
|
|
sphere_highlight_locs[n].as_ref(),
|
|
scene.spheres.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);
|
|
|
|
// 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);
|
|
|
|
// --- draw the points ---
|
|
|
|
// use the point rendering program
|
|
ctx.use_program(Some(&point_program));
|
|
|
|
// load the point positions into a new buffer and bind it to the
|
|
// position attribute in the vertex shader
|
|
let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice());
|
|
bind_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, &point_position_buffer);
|
|
|
|
// draw the scene
|
|
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
|
|
|
|
// --- 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 = event_dir(&event);
|
|
console::log_1(&JsValue::from(dir.to_string()));
|
|
let mut clicked: Option<(ElementKey, f64)> = None;
|
|
for (key, elt) in state.assembly.elements.get_clone_untracked() {
|
|
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) {
|
|
Some(depth) => match clicked {
|
|
Some((_, best_depth)) => {
|
|
if depth < best_depth {
|
|
clicked = Some((key, depth))
|
|
}
|
|
},
|
|
None => clicked = Some((key, depth))
|
|
}
|
|
None => ()
|
|
};
|
|
}
|
|
|
|
// if we clicked something, select it
|
|
match clicked {
|
|
Some((key, _)) => state.select(key, event.shift_key()),
|
|
None => state.selection.update(|sel| sel.clear())
|
|
};
|
|
}
|
|
)
|
|
}
|
|
} |