From 78f8ef8215af8aeec841598d2957f25f50061d01 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 19 Sep 2024 17:53:07 -0700 Subject: [PATCH 01/12] Outline: switch to single selection --- app-proto/sketch-outline/src/outline.rs | 42 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 13e506e..8d13df1 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::KeyboardEvent; +use web_sys::{KeyboardEvent, MouseEvent}; use crate::AppState; @@ -18,7 +18,12 @@ pub fn Outline() -> View { }); view! { - ul { + ul( + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { Keyed( list=elements_sorted, view=|elt| { @@ -46,23 +51,38 @@ pub fn Outline() -> View { tabindex="0", on:click={ let id = elt.id.clone(); - move |_| { - state.selection.update(|sel| { - if !sel.remove(&id) { + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); sel.insert(id.clone()); - } - }); + }); + } + event.stop_propagation(); } }, on:keydown={ let id = elt.id.clone(); move |event: KeyboardEvent| { if event.key() == "Enter" { - state.selection.update(|sel| { - if !sel.remove(&id) { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); sel.insert(id.clone()); - } - }); + }); + } event.prevent_default(); } } From d121385c18026bb76176c33ff97669476e1016dc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:21:45 -0700 Subject: [PATCH 02/12] App: store assembly elements in slab --- app-proto/sketch-outline/Cargo.toml | 1 + app-proto/sketch-outline/src/assembly.rs | 8 ++-- app-proto/sketch-outline/src/display.rs | 6 +-- app-proto/sketch-outline/src/main.rs | 49 ++++++++++++++---------- app-proto/sketch-outline/src/outline.rs | 21 +++++----- 5 files changed, 45 insertions(+), 40 deletions(-) diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 35d199b..920469a 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/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/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index fa7fc3e..f34d373 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,5 +1,5 @@ use nalgebra::DVector; -use rustc_hash::FxHashMap; +use slab::Slab; use sycamore::reactive::Signal; #[derive(Clone, PartialEq)] @@ -7,12 +7,12 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector + pub rep: DVector, + pub key: usize } // 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> + pub elements: Signal> } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 2d0900d..b0b9877 100644 --- a/app-proto/sketch-outline/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).values(); + let element_iter = (&elements).into_iter().map(|(_, elt)| elt); 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)) { + if state.selection.with(|sel| sel.contains(&elt.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)) { + if state.selection.with(|sel| sel.contains(&elt.key)) { 1.0_f32 } else { HIGHLIGHT diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 0d1b0c7..f86abaf 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -3,7 +3,8 @@ mod display; mod outline; use nalgebra::DVector; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; +use slab::Slab; use sycamore::prelude::*; use assembly::{Assembly, Element}; @@ -13,50 +14,56 @@ use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } fn main() { sycamore::render(|| { let state = AppState { assembly: Assembly { - elements: create_signal(FxHashMap::default()) + elements: create_signal(Slab::new()) }, selection: create_signal(FxHashSet::default()) }; - state.assembly.elements.update( - |elts| elts.insert( - "wing_a".to_string(), + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.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]) + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + key: key } - ) - ); - state.assembly.elements.update( - |elts| elts.insert( - "wing_b".to_string(), + ); + }); + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.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]) + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + key: key }, - ) - ); - state.assembly.elements.update( - |elts| elts.insert( - "central".to_string(), + ); + }); + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.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]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + key: key } - ) - ); + ); + }); provide_context(state); view! { diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 8d13df1..9913f32 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -12,8 +12,8 @@ pub fn Outline() -> View { state.assembly.elements .get_clone() .into_iter() - .sorted_by_key(|(id, _)| id.clone()) .map(|(_, elt)| elt) + .sorted_by_key(|elt| elt.id.clone()) .collect() }); @@ -29,9 +29,8 @@ pub fn Outline() -> View { view=|elt| { let state = use_context::(); let class = create_memo({ - let id = elt.id.clone(); move || { - if state.selection.with(|sel| sel.contains(&id)) { + if state.selection.with(|sel| sel.contains(&elt.key)) { "selected" } else { "" @@ -50,37 +49,35 @@ pub fn Outline() -> View { class=class.get(), tabindex="0", on:click={ - let id = elt.id.clone(); move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); + if !sel.remove(&elt.key) { + sel.insert(elt.key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(id.clone()); + sel.insert(elt.key); }); } event.stop_propagation(); } }, on:keydown={ - let id = elt.id.clone(); move |event: KeyboardEvent| { if event.key() == "Enter" { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); + if !sel.remove(&elt.key) { + sel.insert(elt.key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(id.clone()); + sel.insert(elt.key); }); } event.prevent_default(); @@ -93,7 +90,7 @@ pub fn Outline() -> View { } } }, - key=|elt| elt.id.clone() + key=|elt| elt.key ) } } From 147e2758234b78ccfc2e24f6ca762b40e9f5d77b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:38:17 -0700 Subject: [PATCH 03/12] App: don't bother copying key into element When we access an element, we always have its key, either because the slab iterator yielded it along side the element or because we used it to get the element from the slab. --- app-proto/sketch-outline/src/assembly.rs | 3 +- app-proto/sketch-outline/src/display.rs | 12 ++++---- app-proto/sketch-outline/src/main.rs | 39 +++++++++--------------- app-proto/sketch-outline/src/outline.rs | 21 ++++++------- 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index f34d373..79912b9 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -7,8 +7,7 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector, - pub key: usize + pub rep: DVector } // a complete, view-independent description of an assembly diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index b0b9877..2ac8ebf 100644 --- a/app-proto/sketch-outline/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().map(|(_, elt)| elt); - 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.key)) { + 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.key)) { + let highlights: Vec<_> = element_iter.map(|(key, _)| + if state.selection.with(|sel| sel.contains(&key)) { 1.0_f32 } else { HIGHLIGHT diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index f86abaf..a43b020 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -25,45 +25,36 @@ fn main() { }, selection: create_signal(FxHashSet::default()) }; - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + 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]), - key: key + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) } - ); - }); - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + ) + ); + 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]), - key: key + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) }, - ); - }); - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + ) + ); + 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]), - key: key + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) } - ); - }); + ) + ); provide_context(state); view! { diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 9913f32..031f9bc 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -12,8 +12,7 @@ pub fn Outline() -> View { state.assembly.elements .get_clone() .into_iter() - .map(|(_, elt)| elt) - .sorted_by_key(|elt| elt.id.clone()) + .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() }); @@ -26,11 +25,11 @@ pub fn Outline() -> View { ) { Keyed( list=elements_sorted, - view=|elt| { + view=|(key, elt)| { let state = use_context::(); let class = create_memo({ move || { - if state.selection.with(|sel| sel.contains(&elt.key)) { + if state.selection.with(|sel| sel.contains(&key)) { "selected" } else { "" @@ -52,14 +51,14 @@ pub fn Outline() -> View { move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&elt.key) { - sel.insert(elt.key); + if !sel.remove(&key) { + sel.insert(key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(elt.key); + sel.insert(key); }); } event.stop_propagation(); @@ -70,14 +69,14 @@ pub fn Outline() -> View { if event.key() == "Enter" { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&elt.key) { - sel.insert(elt.key); + if !sel.remove(&key) { + sel.insert(key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(elt.key); + sel.insert(key); }); } event.prevent_default(); @@ -90,7 +89,7 @@ pub fn Outline() -> View { } } }, - key=|elt| elt.key + key=|(key, _)| key.clone() ) } } From 050e2373a64db94caebb4322fb7c70943186387b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:05:40 -0700 Subject: [PATCH 04/12] App: store constraints Draft listing of constraints in outline view. --- app-proto/sketch-outline/main.css | 43 +++++++++------ app-proto/sketch-outline/src/assembly.rs | 12 ++++- app-proto/sketch-outline/src/main.rs | 17 ++++-- app-proto/sketch-outline/src/outline.rs | 68 ++++++++++++++---------- 4 files changed, 89 insertions(+), 51 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index cd7bc44..fbccab7 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -7,7 +7,7 @@ body { /* outline */ -ul { +#outline { float: left; width: 450px; height: 750px; @@ -19,32 +19,35 @@ ul { } 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 { +.elt { + display: flex; + padding: 3px; + background-color: #444; + border-radius: 8px; +} + +.elt.selected { + color: #fff; + background-color: #666; +} + +.elt > .elt-label { flex-grow: 1; padding: 2px 0px 2px 4px; } -li > .elt-rep { +.elt > .elt-rep { display: flex; } -li > .elt-rep > div { +.elt > .elt-rep > div { padding: 2px; margin-left: 3px; text-align: center; @@ -52,18 +55,26 @@ li > .elt-rep > div { background-color: #333; } -li.selected > .elt-rep > div { +.elt.selected > .elt-rep > div { background-color: #555; } -li > .elt-rep > div:first-child { +.elt-rep > div:first-child { border-radius: 6px 0px 0px 6px; } -li > .elt-rep > div:last-child { +.elt-rep > div:last-child { border-radius: 0px 6px 6px 0px; } +.constraints > li { + margin-top: 4px; + margin-bottom: 4px; + padding: 5px; + background-color: #444; + border-radius: 8px; +} + /* display */ canvas { diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index 79912b9..4cd769c 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,4 +1,5 @@ use nalgebra::DVector; +use rustc_hash::FxHashSet; use slab::Slab; use sycamore::reactive::Signal; @@ -7,11 +8,18 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector + pub rep: DVector, + pub constraints: FxHashSet +} + +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 elements: Signal>, + pub constraints: Signal> } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index a43b020..20ae651 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -21,7 +21,8 @@ fn main() { sycamore::render(|| { let state = AppState { assembly: Assembly { - elements: create_signal(Slab::new()) + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()) }, selection: create_signal(FxHashSet::default()) }; @@ -31,7 +32,13 @@ fn main() { 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: { + let mut set = FxHashSet::default(); + set.insert(1); + set.insert(2); + set + } } ) ); @@ -41,7 +48,8 @@ fn main() { 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() }, ) ); @@ -51,7 +59,8 @@ fn main() { 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() } ) ); diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 031f9bc..4cb3901 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -18,6 +18,7 @@ pub fn Outline() -> View { view! { ul( + id="outline", on:click={ let state = use_context::(); move |_| state.selection.update(|sel| sel.clear()) @@ -30,9 +31,9 @@ pub fn Outline() -> View { let class = create_memo({ move || { if state.selection.with(|sel| sel.contains(&key)) { - "selected" + "elt selected" } else { - "" + "elt" } } }); @@ -44,29 +45,12 @@ pub fn Outline() -> View { view! { /* [TO DO] switch to integer-valued parameters whenever that becomes possible again */ - li( - class=class.get(), - tabindex="0", - 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(); - } - }, - on:keydown={ - move |event: KeyboardEvent| { - if event.key() == "Enter" { + li { + div( + class=class.get(), + tabindex="0", + on:click={ + move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { if !sel.remove(&key) { @@ -79,13 +63,39 @@ pub fn Outline() -> View { sel.insert(key); }); } - event.prevent_default(); + event.stop_propagation(); + } + }, + on:keydown={ + move |event: KeyboardEvent| { + if event.key() == "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(); + } } } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=|c_key: usize| view! { li { (c_key.to_string()) } }, + key=|c_key| c_key.clone() + ) } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } } } }, From 4a24a019285fb09ddba459b301c134bbd95d571c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:40:31 -0700 Subject: [PATCH 05/12] App: insert constraints consistently Also, write constructors for state objects. --- app-proto/sketch-outline/src/assembly.rs | 20 +++++++++++++- app-proto/sketch-outline/src/main.rs | 34 +++++++++++++----------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index 4cd769c..a4c9a0f 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,7 +1,7 @@ use nalgebra::DVector; use rustc_hash::FxHashSet; use slab::Slab; -use sycamore::reactive::Signal; +use sycamore::prelude::*; #[derive(Clone, PartialEq)] pub struct Element { @@ -22,4 +22,22 @@ pub struct Constraint { 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/main.rs b/app-proto/sketch-outline/src/main.rs index 20ae651..b86f979 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -7,7 +7,7 @@ use rustc_hash::FxHashSet; use slab::Slab; use sycamore::prelude::*; -use assembly::{Assembly, Element}; +use assembly::{Assembly, Constraint, Element}; use display::Display; use outline::Outline; @@ -17,32 +17,30 @@ struct AppState { 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(Slab::new()), - constraints: create_signal(Slab::new()) - }, - selection: create_signal(FxHashSet::default()) - }; - state.assembly.elements.update( + 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: { - let mut set = FxHashSet::default(); - set.insert(1); - set.insert(2); - set - } + constraints: FxHashSet::default() } ) ); - state.assembly.elements.update( + let key_b = state.assembly.elements.update( |elts| elts.insert( Element { id: String::from("wing_b"), @@ -64,6 +62,10 @@ fn main() { } ) ); + state.assembly.insert_constraint(Constraint { + args: (key_a, key_b), + rep: 0.5 + }); provide_context(state); view! { From edee153e37480a013a78eb0709dbde199ea5d91a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:50:16 -0700 Subject: [PATCH 06/12] App: remove unused import --- app-proto/sketch-outline/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index b86f979..0466724 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -4,7 +4,6 @@ mod outline; use nalgebra::DVector; use rustc_hash::FxHashSet; -use slab::Slab; use sycamore::prelude::*; use assembly::{Assembly, Constraint, Element}; From 7709c61f7112579b607511092428a09a23ce6ce2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:55:07 -0700 Subject: [PATCH 07/12] Outline: spruce up styling Use `details` elements to hide and show constraints. --- app-proto/sketch-outline/main.css | 95 +++++++++--------- app-proto/sketch-outline/src/outline.rs | 125 +++++++++++++++--------- 2 files changed, 123 insertions(+), 97 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index fbccab7..c3317ea 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -1,6 +1,5 @@ body { - margin-left: 20px; - margin-top: 20px; + margin: 0px; color: #fcfcfc; background-color: #222; } @@ -10,76 +9,72 @@ body { #outline { float: left; width: 450px; - height: 750px; + height: 100vh; margin: 0px; - padding: 8px; - border: 1px solid #555; - border-radius: 16px; - box-sizing: border-box; + padding: 0px; + border-width: 0px 1px 0px 0px; + border-style: solid; + border-color: #555; + overflow-y: scroll; } -li { - list-style-type: none; -} - -li:not(:last-child) { - margin-bottom: 8px; -} - -.elt { +summary { display: flex; - padding: 3px; - background-color: #444; - border-radius: 8px; + user-select: none; } -.elt.selected { +summary.selected { color: #fff; - background-color: #666; + background-color: #444; } -.elt > .elt-label { +summary > div, .cst { + padding-top: 4px; + padding-bottom: 4px; +} + +.elt, .cst { + display: flex; flex-grow: 1; - padding: 2px 0px 2px 4px; + padding-left: 8px; + padding-right: 8px; } -.elt > .elt-rep { +.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; +} + +.elt-rep { display: flex; } -.elt > .elt-rep > div { - padding: 2px; - margin-left: 3px; +.elt-rep > div { + padding: 2px 0px 0px 0px; + font-size: 10pt; text-align: center; - width: 60px; - background-color: #333; -} - -.elt.selected > .elt-rep > div { - background-color: #555; -} - -.elt-rep > div:first-child { - border-radius: 6px 0px 0px 6px; -} - -.elt-rep > div:last-child { - border-radius: 0px 6px 6px 0px; -} - -.constraints > li { - margin-top: 4px; - margin-bottom: 4px; - padding: 5px; - background-color: #444; - border-radius: 8px; + width: 56px; } /* display */ canvas { float: left; - margin-left: 16px; + margin-left: 20px; + margin-top: 20px; background-color: #020202; border: 1px solid #555; border-radius: 16px; diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 4cb3901..e71a4ab 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{KeyboardEvent, MouseEvent}; +use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::AppState; @@ -31,9 +31,9 @@ pub fn Outline() -> View { let class = create_memo({ move || { if state.selection.with(|sel| sel.contains(&key)) { - "elt selected" + "selected" } else { - "elt" + "" } } }); @@ -42,59 +42,90 @@ pub fn Outline() -> View { 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 { - div( - class=class.get(), - tabindex="0", - 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(); - } - }, - on:keydown={ - move |event: KeyboardEvent| { - if event.key() == "Enter" { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); + 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); + }); } - }); - } 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"); + }, + _ => () } - event.prevent_default(); } } + ) { + 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=|c_key: usize| view! { + li(class="cst") { + (c_key.to_string()) + } + }, + key=|c_key| c_key.clone() + ) } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - ul(class="constraints") { - Keyed( - list=elt.constraints.into_iter().collect::>(), - view=|c_key: usize| view! { li { (c_key.to_string()) } }, - key=|c_key| c_key.clone() - ) } } } From fc85d15f8307a337341e33e533c1c236eb33378c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 23 Sep 2024 00:39:14 -0700 Subject: [PATCH 08/12] Outline: show constraint details --- app-proto/sketch-outline/main.css | 15 +++++++++++++-- app-proto/sketch-outline/src/assembly.rs | 1 + app-proto/sketch-outline/src/outline.rs | 24 +++++++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index c3317ea..51cdb3c 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -18,9 +18,12 @@ body { overflow-y: scroll; } +li { + user-select: none; +} + summary { display: flex; - user-select: none; } summary.selected { @@ -58,17 +61,25 @@ details[open]:has(li) .elt-switch::after { flex-grow: 1; } +.cst-label { + flex-grow: 1; +} + .elt-rep { display: flex; } -.elt-rep > div { +.elt-rep > div, .cst-rep { padding: 2px 0px 0px 0px; font-size: 10pt; text-align: center; width: 56px; } +.cst { + font-style: italic; +} + /* display */ canvas { diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index a4c9a0f..6fac59f 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -12,6 +12,7 @@ pub struct Element { pub constraints: FxHashSet } +#[derive(Clone)] pub struct Constraint { pub args: (usize, usize), pub rep: f64 diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index e71a4ab..be5a9b1 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -4,6 +4,12 @@ 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 @@ -118,9 +124,21 @@ pub fn Outline() -> View { ul(class="constraints") { Keyed( list=elt.constraints.into_iter().collect::>(), - view=|c_key: usize| view! { - li(class="cst") { - (c_key.to_string()) + 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() From e6281cdcc6377fd7b0d3416748608be15efd5136 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Sep 2024 14:48:58 -0700 Subject: [PATCH 09/12] Display: shrink canvas to 600px This makes profiling more comparable with `inversive-display`. --- app-proto/sketch-outline/src/display.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 2ac8ebf..52b2ae9 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -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" { From 7ff1b9cb6591c48b3457a7415bfc644c54d4e4e4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:22:48 -0700 Subject: [PATCH 10/12] App: rename directory --- app-proto/{sketch-outline => full-interface}/.gitignore | 0 app-proto/{sketch-outline => full-interface}/Cargo.toml | 0 app-proto/{sketch-outline => full-interface}/index.html | 0 app-proto/{sketch-outline => full-interface}/main.css | 0 app-proto/{sketch-outline => full-interface}/src/assembly.rs | 0 app-proto/{sketch-outline => full-interface}/src/display.rs | 0 app-proto/{sketch-outline => full-interface}/src/identity.vert | 0 app-proto/{sketch-outline => full-interface}/src/inversive.frag | 0 app-proto/{sketch-outline => full-interface}/src/main.rs | 0 app-proto/{sketch-outline => full-interface}/src/outline.rs | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename app-proto/{sketch-outline => full-interface}/.gitignore (100%) rename app-proto/{sketch-outline => full-interface}/Cargo.toml (100%) rename app-proto/{sketch-outline => full-interface}/index.html (100%) rename app-proto/{sketch-outline => full-interface}/main.css (100%) rename app-proto/{sketch-outline => full-interface}/src/assembly.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/display.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/identity.vert (100%) rename app-proto/{sketch-outline => full-interface}/src/inversive.frag (100%) rename app-proto/{sketch-outline => full-interface}/src/main.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/outline.rs (100%) diff --git a/app-proto/sketch-outline/.gitignore b/app-proto/full-interface/.gitignore similarity index 100% rename from app-proto/sketch-outline/.gitignore rename to app-proto/full-interface/.gitignore diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/full-interface/Cargo.toml similarity index 100% rename from app-proto/sketch-outline/Cargo.toml rename to app-proto/full-interface/Cargo.toml 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/sketch-outline/main.css b/app-proto/full-interface/main.css similarity index 100% rename from app-proto/sketch-outline/main.css rename to app-proto/full-interface/main.css diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/full-interface/src/assembly.rs similarity index 100% rename from app-proto/sketch-outline/src/assembly.rs rename to app-proto/full-interface/src/assembly.rs diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/full-interface/src/display.rs similarity index 100% rename from app-proto/sketch-outline/src/display.rs rename to app-proto/full-interface/src/display.rs 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 100% rename from app-proto/sketch-outline/src/main.rs rename to app-proto/full-interface/src/main.rs diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/full-interface/src/outline.rs similarity index 100% rename from app-proto/sketch-outline/src/outline.rs rename to app-proto/full-interface/src/outline.rs From 4e3c86fb717fb7a0b25a63afe34427c76a60446a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:23:56 -0700 Subject: [PATCH 11/12] Ignore profiling folders --- app-proto/full-interface/.gitignore | 1 + app-proto/inversive-display/.gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/app-proto/full-interface/.gitignore b/app-proto/full-interface/.gitignore index 238273d..19aa86b 100644 --- a/app-proto/full-interface/.gitignore +++ b/app-proto/full-interface/.gitignore @@ -1,3 +1,4 @@ target dist +profiling Cargo.lock \ 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 From f5486fb0dd2c4f5f59ea52782af464c2faa4c714 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 15:02:51 -0700 Subject: [PATCH 12/12] AddRemove: make a button that adds constraints --- app-proto/full-interface/main.css | 28 ++++++++++- app-proto/full-interface/src/add_remove.rs | 55 ++++++++++++++++++++++ app-proto/full-interface/src/main.rs | 7 ++- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 app-proto/full-interface/src/add_remove.rs diff --git a/app-proto/full-interface/main.css b/app-proto/full-interface/main.css index 51cdb3c..a687aac 100644 --- a/app-proto/full-interface/main.css +++ b/app-proto/full-interface/main.css @@ -4,9 +4,11 @@ body { background-color: #222; } -/* outline */ +/* sidebar */ -#outline { +#sidebar { + display: flex; + flex-direction: column; float: left; width: 450px; height: 100vh; @@ -15,6 +17,28 @@ body { 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; } 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/main.rs b/app-proto/full-interface/src/main.rs index 0466724..2f31ada 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -1,3 +1,4 @@ +mod add_remove; mod assembly; mod display; mod outline; @@ -6,6 +7,7 @@ use nalgebra::DVector; use rustc_hash::FxHashSet; use sycamore::prelude::*; +use add_remove::AddRemove; use assembly::{Assembly, Constraint, Element}; use display::Display; use outline::Outline; @@ -68,7 +70,10 @@ fn main() { provide_context(state); view! { - Outline {} + div(id="sidebar") { + AddRemove {} + Outline {} + } Display {} } });