diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs index 59220ae..40b0e98 100644 --- a/app-proto/full-interface/src/add_remove.rs +++ b/app-proto/full-interface/src/add_remove.rs @@ -1,21 +1,227 @@ +use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; -use web_sys::{MouseEvent, console, wasm_bindgen::JsValue}; +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 + } + ); +} + +/* 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 { - let state = use_context::(); + /* 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(assembly), + _ => () + }; + }); + }); view! { div(id="add-remove") { button( - on:click=move |event: MouseEvent| { + 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,32 +230,11 @@ pub fn AddRemove() -> View { } }); } - ) { "+" } - 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()); - } ) { "🔗" } + 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/assembly.rs b/app-proto/full-interface/src/assembly.rs index 6fac59f..c0c9959 100644 --- a/app-proto/full-interface/src/assembly.rs +++ b/app-proto/full-interface/src/assembly.rs @@ -1,6 +1,7 @@ use nalgebra::DVector; -use rustc_hash::FxHashSet; +use rustc_hash::FxHashMap; use slab::Slab; +use std::collections::BTreeSet; use sycamore::prelude::*; #[derive(Clone, PartialEq)] @@ -9,7 +10,7 @@ pub struct Element { pub label: String, pub color: [f32; 3], pub rep: DVector, - pub constraints: FxHashSet + pub constraints: BTreeSet } #[derive(Clone)] @@ -21,18 +22,65 @@ 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()) } } + // 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_untracked( + |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; + let mut id = format!("sphere{}", id_num); + while self.elements_by_id.with_untracked( + |elts_by_id| elts_by_id.contains_key(&id) + ) { + id_num += 1; + id = format!("sphere{}", id_num); + } + + // create and insert a new element + 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) { let args = constraint.args; let key = self.constraints.update(|csts| csts.insert(constraint)); diff --git a/app-proto/full-interface/src/display.rs b/app-proto/full-interface/src/display.rs index 52b2ae9..c32b470 100644 --- a/app-proto/full-interface/src/display.rs +++ b/app-proto/full-interface/src/display.rs @@ -98,10 +98,12 @@ 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); create_effect(move || { + state.assembly.elements.track(); state.selection.track(); scene_changed.set(true); }); @@ -119,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; @@ -241,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 = { @@ -252,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( @@ -351,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; @@ -400,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); } }, 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 2f31ada..2c71a83 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -1,14 +1,14 @@ mod add_remove; mod assembly; mod display; +mod engine; mod outline; -use nalgebra::DVector; use rustc_hash::FxHashSet; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, Constraint, Element}; +use assembly::Assembly; use display::Display; use outline::Outline; @@ -29,45 +29,7 @@ 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: FxHashSet::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: FxHashSet::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: FxHashSet::default() - } - ) - ); - state.assembly.insert_constraint(Constraint { - args: (key_a, key_b), - rep: 0.5 - }); - provide_context(state); + provide_context(AppState::new()); view! { div(id="sidebar") { diff --git a/app-proto/full-interface/src/outline.rs b/app-proto/full-interface/src/outline.rs index be5a9b1..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, _)| key.clone() + key=|(key, elt)| ( + key.clone(), + elt.id.clone(), + elt.label.clone(), + elt.constraints.clone() + ) ) } }