From 65cee1ecc236402ef4b5a462778cd8df51c58908 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 15 Nov 2024 03:32:47 +0000 Subject: [PATCH] Clean up the outline view (#19) Clean up the source code and interface of the outline view. In addition, [fix a bug](commit/6e42681b719d7ec97c4225ca321225979bf87b56) that could cause `Assembly::realize` to react to itself under certain circumstances. Those circumstances arose, making the bug noticeable, while this branch was being written. #### Source code - Modularize the `Outline` component into smaller components. - Switch from static iteration to dynamic Sycamore lists. This reduces the amount of re-rendering that happens when an element or constraint changes. It also allows constraint details to stay open or closed during constraint updates, rather than resetting to closed. - Make `Element::index` private, as discussed [here](pulls/15#issuecomment-1816). #### Interface - Make constraints editable, updating the assembly realization on input. Flag constraints where the Lorentz product value doesn't parse. - Round element vector coordinates to prevent the displayed strings from overlapping. Note that issue #20 was created by this PR, but it will be addressed shortly. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/19 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 4 +- app-proto/index.html | 4 +- app-proto/main.css | 90 +++++++++--- app-proto/src/add_remove.rs | 222 +++++++++++++--------------- app-proto/src/assembly.rs | 71 +++++---- app-proto/src/display.rs | 61 +++++--- app-proto/src/outline.rs | 280 +++++++++++++++++++----------------- 7 files changed, 404 insertions(+), 328 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e5bc05e..e623b26 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "sketch-outline" +name = "dyna3" version = "0.1.0" -authors = ["Aaron"] +authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" [features] diff --git a/app-proto/index.html b/app-proto/index.html index 5474fe9..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -2,8 +2,10 @@ - Sketch outline + dyna3 + + diff --git a/app-proto/main.css b/app-proto/main.css index 32ae5bf..b9fc0a1 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -1,7 +1,20 @@ +:root { + --text: #fcfcfc; /* almost white */ + --text-bright: white; + --text-invalid: #f58fc2; /* bright pink */ + --border: #555; /* light gray */ + --border-focus: #aaa; /* bright gray */ + --border-invalid: #70495c; /* dusky pink */ + --selection-highlight: #444; /* medium gray */ + --page-background: #222; /* dark gray */ + --display-background: #020202; /* almost black */ +} + body { margin: 0px; - color: #fcfcfc; - background-color: #222; + color: var(--text); + background-color: var(--page-background); + font-family: 'Fira Sans', sans-serif; } /* sidebar */ @@ -16,7 +29,7 @@ body { padding: 0px; border-width: 0px 1px 0px 0px; border-style: solid; - border-color: #555; + border-color: var(--border); } /* add-remove */ @@ -33,6 +46,15 @@ body { font-size: large; } +/* KLUDGE */ +/* + for convenience, we're using emoji as temporary icons for some buttons. these + buttons need to be displayed in an emoji font +*/ +#add-remove > button.emoji { + font-family: 'Noto Emoji', sans-serif; +} + /* outline */ #outline { @@ -51,81 +73,103 @@ summary { } summary.selected { - color: #fff; - background-color: #444; + color: var(--text-bright); + background-color: var(--selection-highlight); } -summary > div, .cst { +summary > div, .constraint { padding-top: 4px; padding-bottom: 4px; } -.elt, .cst { +.element, .constraint { display: flex; flex-grow: 1; padding-left: 8px; padding-right: 8px; } -.elt-switch { +.element-switch { width: 18px; padding-left: 2px; text-align: center; } -details:has(li) .elt-switch::after { +details:has(li) .element-switch::after { content: '▸'; } -details[open]:has(li) .elt-switch::after { +details[open]:has(li) .element-switch::after { content: '▾'; } -.elt-label { +.element-label { flex-grow: 1; } -.cst-label { +.constraint-label { flex-grow: 1; } -.elt-rep { +.element-representation { display: flex; } -.elt-rep > div { +.element-representation > div { padding: 2px 0px 0px 0px; font-size: 10pt; - text-align: center; + font-variant-numeric: tabular-nums; + text-align: right; width: 56px; } -.cst { +.constraint { font-style: italic; } -.cst > input[type=checkbox] { +.constraint.invalid { + color: var(--text-invalid); +} + +.constraint > input[type=checkbox] { margin: 0px 8px 0px 0px; } -.cst > input[type=text] { - color: #fcfcfc; +.constraint > input[type=text] { + color: inherit; background-color: inherit; - border: 1px solid #555; + border: 1px solid var(--border); border-radius: 2px; } +.constraint.invalid > input[type=text] { + border-color: var(--border-invalid); +} + +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + +.invalid > .status::after, details:has(.invalid):not([open]) .status::after { + content: '⚠'; + color: var(--text-invalid); +} + /* display */ canvas { float: left; margin-left: 20px; margin-top: 20px; - background-color: #020202; - border: 1px solid #555; + background-color: var(--display-background); + border: 1px solid var(--border); border-radius: 16px; } canvas:focus { - border-color: #aaa; + border-color: var(--border-focus); } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 19b4b8d..ba02e65 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,155 +1,130 @@ -use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_a"), - label: String::from("Castor"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - representation: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_b"), - label: String::from("Pollux"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_major"), - label: String::from("Ursa major"), - color: [0.25_f32, 0.00_f32, 1.00_f32], - representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_minor"), - label: String::from("Ursa minor"), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_deimos"), - label: String::from("Deimos"), - color: [0.75_f32, 0.75_f32, 0.00_f32], - representation: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_phobos"), - label: String::from("Phobos"), - color: [0.00_f32, 0.75_f32, 0.50_f32], - representation: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) ); } /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( - Element { - id: "central".to_string(), - label: "Central".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "assemb_plane".to_string(), - label: "Assembly plane".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side1".to_string(), - label: "Side 1".to_string(), - color: [1.00_f32, 0.00_f32, 0.25_f32], - representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side2".to_string(), - label: "Side 2".to_string(), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side3".to_string(), - label: "Side 3".to_string(), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner1".to_string(), - label: "Corner 1".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner2".to_string(), - label: "Corner 2".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("corner3"), - label: String::from("Corner 3"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); } @@ -202,6 +177,7 @@ pub fn AddRemove() -> View { } ) { "+" } button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ let state = use_context::(); state.selection.with(|sel| sel.len() != 2) @@ -215,15 +191,15 @@ pub fn AddRemove() -> View { } ); let lorentz_prod = create_signal(0.0); + let lorentz_prod_valid = create_signal(false); let active = create_signal(true); state.assembly.insert_constraint(Constraint { subjects: subjects, lorentz_prod: lorentz_prod, lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: create_signal(false), + lorentz_prod_valid: lorentz_prod_valid, active: active, }); - state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ @@ -241,23 +217,23 @@ pub fn AddRemove() -> View { } }); - // update the realization when the constraint activated, or - // edited while active + // update the realization when the constraint becomes active + // and valid, or is edited while active and valid create_effect(move || { + console::log_1(&JsValue::from( + format!("Constraint ({}, {}) updated", subjects.0, subjects.1) + )); lorentz_prod.track(); - console::log_2( - &JsValue::from("Lorentz product updated to"), - &JsValue::from(lorentz_prod.get_untracked()) - ); - if active.get() { + if active.get() && lorentz_prod_valid.get() { state.assembly.realize(); } }); } ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ + select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="empty") { "Empty" } } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0cdf61b..35b4417 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,17 +18,33 @@ pub struct Element { pub id: String, pub label: String, pub color: ElementColor, - pub representation: DVector, - pub constraints: BTreeSet, + pub representation: Signal>, + pub constraints: Signal>, // the configuration matrix column index that was assigned to this element // last time the assembly was realized - /* TO DO */ - // this is public, as a kludge, because `Element` doesn't have a constructor - // yet. it should be made private as soon as the constructor is written - pub index: usize + column_index: usize } +impl Element { + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Element { + Element { + id: id, + label: label, + color: color, + representation: create_signal(representation), + constraints: create_signal(BTreeSet::default()), + column_index: 0 + } + } +} + + #[derive(Clone)] pub struct Constraint { pub subjects: (ElementKey, ElementKey), @@ -92,24 +108,23 @@ impl Assembly { // create and insert a new element self.insert_element_unchecked( - Element { - id: id, - label: format!("Sphere {}", id_num), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + id, + format!("Sphere {}", id_num), + [0.75_f32, 0.75_f32, 0.75_f32], + DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + ) ); } pub fn insert_constraint(&self, constraint: Constraint) { let subjects = constraint.subjects; let key = self.constraints.update(|csts| csts.insert(constraint)); - self.elements.update(|elts| { - elts[subjects.0].constraints.insert(key); - elts[subjects.1].constraints.insert(key); - }); + let subject_constraints = self.elements.with( + |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + ); + subject_constraints.0.update(|csts| csts.insert(key)); + subject_constraints.1.update(|csts| csts.insert(key)); } // --- realization --- @@ -118,7 +133,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.index = index; + elt.column_index = index; } }); @@ -130,8 +145,8 @@ impl Assembly { for (_, cst) in csts { if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { let subjects = cst.subjects; - let row = elts[subjects.0].index; - let col = elts[subjects.1].index; + let row = elts[subjects.0].column_index; + let col = elts[subjects.1].column_index; gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); } } @@ -141,9 +156,9 @@ impl Assembly { // Gram matrix let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { - let index = elt.index; + let index = elt.column_index; gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation); + guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); } (gram_to_be, guess_to_be) @@ -185,11 +200,11 @@ impl Assembly { if success { // read out the solution - self.elements.update(|elts| { - for (_, elt) in elts.iter_mut() { - elt.representation.set_column(0, &config.column(elt.index)); - } - }); + for (_, elt) in self.elements.get_clone_untracked() { + elt.representation.update( + |rep| rep.set_column(0, &config.column(elt.column_index)) + ); + } } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 79199ec..ee0af47 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -103,7 +103,11 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { - state.assembly.elements.track(); + state.assembly.elements.with(|elts| { + for (_, elt) in elts { + elt.representation.track(); + } + }); state.selection.track(); scene_changed.set(true); }); @@ -295,25 +299,40 @@ pub fn Display() -> View { let assembly_to_world = &location * &orientation; // 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.representation - ).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(|(key, _)| - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - ).collect(); + let ( + elt_cnt, + reps_world, + colors, + highlights + ) = state.assembly.elements.with(|elts| { + ( + // number of elements + elts.len() as i32, + + // representation vectors in world coordinates + elts.iter().map( + |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + ).collect::>(), + + // colors + elts.iter().map(|(key, elt)| { + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + }).collect::>(), + + // highlight levels + elts.iter().map(|(key, _)| { + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + }).collect::>() + ) + }); // set the resolution let width = canvas.width() as f32; @@ -322,7 +341,7 @@ pub fn Display() -> View { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); for n in 0..reps_world.len() { let v = &reps_world[n]; ctx.uniform3f( diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index f7c975c..ee1603f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; +use sycamore::prelude::*; use web_sys::{ - Element, Event, HtmlInputElement, KeyboardEvent, @@ -9,7 +8,7 @@ use web_sys::{ wasm_bindgen::JsCast }; -use crate::{AppState, assembly::Constraint}; +use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; // an editable view of the Lorentz product representing a constraint #[component(inline_props)] @@ -32,6 +31,143 @@ fn LorentzProductInput(constraint: Constraint) -> View { } } +// a list item that shows a constraint in an outline view of an element +#[component(inline_props)] +fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { + let state = use_context::(); + let assembly = &state.assembly; + let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); + let other_subject = if constraint.subjects.0 == element_key { + constraint.subjects.1 + } else { + constraint.subjects.0 + }; + let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); + let class = constraint.lorentz_prod_valid.map( + |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } + ); + view! { + li(class=class.get()) { + input(r#type="checkbox", bind:checked=constraint.active) + div(class="constraint-label") { (other_subject_label) } + LorentzProductInput(constraint=constraint) + div(class="status") + } + } +} + +// a list item that shows an element in an outline view of an assembly +#[component(inline_props)] +fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { + let state = use_context::(); + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); + let label = element.label.clone(); + let rep_components = element.representation.map( + |rep| rep.iter().map( + |u| format!("{:.3}", u).replace("-", "\u{2212}") + ).collect() + ); + let constrained = element.constraints.map(|csts| csts.len() > 0); + let constraint_list = element.constraints.map( + |csts| csts.clone().into_iter().collect() + ); + let details_node = create_node_ref(); + view! { + 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.get() => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="element-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="element", + 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="element-label") { (label) } + div(class="element-representation") { + Indexed( + list=rep_components, + view=|coord_str| view! { + div { (coord_str) } + } + ) + } + div(class="status") + } + } + ul(class="constraints") { + Keyed( + list=constraint_list, + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) + }, + key=|cst_key| cst_key.clone() + ) + } + } + } + } +} + // a component that lists the elements of the current assembly, showing the // constraints on each element as a collapsible sub-list. its implementation // is based on Kate Morley's HTML + CSS tree views: @@ -40,15 +176,16 @@ fn LorentzProductInput(constraint: Constraint) -> View { // #[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() + let state = use_context::(); + + // list the elements alphabetically by ID + let element_list = state.assembly.elements.map( + |elts| elts + .clone() .into_iter() .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() - }); + ); view! { ul( @@ -59,128 +196,11 @@ pub fn Outline() -> View { } ) { 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.representation.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! { - 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| { - 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.subjects.0 == key { - cst.subjects.1 - } else { - cst.subjects.0 - }; - let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); - view! { - li(class="cst") { - input(r#type="checkbox", bind:checked=cst.active) - div(class="cst-label") { (other_arg_label) } - LorentzProductInput(constraint=cst) - } - } - }, - key=|c_key| c_key.clone() - ) - } - } - } - } + list=element_list, + view=|(key, elt)| view! { + ElementOutlineItem(key=key, element=elt) }, - key=|(key, elt)| ( - key.clone(), - elt.id.clone(), - elt.label.clone(), - elt.constraints.clone() - ) + key=|(key, _)| key.clone() ) } }