diff --git a/app-proto/full-interface/.gitignore b/app-proto/full-interface/.gitignore deleted file mode 100644 index 19aa86b..0000000 --- a/app-proto/full-interface/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -dist -profiling -Cargo.lock \ No newline at end of file diff --git a/app-proto/full-interface/main.css b/app-proto/full-interface/main.css deleted file mode 100644 index a687aac..0000000 --- a/app-proto/full-interface/main.css +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index 59220ae..0000000 --- a/app-proto/full-interface/src/add_remove.rs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 6fac59f..0000000 --- a/app-proto/full-interface/src/assembly.rs +++ /dev/null @@ -1,44 +0,0 @@ -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/full-interface/src/outline.rs b/app-proto/full-interface/src/outline.rs deleted file mode 100644 index be5a9b1..0000000 --- a/app-proto/full-interface/src/outline.rs +++ /dev/null @@ -1,155 +0,0 @@ -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 19aa86b..238273d 100644 --- a/app-proto/inversive-display/.gitignore +++ b/app-proto/inversive-display/.gitignore @@ -1,4 +1,3 @@ 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 new file mode 100644 index 0000000..238273d --- /dev/null +++ b/app-proto/sketch-outline/.gitignore @@ -0,0 +1,3 @@ +target +dist +Cargo.lock \ No newline at end of file diff --git a/app-proto/full-interface/Cargo.toml b/app-proto/sketch-outline/Cargo.toml similarity index 98% rename from app-proto/full-interface/Cargo.toml rename to app-proto/sketch-outline/Cargo.toml index 920469a..35d199b 100644 --- a/app-proto/full-interface/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -12,7 +12,6 @@ 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/full-interface/index.html b/app-proto/sketch-outline/index.html similarity index 100% rename from app-proto/full-interface/index.html rename to app-proto/sketch-outline/index.html diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css new file mode 100644 index 0000000..cd7bc44 --- /dev/null +++ b/app-proto/sketch-outline/main.css @@ -0,0 +1,79 @@ +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 new file mode 100644 index 0000000..fa7fc3e --- /dev/null +++ b/app-proto/sketch-outline/src/assembly.rs @@ -0,0 +1,18 @@ +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/full-interface/src/display.rs b/app-proto/sketch-outline/src/display.rs similarity index 97% rename from app-proto/full-interface/src/display.rs rename to app-proto/sketch-outline/src/display.rs index 52b2ae9..2d0900d 100644 --- a/app-proto/full-interface/src/display.rs +++ b/app-proto/sketch-outline/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).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)) { + 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)) { elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); - let highlights: Vec<_> = element_iter.map(|(key, _)| - if state.selection.with(|sel| sel.contains(&key)) { + let highlights: Vec<_> = element_iter.map(|elt| + if state.selection.with(|sel| sel.contains(&elt.id)) { 1.0_f32 } else { HIGHLIGHT @@ -386,8 +386,8 @@ pub fn Display() -> View { // again canvas( ref=display, - width="600", - height="600", + width="750", + height="750", tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { diff --git a/app-proto/full-interface/src/identity.vert b/app-proto/sketch-outline/src/identity.vert similarity index 100% rename from app-proto/full-interface/src/identity.vert rename to app-proto/sketch-outline/src/identity.vert diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag similarity index 100% rename from app-proto/full-interface/src/inversive.frag rename to app-proto/sketch-outline/src/inversive.frag diff --git a/app-proto/full-interface/src/main.rs b/app-proto/sketch-outline/src/main.rs similarity index 61% rename from app-proto/full-interface/src/main.rs rename to app-proto/sketch-outline/src/main.rs index 2f31ada..0d1b0c7 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,79 +1,66 @@ -mod add_remove; mod assembly; mod display; mod outline; use nalgebra::DVector; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use sycamore::prelude::*; -use add_remove::AddRemove; -use assembly::{Assembly, Constraint, Element}; +use assembly::{Assembly, Element}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> -} - -impl AppState { - fn new() -> AppState { - AppState { - assembly: Assembly::new(), - selection: create_signal(FxHashSet::default()) - } - } + selection: Signal> } fn main() { sycamore::render(|| { - let state = AppState::new(); - let key_a = state.assembly.elements.update( + let state = AppState { + assembly: Assembly { + elements: create_signal(FxHashMap::default()) + }, + selection: create_signal(FxHashSet::default()) + }; + 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]), - constraints: FxHashSet::default() + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) } ) ); - let key_b = state.assembly.elements.update( + 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]), - constraints: FxHashSet::default() + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) }, ) ); 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]), - constraints: FxHashSet::default() + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) } ) ); - state.assembly.insert_constraint(Constraint { - args: (key_a, key_b), - rep: 0.5 - }); provide_context(state); view! { - div(id="sidebar") { - AddRemove {} - Outline {} - } + Outline {} Display {} } }); diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs new file mode 100644 index 0000000..13e506e --- /dev/null +++ b/app-proto/sketch-outline/src/outline.rs @@ -0,0 +1,80 @@ +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