diff --git a/app-proto/full-interface/.gitignore b/app-proto/full-interface/.gitignore new file mode 100644 index 0000000..19aa86b --- /dev/null +++ b/app-proto/full-interface/.gitignore @@ -0,0 +1,4 @@ +target +dist +profiling +Cargo.lock \ No newline at end of file diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/full-interface/Cargo.toml similarity index 98% rename from app-proto/sketch-outline/Cargo.toml rename to app-proto/full-interface/Cargo.toml index 35d199b..920469a 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/full-interface/Cargo.toml @@ -12,6 +12,7 @@ itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" rustc-hash = "2.0.0" +slab = "0.4.9" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/sketch-outline/index.html b/app-proto/full-interface/index.html similarity index 100% rename from app-proto/sketch-outline/index.html rename to app-proto/full-interface/index.html diff --git a/app-proto/full-interface/main.css b/app-proto/full-interface/main.css new file mode 100644 index 0000000..a687aac --- /dev/null +++ b/app-proto/full-interface/main.css @@ -0,0 +1,120 @@ +body { + margin: 0px; + color: #fcfcfc; + background-color: #222; +} + +/* sidebar */ + +#sidebar { + display: flex; + flex-direction: column; + float: left; + width: 450px; + height: 100vh; + margin: 0px; + padding: 0px; + border-width: 0px 1px 0px 0px; + border-style: solid; + border-color: #555; +} + +/* add-remove */ + +#add-remove { + display: flex; + gap: 8px; + margin: 8px; +} + +#add-remove > button { + width: 32px; + height: 32px; + font-size: large; +} + +/* outline */ + +#outline { + flex-grow: 1; + margin: 0px; + padding: 0px; + overflow-y: scroll; +} + +li { + user-select: none; +} + +summary { + display: flex; +} + +summary.selected { + color: #fff; + background-color: #444; +} + +summary > div, .cst { + padding-top: 4px; + padding-bottom: 4px; +} + +.elt, .cst { + display: flex; + flex-grow: 1; + padding-left: 8px; + padding-right: 8px; +} + +.elt-switch { + width: 18px; + padding-left: 2px; + text-align: center; +} + +details:has(li) .elt-switch::after { + content: '▸'; +} + +details[open]:has(li) .elt-switch::after { + content: '▾'; +} + +.elt-label { + flex-grow: 1; +} + +.cst-label { + flex-grow: 1; +} + +.elt-rep { + display: flex; +} + +.elt-rep > div, .cst-rep { + padding: 2px 0px 0px 0px; + font-size: 10pt; + text-align: center; + width: 56px; +} + +.cst { + font-style: italic; +} + +/* display */ + +canvas { + float: left; + margin-left: 20px; + margin-top: 20px; + background-color: #020202; + border: 1px solid #555; + border-radius: 16px; +} + +canvas:focus { + border-color: #aaa; +} \ No newline at end of file diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs new file mode 100644 index 0000000..59220ae --- /dev/null +++ b/app-proto/full-interface/src/add_remove.rs @@ -0,0 +1,55 @@ +use sycamore::prelude::*; +use web_sys::{MouseEvent, console, wasm_bindgen::JsValue}; + +use crate::AppState; +use crate::Constraint; + +#[component] +pub fn AddRemove() -> View { + let state = use_context::(); + + view! { + div(id="add-remove") { + button( + on:click=move |event: MouseEvent| { + console::log_1(&JsValue::from("constraints:")); + state.assembly.constraints.with(|csts| { + for (_, cst) in csts.into_iter() { + console::log_4( + &JsValue::from(cst.args.0), + &JsValue::from(cst.args.1), + &JsValue::from(":"), + &JsValue::from(cst.rep) + ); + } + }); + } + ) { "+" } + 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()); + } + ) { "🔗" } + } + } +} \ No newline at end of file diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/full-interface/src/assembly.rs new file mode 100644 index 0000000..6fac59f --- /dev/null +++ b/app-proto/full-interface/src/assembly.rs @@ -0,0 +1,44 @@ +use nalgebra::DVector; +use rustc_hash::FxHashSet; +use slab::Slab; +use sycamore::prelude::*; + +#[derive(Clone, PartialEq)] +pub struct Element { + pub id: String, + pub label: String, + pub color: [f32; 3], + pub rep: DVector, + pub constraints: FxHashSet +} + +#[derive(Clone)] +pub struct Constraint { + pub args: (usize, usize), + pub rep: f64 +} + +// a complete, view-independent description of an assembly +#[derive(Clone)] +pub struct Assembly { + pub elements: Signal>, + pub constraints: Signal> +} + +impl Assembly { + pub fn new() -> Assembly { + Assembly { + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()) + } + } + + pub fn insert_constraint(&self, constraint: Constraint) { + let args = constraint.args; + let key = self.constraints.update(|csts| csts.insert(constraint)); + self.elements.update(|elts| { + elts[args.0].constraints.insert(key); + elts[args.1].constraints.insert(key); + }) + } +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/full-interface/src/display.rs similarity index 97% rename from app-proto/sketch-outline/src/display.rs rename to app-proto/full-interface/src/display.rs index 2d0900d..52b2ae9 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/full-interface/src/display.rs @@ -288,17 +288,17 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).values(); - let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.clone().map(|elt| - if state.selection.with(|sel| sel.contains(&elt.id)) { + let element_iter = (&elements).into_iter(); + let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let colors: Vec<_> = element_iter.clone().map(|(key, elt)| + if state.selection.with(|sel| sel.contains(&key)) { elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); - let highlights: Vec<_> = element_iter.map(|elt| - if state.selection.with(|sel| sel.contains(&elt.id)) { + let highlights: Vec<_> = element_iter.map(|(key, _)| + if state.selection.with(|sel| sel.contains(&key)) { 1.0_f32 } else { HIGHLIGHT @@ -386,8 +386,8 @@ pub fn Display() -> View { // again canvas( ref=display, - width="750", - height="750", + width="600", + height="600", tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { diff --git a/app-proto/sketch-outline/src/identity.vert b/app-proto/full-interface/src/identity.vert similarity index 100% rename from app-proto/sketch-outline/src/identity.vert rename to app-proto/full-interface/src/identity.vert diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/full-interface/src/inversive.frag similarity index 100% rename from app-proto/sketch-outline/src/inversive.frag rename to app-proto/full-interface/src/inversive.frag diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/full-interface/src/main.rs similarity index 61% rename from app-proto/sketch-outline/src/main.rs rename to app-proto/full-interface/src/main.rs index 0d1b0c7..2f31ada 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -1,66 +1,79 @@ +mod add_remove; mod assembly; mod display; mod outline; use nalgebra::DVector; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; use sycamore::prelude::*; -use assembly::{Assembly, Element}; +use add_remove::AddRemove; +use assembly::{Assembly, Constraint, Element}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> +} + +impl AppState { + fn new() -> AppState { + AppState { + assembly: Assembly::new(), + selection: create_signal(FxHashSet::default()) + } + } } fn main() { sycamore::render(|| { - let state = AppState { - assembly: Assembly { - elements: create_signal(FxHashMap::default()) - }, - selection: create_signal(FxHashSet::default()) - }; - state.assembly.elements.update( + let state = AppState::new(); + let key_a = state.assembly.elements.update( |elts| elts.insert( - "wing_a".to_string(), 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]) + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + constraints: FxHashSet::default() } ) ); - state.assembly.elements.update( + let key_b = state.assembly.elements.update( |elts| elts.insert( - "wing_b".to_string(), 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]) + 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( - "central".to_string(), 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]) + 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); view! { - Outline {} + div(id="sidebar") { + AddRemove {} + Outline {} + } Display {} } }); diff --git a/app-proto/full-interface/src/outline.rs b/app-proto/full-interface/src/outline.rs new file mode 100644 index 0000000..be5a9b1 --- /dev/null +++ b/app-proto/full-interface/src/outline.rs @@ -0,0 +1,155 @@ +use itertools::Itertools; +use sycamore::{prelude::*, web::tags::div}; +use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; + +use crate::AppState; + +// this component lists the elements of the assembly, showing the constraints +// on each element as a collapsible sub-list. its implementation is based on +// Kate Morley's HTML + CSS tree views: +// +// https://iamkate.com/code/tree-views/ +// +#[component] +pub fn Outline() -> View { + // sort the elements alphabetically by ID + let elements_sorted = create_memo(|| { + let state = use_context::(); + state.assembly.elements + .get_clone() + .into_iter() + .sorted_by_key(|(_, elt)| elt.id.clone()) + .collect() + }); + + view! { + ul( + id="outline", + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { + Keyed( + list=elements_sorted, + view=|(key, elt)| { + let state = use_context::(); + let class = create_memo({ + move || { + if state.selection.with(|sel| sel.contains(&key)) { + "selected" + } else { + "" + } + } + }); + let label = elt.label.clone(); + let rep_components = elt.rep.iter().map(|u| { + let u_coord = u.to_string().replace("-", "\u{2212}"); + View::from(div().children(u_coord)) + }).collect::>(); + let constrained = elt.constraints.len() > 0; + let details_node = create_node_ref(); + view! { + /* [TO DO] switch to integer-valued parameters whenever + that becomes possible again */ + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if 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(); + }, + "ArrowRight" if constrained => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="elt-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="elt", + on:click={ + move |event: MouseEvent| { + if 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.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=move |c_key: usize| { + let c_state = use_context::(); + let assembly = &c_state.assembly; + let cst = assembly.constraints.with(|csts| csts[c_key].clone()); + let other_arg = if cst.args.0 == key { + cst.args.1 + } else { + cst.args.0 + }; + let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + view! { + li(class="cst") { + div(class="cst-label") { (other_arg_label) } + div(class="cst-rep") { (cst.rep) } + } + } + }, + key=|c_key| c_key.clone() + ) + } + } + } + } + }, + key=|(key, _)| key.clone() + ) + } + } +} \ No newline at end of file diff --git a/app-proto/inversive-display/.gitignore b/app-proto/inversive-display/.gitignore index 238273d..19aa86b 100644 --- a/app-proto/inversive-display/.gitignore +++ b/app-proto/inversive-display/.gitignore @@ -1,3 +1,4 @@ target dist +profiling Cargo.lock \ No newline at end of file diff --git a/app-proto/sketch-outline/.gitignore b/app-proto/sketch-outline/.gitignore deleted file mode 100644 index 238273d..0000000 --- a/app-proto/sketch-outline/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -target -dist -Cargo.lock \ No newline at end of file diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css deleted file mode 100644 index cd7bc44..0000000 --- a/app-proto/sketch-outline/main.css +++ /dev/null @@ -1,79 +0,0 @@ -body { - margin-left: 20px; - margin-top: 20px; - color: #fcfcfc; - background-color: #222; -} - -/* outline */ - -ul { - float: left; - width: 450px; - height: 750px; - margin: 0px; - padding: 8px; - border: 1px solid #555; - border-radius: 16px; - box-sizing: border-box; -} - -li { - display: flex; - padding: 3px; - list-style-type: none; - background-color: #444; - border-radius: 8px; -} - -li.selected { - color: #fff; - background-color: #666; -} - -li:not(:last-child) { - margin-bottom: 8px; -} - -li > .elt-label { - flex-grow: 1; - padding: 2px 0px 2px 4px; -} - -li > .elt-rep { - display: flex; -} - -li > .elt-rep > div { - padding: 2px; - margin-left: 3px; - text-align: center; - width: 60px; - background-color: #333; -} - -li.selected > .elt-rep > div { - background-color: #555; -} - -li > .elt-rep > div:first-child { - border-radius: 6px 0px 0px 6px; -} - -li > .elt-rep > div:last-child { - border-radius: 0px 6px 6px 0px; -} - -/* display */ - -canvas { - float: left; - margin-left: 16px; - background-color: #020202; - border: 1px solid #555; - border-radius: 16px; -} - -canvas:focus { - border-color: #aaa; -} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs deleted file mode 100644 index fa7fc3e..0000000 --- a/app-proto/sketch-outline/src/assembly.rs +++ /dev/null @@ -1,18 +0,0 @@ -use nalgebra::DVector; -use rustc_hash::FxHashMap; -use sycamore::reactive::Signal; - -#[derive(Clone, PartialEq)] -pub struct Element { - pub id: String, - pub label: String, - pub color: [f32; 3], - pub rep: DVector -} - -// a complete, view-independent description of an assembly -#[derive(Clone)] -pub struct Assembly { - // the order of the elements is arbitrary, and it could change at any time - pub elements: Signal> -} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs deleted file mode 100644 index 13e506e..0000000 --- a/app-proto/sketch-outline/src/outline.rs +++ /dev/null @@ -1,80 +0,0 @@ -use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; -use web_sys::KeyboardEvent; - -use crate::AppState; - -#[component] -pub fn Outline() -> View { - // sort the elements alphabetically by ID - let elements_sorted = create_memo(|| { - let state = use_context::(); - state.assembly.elements - .get_clone() - .into_iter() - .sorted_by_key(|(id, _)| id.clone()) - .map(|(_, elt)| elt) - .collect() - }); - - view! { - ul { - Keyed( - list=elements_sorted, - view=|elt| { - let state = use_context::(); - let class = create_memo({ - let id = elt.id.clone(); - move || { - if state.selection.with(|sel| sel.contains(&id)) { - "selected" - } else { - "" - } - } - }); - let label = elt.label.clone(); - let rep_components = elt.rep.iter().map(|u| { - let u_coord = u.to_string().replace("-", "\u{2212}"); - View::from(div().children(u_coord)) - }).collect::>(); - view! { - /* [TO DO] switch to integer-valued parameters whenever - that becomes possible again */ - li( - class=class.get(), - tabindex="0", - on:click={ - let id = elt.id.clone(); - move |_| { - state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); - } - }); - } - }, - on:keydown={ - let id = elt.id.clone(); - move |event: KeyboardEvent| { - if event.key() == "Enter" { - state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); - } - }); - event.prevent_default(); - } - } - } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - } - }, - key=|elt| elt.id.clone() - ) - } - } -} \ No newline at end of file