Compare commits

...

4 commits

Author SHA1 Message Date
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 163 additions and 94 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 */
@ -77,12 +78,12 @@ summary.selected {
background-color: var(--selection-highlight); background-color: var(--selection-highlight);
} }
summary > div, .constraint { summary > div, .observable {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.element, .constraint { .element, .observable {
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 { .observable-label {
flex-grow: 1; flex-grow: 1;
} }
@ -123,26 +124,32 @@ details[open]:has(li) .element-switch::after {
width: 56px; width: 56px;
} }
.constraint { .observable {
font-style: italic; font-style: italic;
} }
.constraint.invalid { .observable.invalid-constraint {
color: var(--text-invalid); color: var(--text-invalid);
} }
.constraint > input[type=checkbox] { .observable > 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] { .observable > input::placeholder {
color: inherit;
opacity: 54%;
font-style: italic;
}
.observable.valid-constraint > input {
background-color: var(--display-background);
}
.observable.invalid-constraint > input {
border-color: var(--border-invalid); border-color: var(--border-invalid);
} }
@ -154,7 +161,7 @@ details[open]:has(li) .element-switch::after {
font-style: normal; font-style: normal;
} }
.invalid > .status::after, details:has(.invalid):not([open]) .status::after { .invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after {
content: '⚠'; content: '⚠';
color: var(--text-invalid); color: var(--text-invalid);
} }
@ -171,5 +178,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,17 @@
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,
Observable,
ObservableRole,
Element
},
engine::Q
};
/* 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,41 +200,49 @@ 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); let measured = state.assembly.elements.map(
let lorentz_prod_valid = create_signal(false); move |elts| {
let active = create_signal(true); let reps = (
state.assembly.insert_constraint(Constraint { elts[subjects.0].representation.get_clone(),
elts[subjects.1].representation.get_clone()
);
reps.0.dot(&(&*Q * reps.1))
}
);
let desired = create_signal(0.0);
let role = create_signal(ObservableRole::Measurement);
state.assembly.insert_observable(Observable {
subjects: subjects, subjects: subjects,
lorentz_prod: lorentz_prod, measured: measured,
lorentz_prod_text: create_signal(String::new()), desired: desired,
lorentz_prod_valid: lorentz_prod_valid, desired_text: create_signal(String::new()),
active: active, role: role,
}); });
state.selection.update(|sel| sel.clear()); state.selection.update(|sel| sel.clear());
/* DEBUG */ /* DEBUG */
// print updated constraint list // print updated observable list
console::log_1(&JsValue::from("Constraints:")); console::log_1(&JsValue::from("Observables:"));
state.assembly.constraints.with(|csts| { state.assembly.observables.with(|obsls| {
for (_, cst) in csts.into_iter() { for (_, obs) in obsls.into_iter() {
console::log_5( console::log_5(
&JsValue::from(" "), &JsValue::from(" "),
&JsValue::from(cst.subjects.0), &JsValue::from(obs.subjects.0),
&JsValue::from(cst.subjects.1), &JsValue::from(obs.subjects.1),
&JsValue::from(":"), &JsValue::from(":"),
&JsValue::from(cst.lorentz_prod.get_untracked()) &JsValue::from(obs.desired.get_untracked())
); );
} }
}); });
// update the realization when the constraint becomes active // update the realization when the observable becomes
// and valid, or is edited while active and valid // constrained, or is edited while constrained
create_effect(move || { create_effect(move || {
console::log_1(&JsValue::from( console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1) format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)
)); ));
lorentz_prod.track(); desired.track();
if active.get() && lorentz_prod_valid.get() { if role.with(|rl| rl.is_valid_constraint()) {
state.assembly.realize(); state.assembly.realize();
} }
}); });

View file

@ -7,9 +7,9 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; use crate::engine::{realize_gram, local_unif_to_std, 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 observables
pub type ElementKey = usize; pub type ElementKey = usize;
pub type ConstraintKey = usize; pub type ObservableKey = 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 observables: Signal<BTreeSet<ObservableKey>>,
// 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()), observables: create_signal(BTreeSet::default()),
serial: serial, serial: serial,
column_index: None column_index: None
} }
@ -111,13 +111,33 @@ impl Element {
} }
} }
pub enum ObservableRole {
Measurement,
Constraint(bool)
}
impl ObservableRole {
pub fn is_valid_constraint(&self) -> bool {
match self {
ObservableRole::Measurement => false,
ObservableRole::Constraint(valid) => *valid
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Constraint { pub struct Observable {
pub subjects: (ElementKey, ElementKey), pub subjects: (ElementKey, ElementKey),
pub lorentz_prod: Signal<f64>, pub measured: ReadSignal<f64>,
pub lorentz_prod_text: Signal<String>, pub desired: Signal<f64>,
pub lorentz_prod_valid: Signal<bool>, pub desired_text: Signal<String>,
pub active: Signal<bool> pub role: Signal<ObservableRole>
}
impl Observable {
fn role_is_valid_constraint_untracked(&self) -> bool {
self.role.with_untracked(|role| role.is_valid_constraint())
}
} }
// the velocity is expressed in uniform coordinates // the velocity is expressed in uniform coordinates
@ -131,9 +151,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 observables
pub elements: Signal<Slab<Element>>, pub elements: Signal<Slab<Element>>,
pub constraints: Signal<Slab<Constraint>>, pub observables: Signal<Slab<Observable>>,
// 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 +175,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()), observables: 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 observables ---
// 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 +224,14 @@ impl Assembly {
); );
} }
pub fn insert_constraint(&self, constraint: Constraint) { pub fn insert_observable(&self, observable: Observable) {
let subjects = constraint.subjects; let subjects = observable.subjects;
let key = self.constraints.update(|csts| csts.insert(constraint)); let key = self.observables.update(|obsls| obsls.insert(observable));
let subject_constraints = self.elements.with( let subject_observables = self.elements.with(
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) |elts| (elts[subjects.0].observables, elts[subjects.1].observables)
); );
subject_constraints.0.update(|csts| csts.insert(key)); subject_observables.0.update(|obsls| obsls.insert(key));
subject_constraints.1.update(|csts| csts.insert(key)); subject_observables.1.update(|obsls| obsls.insert(key));
} }
// --- realization --- // --- realization ---
@ -228,13 +248,13 @@ 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.observables.with_untracked(|obsls| {
for (_, cst) in csts { for (_, obs) in obsls {
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { if obs.role_is_valid_constraint_untracked() {
let subjects = cst.subjects; let subjects = obs.subjects;
let row = elts[subjects.0].column_index.unwrap(); let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); gram_to_be.push_sym(row, col, obs.desired.get_untracked());
} }
} }
}); });

View file

@ -8,49 +8,67 @@ use web_sys::{
wasm_bindgen::JsCast wasm_bindgen::JsCast
}; };
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; use crate::{
AppState,
assembly,
assembly::{
Observable,
ObservableKey,
ObservableRole::*,
ElementKey
}
};
// an editable view of the Lorentz product representing a constraint // an editable view of the Lorentz product representing an observable
#[component(inline_props)] #[component(inline_props)]
fn LorentzProductInput(constraint: Constraint) -> View { fn ObservableInput(observable: Observable) -> View {
view! { view! {
input( input(
r#type="text", r#type="text",
bind:value=constraint.lorentz_prod_text, placeholder=observable.measured.with(|result| result.to_string()),
bind:value=observable.desired_text,
on:change=move |event: Event| { on:change=move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let target: HtmlInputElement = event.target().unwrap().unchecked_into();
match target.value().parse::<f64>() { let value = target.value();
Ok(lorentz_prod) => batch(|| { if value.is_empty() {
constraint.lorentz_prod.set(lorentz_prod); observable.role.set(Measurement);
constraint.lorentz_prod_valid.set(true); } else {
}), match target.value().parse::<f64>() {
Err(_) => constraint.lorentz_prod_valid.set(false) Ok(desired) => batch(|| {
}; observable.desired.set(desired);
observable.role.set(Constraint(true));
}),
Err(_) => observable.role.set(Constraint(false))
};
}
} }
) )
} }
} }
// a list item that shows a constraint in an outline view of an element // a list item that shows an observable in an outline view of an element
#[component(inline_props)] #[component(inline_props)]
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { fn ObservableOutlineItem(observable_key: ObservableKey, 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 observable = assembly.observables.with(|obsls| obsls[observable_key].clone());
let other_subject = if constraint.subjects.0 == element_key { let other_subject = if observable.subjects.0 == element_key {
constraint.subjects.1 observable.subjects.1
} else { } else {
constraint.subjects.0 observable.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( let class = observable.role.map(
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } |role| match role {
Measurement => "observable",
Constraint(true) => "observable valid-constraint",
Constraint(false) => "observable invalid-constraint"
}
); );
view! { view! {
li(class=class.get()) { li(class=class.get()) {
input(r#type="checkbox", bind:checked=constraint.active) div(class="observable-label") { (other_subject_label) }
div(class="constraint-label") { (other_subject_label) } ObservableInput(observable=observable)
LorentzProductInput(constraint=constraint)
div(class="status") div(class="status")
} }
} }
@ -74,9 +92,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
).collect::<Vec<_>>() ).collect::<Vec<_>>()
) )
}; };
let constrained = element.constraints.map(|csts| csts.len() > 0); let observed = element.observables.map(|obsls| obsls.len() > 0);
let constraint_list = element.constraints.map( let observable_list = element.observables.map(
|csts| csts.clone().into_iter().collect() |obsls| obsls.clone().into_iter().collect()
); );
let details_node = create_node_ref(); let details_node = create_node_ref();
view! { view! {
@ -91,7 +109,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 observed.get() => {
let _ = details_node let _ = details_node
.get() .get()
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
@ -138,16 +156,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
div(class="status") div(class="status")
} }
} }
ul(class="constraints") { ul(class="observables") {
Keyed( Keyed(
list=constraint_list, list=observable_list,
view=move |cst_key| view! { view=move |obs_key| view! {
ConstraintOutlineItem( ObservableOutlineItem(
constraint_key=cst_key, observable_key=obs_key,
element_key=key element_key=key
) )
}, },
key=|cst_key| cst_key.clone() key=|obs_key| obs_key.clone()
) )
} }
} }
@ -156,8 +174,8 @@ 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 the
// constraints on each element as a collapsible sub-list. its implementation // observables associated with each element as a collapsible sub-list. its
// is based on Kate Morley's HTML + CSS tree views: // implementation is based on Kate Morley's HTML + CSS tree views:
// //
// https://iamkate.com/code/tree-views/ // https://iamkate.com/code/tree-views/
// //