Compare commits
2 Commits
a8e13b8110
...
f4c9d3d7f4
Author | SHA1 | Date | |
---|---|---|---|
|
f4c9d3d7f4 | ||
|
17d4ed86e4 |
@ -26,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
|||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
features = [
|
features = [
|
||||||
|
'DomRect',
|
||||||
'HtmlCanvasElement',
|
'HtmlCanvasElement',
|
||||||
'HtmlInputElement',
|
'HtmlInputElement',
|
||||||
'Performance',
|
'Performance',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use nalgebra::{DMatrix, DVector};
|
use nalgebra::{DMatrix, DVector, Vector3};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
|
||||||
@ -65,6 +65,49 @@ impl Element {
|
|||||||
column_index: 0
|
column_index: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the smallest positive depth, represented as a multiple of `dir`, where
|
||||||
|
// the line generated by `dir` hits the element (which is assumed to be a
|
||||||
|
// sphere). returns `None` if the line misses the sphere. this function
|
||||||
|
// should be kept synchronized with `sphere_cast` in `inversive.frag`, which
|
||||||
|
// does essentially the same thing on the GPU side
|
||||||
|
pub fn cast(&self, dir: Vector3<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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@ use sycamore::{prelude::*, motion::create_raf};
|
|||||||
use web_sys::{
|
use web_sys::{
|
||||||
console,
|
console,
|
||||||
window,
|
window,
|
||||||
|
Element,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
|
MouseEvent,
|
||||||
WebGl2RenderingContext,
|
WebGl2RenderingContext,
|
||||||
WebGlProgram,
|
WebGlProgram,
|
||||||
WebGlShader,
|
WebGlShader,
|
||||||
@ -12,7 +14,7 @@ use web_sys::{
|
|||||||
wasm_bindgen::{JsCast, JsValue}
|
wasm_bindgen::{JsCast, JsValue}
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{AppState, assembly::ElementKey};
|
||||||
|
|
||||||
fn compile_shader(
|
fn compile_shader(
|
||||||
context: &WebGl2RenderingContext,
|
context: &WebGl2RenderingContext,
|
||||||
@ -82,6 +84,24 @@ fn bind_vertex_attrib(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the direction in camera space that a mouse event is pointing along
|
||||||
|
fn event_dir(event: &MouseEvent) -> Vector3<f64> {
|
||||||
|
let target: Element = event.target().unwrap().unchecked_into();
|
||||||
|
let rect = target.get_bounding_client_rect();
|
||||||
|
let width = rect.width();
|
||||||
|
let height = rect.height();
|
||||||
|
let shortdim = width.min(height);
|
||||||
|
|
||||||
|
// this constant should be kept synchronized with `inversive.frag`
|
||||||
|
const FOCAL_SLOPE: f64 = 0.3;
|
||||||
|
|
||||||
|
Vector3::new(
|
||||||
|
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
||||||
|
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
|
||||||
|
-1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Display() -> View {
|
pub fn Display() -> View {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
@ -89,6 +109,9 @@ pub fn Display() -> View {
|
|||||||
// canvas
|
// canvas
|
||||||
let display = create_node_ref();
|
let display = create_node_ref();
|
||||||
|
|
||||||
|
// viewpoint
|
||||||
|
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
||||||
|
|
||||||
// navigation
|
// navigation
|
||||||
let pitch_up = create_signal(0.0);
|
let pitch_up = create_signal(0.0);
|
||||||
let pitch_down = create_signal(0.0);
|
let pitch_down = create_signal(0.0);
|
||||||
@ -296,7 +319,7 @@ pub fn Display() -> View {
|
|||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
0.0, 0.0, 0.0, 0.0, 1.0
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
let assembly_to_world = &location * &orientation;
|
let asm_to_world = &location * &orientation;
|
||||||
|
|
||||||
// get the assembly
|
// get the assembly
|
||||||
let (
|
let (
|
||||||
@ -311,7 +334,7 @@ pub fn Display() -> View {
|
|||||||
|
|
||||||
// representation vectors in world coordinates
|
// representation vectors in world coordinates
|
||||||
elts.iter().map(
|
elts.iter().map(
|
||||||
|(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep)
|
|(_, elt)| elt.representation.with(|rep| &asm_to_world * rep)
|
||||||
).collect::<Vec<_>>(),
|
).collect::<Vec<_>>(),
|
||||||
|
|
||||||
// colors
|
// colors
|
||||||
@ -370,6 +393,9 @@ pub fn Display() -> View {
|
|||||||
// draw the scene
|
// draw the scene
|
||||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||||
|
|
||||||
|
// update the viewpoint
|
||||||
|
assembly_to_world.set(asm_to_world);
|
||||||
|
|
||||||
// clear the scene change flag
|
// clear the scene change flag
|
||||||
scene_changed.set(
|
scene_changed.set(
|
||||||
pitch_up_val != 0.0
|
pitch_up_val != 0.0
|
||||||
@ -458,6 +484,31 @@ pub fn Display() -> View {
|
|||||||
yaw_left.set(0.0);
|
yaw_left.set(0.0);
|
||||||
roll_ccw.set(0.0);
|
roll_ccw.set(0.0);
|
||||||
roll_cw.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())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,24 @@ impl AppState {
|
|||||||
selection: create_signal(FxHashSet::default())
|
selection: create_signal(FxHashSet::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in single-selection mode, select the element with the given key. in
|
||||||
|
// multiple-selection mode, toggle whether the element with the given key
|
||||||
|
// is selected
|
||||||
|
fn select(&self, key: ElementKey, multi: bool) {
|
||||||
|
if multi {
|
||||||
|
self.selection.update(|sel| {
|
||||||
|
if !sel.remove(&key) {
|
||||||
|
sel.insert(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.selection.update(|sel| {
|
||||||
|
sel.clear();
|
||||||
|
sel.insert(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -83,18 +83,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
|||||||
move |event: KeyboardEvent| {
|
move |event: KeyboardEvent| {
|
||||||
match event.key().as_str() {
|
match event.key().as_str() {
|
||||||
"Enter" => {
|
"Enter" => {
|
||||||
if event.shift_key() {
|
state.select(key, 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();
|
event.prevent_default();
|
||||||
},
|
},
|
||||||
"ArrowRight" if constrained.get() => {
|
"ArrowRight" if constrained.get() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user