Compare commits

..

No commits in common. "f5486fb0dd2c4f5f59ea52782af464c2faa4c714" and "96f8b6b5f34c5374b4aebab3cf5d9e582e37ecb3" have entirely different histories.

16 changed files with 206 additions and 419 deletions

View File

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

View File

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

View File

@ -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::<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

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

@ -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::<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,4 +1,3 @@
target target
dist dist
profiling
Cargo.lock Cargo.lock

3
app-proto/sketch-outline/.gitignore vendored Normal file
View File

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

View File

@ -12,7 +12,6 @@ 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,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;
}

View File

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

@ -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).into_iter(); let element_iter = (&elements).values();
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(|(key, elt)| let colors: Vec<_> = element_iter.clone().map(|elt|
if state.selection.with(|sel| sel.contains(&key)) { if state.selection.with(|sel| sel.contains(&elt.id)) {
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(|(key, _)| let highlights: Vec<_> = element_iter.map(|elt|
if state.selection.with(|sel| sel.contains(&key)) { if state.selection.with(|sel| sel.contains(&elt.id)) {
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="600", width="750",
height="600", height="750",
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,79 +1,66 @@
mod add_remove;
mod assembly; mod assembly;
mod display; mod display;
mod outline; mod outline;
use nalgebra::DVector; use nalgebra::DVector;
use rustc_hash::FxHashSet; use rustc_hash::{FxHashMap, FxHashSet};
use sycamore::prelude::*; use sycamore::prelude::*;
use add_remove::AddRemove; use assembly::{Assembly, Element};
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<usize>> selection: Signal<FxHashSet<String>>
}
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::new(); let state = AppState {
let key_a = state.assembly.elements.update( assembly: Assembly {
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()
} }
) )
); );
let key_b = state.assembly.elements.update( 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,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::<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()
)
}
}
}