Compare commits

...

12 Commits

Author SHA1 Message Date
Aaron Fenyes
f5486fb0dd AddRemove: make a button that adds constraints 2024-09-26 15:02:51 -07:00
Aaron Fenyes
4e3c86fb71 Ignore profiling folders 2024-09-26 13:23:56 -07:00
Aaron Fenyes
7ff1b9cb65 App: rename directory 2024-09-26 13:22:48 -07:00
Aaron Fenyes
e6281cdcc6 Display: shrink canvas to 600px
This makes profiling more comparable with `inversive-display`.
2024-09-25 14:48:58 -07:00
Aaron Fenyes
fc85d15f83 Outline: show constraint details 2024-09-23 00:39:14 -07:00
Aaron Fenyes
7709c61f71 Outline: spruce up styling
Use `details` elements to hide and show constraints.
2024-09-22 23:55:07 -07:00
Aaron Fenyes
edee153e37 App: remove unused import 2024-09-22 23:50:16 -07:00
Aaron Fenyes
4a24a01928 App: insert constraints consistently
Also, write constructors for state objects.
2024-09-22 14:40:31 -07:00
Aaron Fenyes
050e2373a6 App: store constraints
Draft listing of constraints in outline view.
2024-09-22 14:05:40 -07:00
Aaron Fenyes
147e275823 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.
2024-09-22 02:38:17 -07:00
Aaron Fenyes
d121385c18 App: store assembly elements in slab 2024-09-22 02:21:45 -07:00
Aaron Fenyes
78f8ef8215 Outline: switch to single selection 2024-09-19 17:53:07 -07:00
16 changed files with 419 additions and 206 deletions

4
app-proto/full-interface/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
dist
profiling
Cargo.lock

View File

@ -12,6 +12,7 @@ itertools = "0.13.0"
js-sys = "0.3.70" js-sys = "0.3.70"
nalgebra = "0.33.0" nalgebra = "0.33.0"
rustc-hash = "2.0.0" rustc-hash = "2.0.0"
slab = "0.4.9"
sycamore = "0.9.0-beta.3" sycamore = "0.9.0-beta.3"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by

View File

@ -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;
}

View File

@ -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::<AppState>();
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());
}
) { "🔗" }
}
}
}

View File

@ -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<f64>,
pub constraints: FxHashSet<usize>
}
#[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<Slab<Element>>,
pub constraints: Signal<Slab<Constraint>>
}
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);
})
}
}

View File

@ -288,17 +288,17 @@ pub fn Display() -> View {
// get the assembly // get the assembly
let elements = state.assembly.elements.get_clone(); let elements = state.assembly.elements.get_clone();
let element_iter = (&elements).values(); let element_iter = (&elements).into_iter();
let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect();
let colors: Vec<_> = element_iter.clone().map(|elt| let colors: Vec<_> = element_iter.clone().map(|(key, elt)|
if state.selection.with(|sel| sel.contains(&elt.id)) { if state.selection.with(|sel| sel.contains(&key)) {
elt.color.map(|ch| 0.2 + 0.8*ch) elt.color.map(|ch| 0.2 + 0.8*ch)
} else { } else {
elt.color elt.color
} }
).collect(); ).collect();
let highlights: Vec<_> = element_iter.map(|elt| let highlights: Vec<_> = element_iter.map(|(key, _)|
if state.selection.with(|sel| sel.contains(&elt.id)) { if state.selection.with(|sel| sel.contains(&key)) {
1.0_f32 1.0_f32
} else { } else {
HIGHLIGHT HIGHLIGHT
@ -386,8 +386,8 @@ pub fn Display() -> View {
// again // again
canvas( canvas(
ref=display, ref=display,
width="750", width="600",
height="750", height="600",
tabindex="0", tabindex="0",
on:keydown=move |event: KeyboardEvent| { on:keydown=move |event: KeyboardEvent| {
if event.key() == "Shift" { if event.key() == "Shift" {

View File

@ -1,66 +1,79 @@
mod add_remove;
mod assembly; mod assembly;
mod display; mod display;
mod outline; mod outline;
use nalgebra::DVector; use nalgebra::DVector;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::FxHashSet;
use sycamore::prelude::*; use sycamore::prelude::*;
use assembly::{Assembly, Element}; use add_remove::AddRemove;
use assembly::{Assembly, Constraint, Element};
use display::Display; use display::Display;
use outline::Outline; use outline::Outline;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
assembly: Assembly, assembly: Assembly,
selection: Signal<FxHashSet<String>> selection: Signal<FxHashSet<usize>>
}
impl AppState {
fn new() -> AppState {
AppState {
assembly: Assembly::new(),
selection: create_signal(FxHashSet::default())
}
}
} }
fn main() { fn main() {
sycamore::render(|| { sycamore::render(|| {
let state = AppState { let state = AppState::new();
assembly: Assembly { let key_a = state.assembly.elements.update(
elements: create_signal(FxHashMap::default())
},
selection: create_signal(FxHashSet::default())
};
state.assembly.elements.update(
|elts| elts.insert( |elts| elts.insert(
"wing_a".to_string(),
Element { Element {
id: String::from("wing_a"), id: String::from("wing_a"),
label: String::from("Wing A"), label: String::from("Wing A"),
color: [1.00_f32, 0.25_f32, 0.00_f32], 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]) rep: DVector::<f64>::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( |elts| elts.insert(
"wing_b".to_string(),
Element { Element {
id: String::from("wing_b"), id: String::from("wing_b"),
label: String::from("Wing B"), label: String::from("Wing B"),
color: [0.00_f32, 0.25_f32, 1.00_f32], 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]) rep: DVector::<f64>::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]),
constraints: FxHashSet::default()
}, },
) )
); );
state.assembly.elements.update( state.assembly.elements.update(
|elts| elts.insert( |elts| elts.insert(
"central".to_string(),
Element { Element {
id: String::from("central"), id: String::from("central"),
label: String::from("Central"), label: String::from("Central"),
color: [0.75_f32, 0.75_f32, 0.75_f32], 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]) 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); provide_context(state);
view! { view! {
div(id="sidebar") {
AddRemove {}
Outline {} Outline {}
}
Display {} Display {}
} }
}); });

View File

@ -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::<AppState>();
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::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list=elements_sorted,
view=|(key, elt)| {
let state = use_context::<AppState>();
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::<Vec<_>>();
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::<Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<Element>()
.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::<Vec<_>>(),
view=move |c_key: usize| {
let c_state = use_context::<AppState>();
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()
)
}
}
}

View File

@ -1,3 +1,4 @@
target target
dist dist
profiling
Cargo.lock Cargo.lock

View File

@ -1,3 +0,0 @@
target
dist
Cargo.lock

View File

@ -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;
}

View File

@ -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<f64>
}
// 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<FxHashMap<String, Element>>
}

View File

@ -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::<AppState>();
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::<AppState>();
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::<Vec<_>>();
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()
)
}
}
}