Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
Aaron Fenyes
befadd25c9 Get the read-only set point signal more simply 2025-02-19 01:22:42 -08:00
Aaron Fenyes
c54b6bc165 Tie invalidity indicator to regulator input
Invalid attempts to specify a regulator's set point are now local to
each view of the regulator, and don't affect the regulator model. In
particular, a regulator will be valid and in force even when one of its
regulator input views is showing an invalid specification attempt. The
invalidity indicator should therefore be tied to the input showing
the invalid specification, not to the whole regulator outline item.
2025-02-18 13:37:30 -08:00
Aaron Fenyes
f2e84fb64a Enforce the validity of set point specifications
Make a regulator's set point specification private, and split the set
point into a private writable signal and a public read-only signal. The
set point can now be initialized only through the factory method
`insert_new_regulator` and changed only through the setter method
`try_specify_set_point`, which both ensure that the set point
specification is valid and consistent with the set point.
2025-02-18 13:29:10 -08:00
Aaron Fenyes
bbd0835a8f Move set point spec validation into Regulator 2025-02-18 12:42:49 -08:00
Aaron Fenyes
302d93638d Require regulators to have valid specifications
When an invalid specification is entered into a regulator input, keep it
confined to that input. Reset a regulator input by pressing *escape*.
2025-02-18 01:27:11 -08:00
Aaron Fenyes
fef4127f69 Only sync regulator inputs on change
This lets us infer a regulator's role from whether it has a set point
and what text specifies the set point.
2025-02-17 14:01:27 -08:00
Aaron Fenyes
b3e4e902f3 Rename Regulator fields 2025-02-12 11:55:45 -08:00
Aaron Fenyes
24139ad5e9 Rename observables to regulators 2025-02-12 11:35:07 -08:00
Aaron Fenyes
de7122d871 Label observable type
Right now, there's only one type of observable, so the label can be
hard-coded.
2025-02-12 10:37:48 -08:00
Aaron Fenyes
dc8330df6a Revise observable styling
Distinguish constraints from observables using dark background rather
than marker. Customize focus highlighting. Drop the input type selector
that used to make styling apply to the Lorentz product field but not the
constraint activation check box.
2025-02-10 00:16:36 -08:00
Aaron Fenyes
af2724f934 Rename ObservableRole variants
Also rename corresponding CSS classes and add methods to check roles.
2025-02-10 00:16:36 -08:00
Aaron Fenyes
677ef47544 Rename constraints to observables 2025-02-10 00:16:36 -08:00
Aaron Fenyes
fb8e391587 Generalize constraints to observables 2025-02-10 00:08:32 -08:00
4 changed files with 222 additions and 124 deletions

View file

@ -3,7 +3,8 @@
--text-bright: white; --text-bright: white;
--text-invalid: #f58fc2; /* bright pink */ --text-invalid: #f58fc2; /* bright pink */
--border: #555; /* light gray */ --border: #555; /* light gray */
--border-focus: #aaa; /* bright gray */ --border-focus-dark: #aaa; /* bright gray */
--border-focus-light: white;
--border-invalid: #70495c; /* dusky pink */ --border-invalid: #70495c; /* dusky pink */
--selection-highlight: #444; /* medium gray */ --selection-highlight: #444; /* medium gray */
--page-background: #222; /* dark gray */ --page-background: #222; /* dark gray */
@ -23,7 +24,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
float: left; float: left;
width: 450px; width: 500px;
height: 100vh; height: 100vh;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
@ -77,12 +78,12 @@ summary.selected {
background-color: var(--selection-highlight); background-color: var(--selection-highlight);
} }
summary > div, .constraint { summary > div, .regulator {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.element, .constraint { .element, .regulator {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
padding-left: 8px; padding-left: 8px;
@ -107,7 +108,7 @@ details[open]:has(li) .element-switch::after {
flex-grow: 1; flex-grow: 1;
} }
.constraint-label { .regulator-label {
flex-grow: 1; flex-grow: 1;
} }
@ -123,26 +124,34 @@ details[open]:has(li) .element-switch::after {
width: 56px; width: 56px;
} }
.constraint { .regulator {
font-style: italic; font-style: italic;
} }
.constraint.invalid { .regulator-type {
color: var(--text-invalid); padding: 2px 8px 0px 8px;
font-size: 10pt;
} }
.constraint > input[type=checkbox] { .regulator-input {
margin: 0px 8px 0px 0px;
}
.constraint > input[type=text] {
color: inherit; color: inherit;
background-color: inherit; background-color: inherit;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 2px; border-radius: 2px;
} }
.constraint.invalid > input[type=text] { .regulator-input::placeholder {
color: inherit;
opacity: 54%;
font-style: italic;
}
.regulator-input.constraint {
background-color: var(--display-background);
}
.regulator-input.invalid {
color: var(--text-invalid);
border-color: var(--border-invalid); border-color: var(--border-invalid);
} }
@ -154,7 +163,7 @@ details[open]:has(li) .element-switch::after {
font-style: normal; font-style: normal;
} }
.invalid > .status::after, details:has(.invalid):not([open]) .status::after { .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠'; content: '⚠';
color: var(--text-invalid); color: var(--text-invalid);
} }
@ -171,5 +180,11 @@ canvas {
} }
canvas:focus { canvas:focus {
border-color: var(--border-focus); border-color: var(--border-focus-dark);
outline: none;
}
input:focus {
border-color: var(--border-focus-light);
outline: none;
} }

View file

@ -1,7 +1,14 @@
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; use web_sys::{console, wasm_bindgen::JsValue};
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; use crate::{
engine,
AppState,
assembly::{
Assembly,
Element
}
};
/* DEBUG */ /* DEBUG */
// load an example assembly for testing. this code will be removed once we've // load an example assembly for testing. this code will be removed once we've
@ -190,44 +197,8 @@ pub fn AddRemove() -> View {
(subject_vec[0].clone(), subject_vec[1].clone()) (subject_vec[0].clone(), subject_vec[1].clone())
} }
); );
let lorentz_prod = create_signal(0.0); state.assembly.insert_new_regulator(subjects);
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: lorentz_prod_valid,
active: active,
});
state.selection.update(|sel| sel.clear()); state.selection.update(|sel| sel.clear());
/* DEBUG */
// print updated constraint list
console::log_1(&JsValue::from("Constraints:"));
state.assembly.constraints.with(|csts| {
for (_, cst) in csts.into_iter() {
console::log_5(
&JsValue::from(" "),
&JsValue::from(cst.subjects.0),
&JsValue::from(cst.subjects.1),
&JsValue::from(":"),
&JsValue::from(cst.lorentz_prod.get_untracked())
);
}
});
// 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();
if active.get() && lorentz_prod_valid.get() {
state.assembly.realize();
}
});
} }
) { "🔗" } ) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser

View file

@ -5,11 +5,11 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; use crate::engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix};
// the types of the keys we use to access an assembly's elements and constraints // the types of the keys we use to access an assembly's elements and regulators
pub type ElementKey = usize; pub type ElementKey = usize;
pub type ConstraintKey = usize; pub type RegulatorKey = usize;
pub type ElementColor = [f32; 3]; pub type ElementColor = [f32; 3];
@ -26,7 +26,7 @@ pub struct Element {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub constraints: Signal<BTreeSet<ConstraintKey>>, pub regulators: Signal<BTreeSet<RegulatorKey>>,
// a serial number, assigned by `Element::new`, that uniquely identifies // a serial number, assigned by `Element::new`, that uniquely identifies
// each element // each element
@ -61,7 +61,7 @@ impl Element {
label: label, label: label,
color: color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
constraints: create_signal(BTreeSet::default()), regulators: create_signal(BTreeSet::default()),
serial: serial, serial: serial,
column_index: None column_index: None
} }
@ -111,13 +111,43 @@ impl Element {
} }
} }
#[derive(Clone)] // `set_point_spec` is always a valid specification of `set_point`
pub struct Constraint { // ┌────────────┬─────────────────────────────────────────────────────┐
// │`set_point` │ `set_point_spec` │
// ┝━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥
// │`Some(x)` │ a string that parses to the floating-point value `x`│
// ├────────────┼─────────────────────────────────────────────────────┤
// │`None` │ the empty string │
// └────────────┴─────────────────────────────────────────────────────┘
#[derive(Clone, Copy)]
pub struct Regulator {
pub subjects: (ElementKey, ElementKey), pub subjects: (ElementKey, ElementKey),
pub lorentz_prod: Signal<f64>, pub measurement: ReadSignal<f64>,
pub lorentz_prod_text: Signal<String>, pub set_point: ReadSignal<Option<f64>>,
pub lorentz_prod_valid: Signal<bool>,
pub active: Signal<bool> set_point_writable: Signal<Option<f64>>,
set_point_spec: Signal<String>
}
impl Regulator {
pub fn get_set_point_spec_clone(&self) -> String {
self.set_point_spec.get_clone()
}
pub fn get_set_point_spec_clone_untracked(&self) -> String {
self.set_point_spec.get_clone_untracked()
}
pub fn try_specify_set_point(&self, spec: String) -> bool {
match spec.parse::<f64>() {
Err(_) if !spec.is_empty() => false,
set_pt => {
self.set_point_writable.set(set_pt.ok());
self.set_point_spec.set(spec);
true
}
}
}
} }
// the velocity is expressed in uniform coordinates // the velocity is expressed in uniform coordinates
@ -131,9 +161,9 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
// a complete, view-independent description of an assembly // a complete, view-independent description of an assembly
#[derive(Clone)] #[derive(Clone)]
pub struct Assembly { pub struct Assembly {
// elements and constraints // elements and regulators
pub elements: Signal<Slab<Element>>, pub elements: Signal<Slab<Element>>,
pub constraints: Signal<Slab<Constraint>>, pub regulators: Signal<Slab<Regulator>>,
// solution variety tangent space. the basis vectors are stored in // solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column // configuration matrix format, ordered according to the elements' column
@ -155,13 +185,13 @@ impl Assembly {
pub fn new() -> Assembly { pub fn new() -> Assembly {
Assembly { Assembly {
elements: create_signal(Slab::new()), elements: create_signal(Slab::new()),
constraints: create_signal(Slab::new()), regulators: create_signal(Slab::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(FxHashMap::default()) elements_by_id: create_signal(FxHashMap::default())
} }
} }
// --- inserting elements and constraints --- // --- inserting elements and regulators ---
// insert an element into the assembly without checking whether we already // insert an element into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the // have an element with the same identifier. any element that does have the
@ -204,14 +234,62 @@ impl Assembly {
); );
} }
pub fn insert_constraint(&self, constraint: Constraint) { pub fn insert_regulator(&self, regulator: Regulator) {
let subjects = constraint.subjects; let subjects = regulator.subjects;
let key = self.constraints.update(|csts| csts.insert(constraint)); let key = self.regulators.update(|regs| regs.insert(regulator));
let subject_constraints = self.elements.with( let subject_regulators = self.elements.with(
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators)
); );
subject_constraints.0.update(|csts| csts.insert(key)); subject_regulators.0.update(|regs| regs.insert(key));
subject_constraints.1.update(|csts| csts.insert(key)); subject_regulators.1.update(|regs| regs.insert(key));
}
pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) {
// create and insert a new regulator
let measurement = self.elements.map(
move |elts| {
let reps = (
elts[subjects.0].representation.get_clone(),
elts[subjects.1].representation.get_clone()
);
reps.0.dot(&(&*Q * reps.1))
}
);
let set_point_writable = create_signal(None);
let set_point = *set_point_writable;
self.insert_regulator(Regulator {
subjects: subjects,
measurement: measurement,
set_point: set_point,
set_point_writable: set_point_writable,
set_point_spec: create_signal(String::new())
});
/* DEBUG */
// print updated regulator list
console::log_1(&JsValue::from("Regulators:"));
self.regulators.with(|regs| {
for (_, reg) in regs.into_iter() {
console::log_5(
&JsValue::from(" "),
&JsValue::from(reg.subjects.0),
&JsValue::from(reg.subjects.1),
&JsValue::from(":"),
&JsValue::from(reg.set_point.get_untracked())
);
}
});
// update the realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
create_effect(move || {
console::log_1(&JsValue::from(
format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)
));
if set_point.with(|set_pt| set_pt.is_some()) {
self.realize();
}
});
} }
// --- realization --- // --- realization ---
@ -228,13 +306,16 @@ impl Assembly {
let (gram, guess) = self.elements.with_untracked(|elts| { let (gram, guess) = self.elements.with_untracked(|elts| {
// set up the off-diagonal part of the Gram matrix // set up the off-diagonal part of the Gram matrix
let mut gram_to_be = PartialMatrix::new(); let mut gram_to_be = PartialMatrix::new();
self.constraints.with_untracked(|csts| { self.regulators.with_untracked(|regs| {
for (_, cst) in csts { for (_, reg) in regs {
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { match reg.set_point.get_untracked() {
let subjects = cst.subjects; Some(set_pt) => {
let row = elts[subjects.0].column_index.unwrap(); let subjects = reg.subjects;
let col = elts[subjects.1].column_index.unwrap(); let row = elts[subjects.0].column_index.unwrap();
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, set_pt);
},
None => ()
} }
} }
}); });

View file

@ -1,56 +1,87 @@
use itertools::Itertools; use itertools::Itertools;
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{ use web_sys::{
Event,
HtmlInputElement,
KeyboardEvent, KeyboardEvent,
MouseEvent, MouseEvent,
wasm_bindgen::JsCast wasm_bindgen::JsCast
}; };
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; use crate::{
AppState,
assembly,
assembly::{
Regulator,
RegulatorKey,
ElementKey
}
};
// an editable view of the Lorentz product representing a constraint // an editable view of a regulator
#[component(inline_props)] #[component(inline_props)]
fn LorentzProductInput(constraint: Constraint) -> View { fn RegulatorInput(regulator: Regulator) -> View {
let valid = create_signal(true);
let value = create_signal(regulator.get_set_point_spec_clone_untracked());
// this closure resets the input value to the regulator's set point
// specification, which is always a valid specification
let reset_value = move || {
batch(|| {
valid.set(true);
value.set(regulator.get_set_point_spec_clone());
})
};
// reset the input value whenever the regulator's set point specification
// is updated
create_effect(reset_value);
view! { view! {
input( input(
r#type="text", r#type="text",
bind:value=constraint.lorentz_prod_text, class=move || {
on:change=move |event: Event| { if valid.get() {
let target: HtmlInputElement = event.target().unwrap().unchecked_into(); match regulator.set_point.get() {
match target.value().parse::<f64>() { Some(_) => "regulator-input constraint",
Ok(lorentz_prod) => batch(|| { None => "regulator-input"
constraint.lorentz_prod.set(lorentz_prod); }
constraint.lorentz_prod_valid.set(true); } else {
}), "regulator-input invalid"
Err(_) => constraint.lorentz_prod_valid.set(false) }
}; },
placeholder=regulator.measurement.with(|result| result.to_string()),
bind:value=value,
on:change=move |_| valid.set(
regulator.try_specify_set_point(value.get_clone_untracked())
),
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Escape" => reset_value(),
_ => ()
}
}
} }
) )
} }
} }
// a list item that shows a constraint in an outline view of an element // a list item that shows a regulator in an outline view of an element
#[component(inline_props)] #[component(inline_props)]
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
let assembly = &state.assembly; let assembly = &state.assembly;
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); let regulator = assembly.regulators.with(|regs| regs[regulator_key]);
let other_subject = if constraint.subjects.0 == element_key { let other_subject = if regulator.subjects.0 == element_key {
constraint.subjects.1 regulator.subjects.1
} else { } else {
constraint.subjects.0 regulator.subjects.0
}; };
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); 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! { view! {
li(class=class.get()) { li(class="regulator") {
input(r#type="checkbox", bind:checked=constraint.active) div(class="regulator-label") { (other_subject_label) }
div(class="constraint-label") { (other_subject_label) } div(class="regulator-type") { "Inversive distance" }
LorentzProductInput(constraint=constraint) RegulatorInput(regulator=regulator)
div(class="status") div(class="status")
} }
} }
@ -74,9 +105,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
).collect::<Vec<_>>() ).collect::<Vec<_>>()
) )
}; };
let constrained = element.constraints.map(|csts| csts.len() > 0); let regulated = element.regulators.map(|regs| regs.len() > 0);
let constraint_list = element.constraints.map( let regulator_list = element.regulators.map(
|csts| csts.clone().into_iter().collect() |regs| regs.clone().into_iter().collect()
); );
let details_node = create_node_ref(); let details_node = create_node_ref();
view! { view! {
@ -91,7 +122,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
state.select(key, event.shift_key()); state.select(key, event.shift_key());
event.prevent_default(); event.prevent_default();
}, },
"ArrowRight" if constrained.get() => { "ArrowRight" if regulated.get() => {
let _ = details_node let _ = details_node
.get() .get()
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
@ -138,16 +169,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
div(class="status") div(class="status")
} }
} }
ul(class="constraints") { ul(class="regulators") {
Keyed( Keyed(
list=constraint_list, list=regulator_list,
view=move |cst_key| view! { view=move |reg_key| view! {
ConstraintOutlineItem( RegulatorOutlineItem(
constraint_key=cst_key, regulator_key=reg_key,
element_key=key element_key=key
) )
}, },
key=|cst_key| cst_key.clone() key=|reg_key| reg_key.clone()
) )
} }
} }
@ -155,9 +186,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
} }
} }
// a component that lists the elements of the current assembly, showing the // a component that lists the elements of the current assembly, showing each
// constraints on each element as a collapsible sub-list. its implementation // element's regulators in a collapsible sub-list. its implementation is based
// is based on Kate Morley's HTML + CSS tree views: // on Kate Morley's HTML + CSS tree views:
// //
// https://iamkate.com/code/tree-views/ // https://iamkate.com/code/tree-views/
// //