Compare commits

...

14 Commits

Author SHA1 Message Date
Aaron Fenyes
18ebf3be2c Display: add turntable for benchmarking
Together with 25fa108 and 4f8f360, this lets us do a benchmarking
routine for `full-interface` which is comparable to the one we've been
using for `inversive-display`.
2024-09-30 00:44:13 -07:00
Aaron Fenyes
edace8e4ea Outline: include ID and label in element diff key 2024-09-29 23:41:16 -07:00
Aaron Fenyes
70bd39b9e5 App: remove unused imports 2024-09-29 23:30:35 -07:00
Aaron Fenyes
25fa108e9b AddRemove: add low-curvature test assembly from inversive-display 2024-09-28 19:37:43 -07:00
Aaron Fenyes
7977b11caf AddRemove: switch between pre-made test assemblies 2024-09-28 18:56:33 -07:00
Aaron Fenyes
1c9fec36e5 Display: make scene change flag track element list 2024-09-28 18:51:28 -07:00
Aaron Fenyes
721a8716d4 Assembly: don't track element list when inserting
Calling `try_insert_element` or `insert_new_element` in a responsive
context shouldn't make the context track `elements_by_id`.
2024-09-28 18:49:17 -07:00
Aaron Fenyes
4f8f36053f App: use general test assembly from inversive-display
This moves us toward dropping the separate display prototype.
2024-09-28 14:18:04 -07:00
Aaron Fenyes
28b1ecb8e9 App: use element insertion method in test 2024-09-28 13:29:09 -07:00
Aaron Fenyes
b08dbd6f93 Assembly: factor out element insertion 2024-09-28 13:27:03 -07:00
Aaron Fenyes
bd0982f821 AddRemove: make a button that adds elements
In the process, switch selection storage back to `FxHashSet`, reverting
commit b3afd6f.
2024-09-27 14:33:49 -07:00
Aaron Fenyes
2444649dd1 AddRemove: underscore unused event variables 2024-09-26 19:17:57 -07:00
Aaron Fenyes
b3afd6f555 App: Store selection in BTreeSet
Since we're using `BTreeSet` for element constraint sets now, we might
as well use it for the selection set too. This removes the `rustc-hash`
dependency.
2024-09-26 19:16:41 -07:00
Aaron Fenyes
9b39fe56b8 Outline: include constraints in element diff key
This tells Sycamore that the outline view of an element should update
when the element's constraint set has changed. To make the constraint
set hashable, so we can include it in the diff key, we store it as a
`BTreeSet` instead of an `FxHashSet`.
2024-09-26 19:10:34 -07:00
6 changed files with 317 additions and 77 deletions

View File

@ -1,21 +1,227 @@
use std::collections::BTreeSet; /* DEBUG */
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{MouseEvent, console, wasm_bindgen::JsValue}; use web_sys::{console, wasm_bindgen::JsValue};
use crate::AppState; use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
use crate::Constraint;
/* 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] #[component]
pub fn AddRemove() -> View { pub fn AddRemove() -> View {
/* 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::<AppState>(); let state = use_context::<AppState>();
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! { view! {
div(id="add-remove") { div(id="add-remove") {
button( button(
on:click=move |event: MouseEvent| { on:click=|_| {
let state = use_context::<AppState>();
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::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click=|_| {
let state = use_context::<AppState>();
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:")); console::log_1(&JsValue::from("constraints:"));
state.assembly.constraints.with(|csts| { state.assembly.constraints.with(|csts| {
for (_, cst) in csts.into_iter() { for (_, cst) in csts.into_iter() {
console::log_4( console::log_5(
&JsValue::from(" "),
&JsValue::from(cst.args.0), &JsValue::from(cst.args.0),
&JsValue::from(cst.args.1), &JsValue::from(cst.args.1),
&JsValue::from(":"), &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" }
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
use nalgebra::DVector; use nalgebra::DVector;
use rustc_hash::FxHashSet; use rustc_hash::FxHashMap;
use slab::Slab; use slab::Slab;
use std::collections::BTreeSet;
use sycamore::prelude::*; use sycamore::prelude::*;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
@ -9,7 +10,7 @@ pub struct Element {
pub label: String, pub label: String,
pub color: [f32; 3], pub color: [f32; 3],
pub rep: DVector<f64>, pub rep: DVector<f64>,
pub constraints: FxHashSet<usize> pub constraints: BTreeSet<usize>
} }
#[derive(Clone)] #[derive(Clone)]
@ -21,18 +22,65 @@ pub struct Constraint {
// a complete, view-independent description of an assembly // a complete, view-independent description of an assembly
#[derive(Clone)] #[derive(Clone)]
pub struct Assembly { pub struct Assembly {
// elements and constraints
pub elements: Signal<Slab<Element>>, pub elements: Signal<Slab<Element>>,
pub constraints: Signal<Slab<Constraint>> pub constraints: Signal<Slab<Constraint>>,
// indexing
pub elements_by_id: Signal<FxHashMap<String, usize>>
} }
impl Assembly { impl Assembly {
pub fn new() -> Assembly { pub fn new() -> Assembly {
Assembly { Assembly {
elements: create_signal(Slab::new()), 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::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]),
constraints: BTreeSet::default()
}
);
}
pub fn insert_constraint(&self, constraint: Constraint) { pub fn insert_constraint(&self, constraint: Constraint) {
let args = constraint.args; let args = constraint.args;
let key = self.constraints.update(|csts| csts.insert(constraint)); let key = self.constraints.update(|csts| csts.insert(constraint));

View File

@ -98,10 +98,12 @@ pub fn Display() -> View {
let roll_cw = create_signal(0.0); let roll_cw = create_signal(0.0);
let zoom_in = create_signal(0.0); let zoom_in = create_signal(0.0);
let zoom_out = create_signal(0.0); let zoom_out = create_signal(0.0);
let turntable = create_signal(false); /* BENCHMARKING */
// change listener // change listener
let scene_changed = create_signal(true); let scene_changed = create_signal(true);
create_effect(move || { create_effect(move || {
state.assembly.elements.track();
state.selection.track(); state.selection.track();
scene_changed.set(true); scene_changed.set(true);
}); });
@ -119,6 +121,7 @@ pub fn Display() -> View {
// viewpoint // viewpoint
const ROT_SPEED: f64 = 0.4; // in radians per second const ROT_SPEED: f64 = 0.4; // in radians per second
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate 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 orientation = DMatrix::<f64>::identity(5, 5);
let mut rotation = DMatrix::<f64>::identity(5, 5); let mut rotation = DMatrix::<f64>::identity(5, 5);
let mut location_z: f64 = 5.0; let mut location_z: f64 = 5.0;
@ -241,6 +244,7 @@ pub fn Display() -> View {
let roll_cw_val = roll_cw.get(); let roll_cw_val = roll_cw.get();
let zoom_in_val = zoom_in.get(); let zoom_in_val = zoom_in.get();
let zoom_out_val = zoom_out.get(); let zoom_out_val = zoom_out.get();
let turntable_val = turntable.get(); /* BENCHMARKING */
// update the assembly's orientation // update the assembly's orientation
let ang_vel = { let ang_vel = {
@ -252,6 +256,10 @@ pub fn Display() -> View {
} else { } else {
Vector3::zeros() 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); let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
rotation_sp.copy_from( rotation_sp.copy_from(
@ -351,6 +359,7 @@ pub fn Display() -> View {
|| roll_ccw_val != 0.0 || roll_ccw_val != 0.0
|| zoom_in_val != 0.0 || zoom_in_val != 0.0
|| zoom_out_val != 0.0 || zoom_out_val != 0.0
|| turntable_val /* BENCHMARKING */
); );
} else { } else {
frames_since_last_sample = 0; frames_since_last_sample = 0;
@ -400,6 +409,10 @@ pub fn Display() -> View {
pitch_up.set(0.0); pitch_up.set(0.0);
pitch_down.set(0.0); pitch_down.set(0.0);
} else { } else {
if event.key() == "Enter" { /* BENCHMARKING */
turntable.set_fn(|turn| !turn);
scene_changed.set(true);
}
set_nav_signal(event, 1.0); set_nav_signal(event, 1.0);
} }
}, },

View File

@ -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<f64> {
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<f64> {
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)
])
}

View File

@ -1,14 +1,14 @@
mod add_remove; mod add_remove;
mod assembly; mod assembly;
mod display; mod display;
mod engine;
mod outline; mod outline;
use nalgebra::DVector;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use sycamore::prelude::*; use sycamore::prelude::*;
use add_remove::AddRemove; use add_remove::AddRemove;
use assembly::{Assembly, Constraint, Element}; use assembly::Assembly;
use display::Display; use display::Display;
use outline::Outline; use outline::Outline;
@ -29,45 +29,7 @@ impl AppState {
fn main() { fn main() {
sycamore::render(|| { sycamore::render(|| {
let state = AppState::new(); provide_context(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::<f64>::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::<f64>::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::<f64>::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);
view! { view! {
div(id="sidebar") { div(id="sidebar") {

View File

@ -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()
)
) )
} }
} }