All checks were successful
/ test (push) Successful in 2m17s
Replaces the former sole Element entity by two, Sphere and Point, both implementing an Element trait. Adds Point display, uses the former Element display for Sphere. Adds a new "canned" configuration, and the ability to add, select, and nudge Point entities. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: #82 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
288 lines
No EOL
9.6 KiB
Rust
288 lines
No EOL
9.6 KiB
Rust
use itertools::Itertools;
|
|
use std::rc::Rc;
|
|
use sycamore::prelude::*;
|
|
use web_sys::{
|
|
KeyboardEvent,
|
|
MouseEvent,
|
|
wasm_bindgen::JsCast
|
|
};
|
|
|
|
use crate::{
|
|
AppState,
|
|
assembly::{
|
|
Element,
|
|
ElementKey,
|
|
ElementRc,
|
|
HalfCurvatureRegulator,
|
|
InversiveDistanceRegulator,
|
|
Regulator,
|
|
RegulatorKey
|
|
},
|
|
specified::SpecifiedValue
|
|
};
|
|
|
|
// an editable view of a regulator
|
|
#[component(inline_props)]
|
|
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|
// get the regulator's measurement and set point signals
|
|
let measurement = regulator.measurement();
|
|
let set_point = regulator.set_point();
|
|
|
|
// the `valid` signal tracks whether the last entered value is a valid set
|
|
// point specification
|
|
let valid = create_signal(true);
|
|
|
|
// the `value` signal holds the current set point specification
|
|
let value = create_signal(
|
|
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
|
);
|
|
|
|
// this `reset_value` closure resets the input value to the regulator's set
|
|
// point specification
|
|
let reset_value = move || {
|
|
batch(|| {
|
|
valid.set(true);
|
|
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
|
})
|
|
};
|
|
|
|
// reset the input value whenever the regulator's set point specification
|
|
// is updated
|
|
create_effect(reset_value);
|
|
|
|
view! {
|
|
input(
|
|
r#type="text",
|
|
class=move || {
|
|
if valid.get() {
|
|
set_point.with(|set_pt| {
|
|
if set_pt.is_present() {
|
|
"regulator-input constraint"
|
|
} else {
|
|
"regulator-input"
|
|
}
|
|
})
|
|
} else {
|
|
"regulator-input invalid"
|
|
}
|
|
},
|
|
placeholder=measurement.with(|result| result.to_string()),
|
|
bind:value=value,
|
|
on:change=move |_| {
|
|
valid.set(
|
|
match SpecifiedValue::try_from(value.get_clone_untracked()) {
|
|
Ok(set_pt) => {
|
|
set_point.set(set_pt);
|
|
true
|
|
}
|
|
Err(_) => false
|
|
}
|
|
)
|
|
},
|
|
on:keydown={
|
|
move |event: KeyboardEvent| {
|
|
match event.key().as_str() {
|
|
"Escape" => reset_value(),
|
|
_ => ()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
pub trait OutlineItem {
|
|
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View;
|
|
}
|
|
|
|
impl OutlineItem for InversiveDistanceRegulator {
|
|
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View {
|
|
let state = use_context::<AppState>();
|
|
let other_subject = if self.subjects[0] == element_key {
|
|
self.subjects[1]
|
|
} else {
|
|
self.subjects[0]
|
|
};
|
|
let other_subject_label = state.assembly.elements.with(
|
|
|elts| elts[other_subject].label().clone()
|
|
);
|
|
view! {
|
|
li(class="regulator") {
|
|
div(class="regulator-label") { (other_subject_label) }
|
|
div(class="regulator-type") { "Inversive distance" }
|
|
RegulatorInput(regulator=self)
|
|
div(class="status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OutlineItem for HalfCurvatureRegulator {
|
|
fn outline_item(self: Rc<Self>, _element_key: ElementKey) -> View {
|
|
view! {
|
|
li(class="regulator") {
|
|
div(class="regulator-label") // for spacing
|
|
div(class="regulator-type") { "Half-curvature" }
|
|
RegulatorInput(regulator=self)
|
|
div(class="status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a list item that shows a regulator in an outline view of an element
|
|
#[component(inline_props)]
|
|
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
|
|
let state = use_context::<AppState>();
|
|
let regulator = state.assembly.regulators.with(
|
|
|regs| regs[regulator_key].clone()
|
|
);
|
|
regulator.outline_item(element_key)
|
|
}
|
|
|
|
// a list item that shows an element in an outline view of an assembly
|
|
#[component(inline_props)]
|
|
fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
|
|
let state = use_context::<AppState>();
|
|
let class = state.selection.map(
|
|
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
|
);
|
|
let label = element.label().clone();
|
|
let representation = element.representation().clone();
|
|
let rep_components = move || {
|
|
representation.with(
|
|
|rep| rep.iter().map(
|
|
|u| {
|
|
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
|
view! { div { (u_str) } }
|
|
}
|
|
).collect::<Vec<_>>()
|
|
)
|
|
};
|
|
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
|
let regulator_list = element.regulators().map(
|
|
move |elt_reg_keys| elt_reg_keys
|
|
.clone()
|
|
.into_iter()
|
|
.sorted_by_key(
|
|
|®_key| state.assembly.regulators.with(
|
|
|regs| regs[reg_key].subjects().len()
|
|
)
|
|
)
|
|
.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" => {
|
|
state.select(key, event.shift_key());
|
|
event.prevent_default();
|
|
},
|
|
"ArrowRight" if regulated.get() => {
|
|
let _ = details_node
|
|
.get()
|
|
.unchecked_into::<web_sys::Element>()
|
|
.set_attribute("open", "");
|
|
},
|
|
"ArrowLeft" => {
|
|
let _ = details_node
|
|
.get()
|
|
.unchecked_into::<web_sys::Element>()
|
|
.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") { (rep_components) }
|
|
div(class="status")
|
|
}
|
|
}
|
|
ul(class="regulators") {
|
|
Keyed(
|
|
list=regulator_list,
|
|
view=move |reg_key| view! {
|
|
RegulatorOutlineItem(
|
|
regulator_key=reg_key,
|
|
element_key=key
|
|
)
|
|
},
|
|
key=|reg_key| reg_key.clone()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a component that lists the elements of the current assembly, showing each
|
|
// element's regulators in 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 {
|
|
let state = use_context::<AppState>();
|
|
|
|
// 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())
|
|
.map(|(key, elt)| (key, ElementRc(elt)))
|
|
.collect()
|
|
);
|
|
|
|
view! {
|
|
ul(
|
|
id="outline",
|
|
on:click={
|
|
let state = use_context::<AppState>();
|
|
move |_| state.selection.update(|sel| sel.clear())
|
|
}
|
|
) {
|
|
Keyed(
|
|
list=element_list,
|
|
view=|(key, ElementRc(elt))| view! {
|
|
ElementOutlineItem(key=key, element=elt)
|
|
},
|
|
key=|(_, ElementRc(elt))| elt.serial()
|
|
)
|
|
}
|
|
}
|
|
} |