Compare commits

...

5 commits

Author SHA1 Message Date
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
4 changed files with 133 additions and 110 deletions

View file

@ -133,28 +133,25 @@ details[open]:has(li) .element-switch::after {
font-size: 10pt;
}
.regulator.invalid-constraint {
color: var(--text-invalid);
}
.regulator > input {
.regulator-input {
color: inherit;
background-color: inherit;
border: 1px solid var(--border);
border-radius: 2px;
}
.regulator > input::placeholder {
.regulator-input::placeholder {
color: inherit;
opacity: 54%;
font-style: italic;
}
.regulator.valid-constraint > input {
.regulator-input.constraint {
background-color: var(--display-background);
}
.regulator.invalid-constraint > input {
.regulator-input.invalid {
color: var(--text-invalid);
border-color: var(--border-invalid);
}
@ -166,7 +163,7 @@ details[open]:has(li) .element-switch::after {
font-style: normal;
}
.invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after {
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠';
color: var(--text-invalid);
}

View file

@ -6,11 +6,8 @@ use crate::{
AppState,
assembly::{
Assembly,
Regulator,
RegulatorRole,
Element
},
engine::Q
}
};
/* DEBUG */
@ -200,52 +197,8 @@ pub fn AddRemove() -> View {
(subject_vec[0].clone(), subject_vec[1].clone())
}
);
let measurement = state.assembly.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 = create_signal(0.0);
let role = create_signal(RegulatorRole::Measurement);
state.assembly.insert_regulator(Regulator {
subjects: subjects,
measurement: measurement,
set_point: set_point,
set_point_text: create_signal(String::new()),
role: role,
});
state.assembly.insert_new_regulator(subjects);
state.selection.update(|sel| sel.clear());
/* DEBUG */
// print updated regulator list
console::log_1(&JsValue::from("Regulators:"));
state.assembly.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)
));
set_point.track();
if role.with(|rl| rl.is_valid_constraint()) {
state.assembly.realize();
}
});
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser

View file

@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
use sycamore::prelude::*;
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 regulators
pub type ElementKey = usize;
@ -111,32 +111,42 @@ impl Element {
}
}
pub enum RegulatorRole {
Measurement,
Constraint(bool)
}
impl RegulatorRole {
pub fn is_valid_constraint(&self) -> bool {
match self {
RegulatorRole::Measurement => false,
RegulatorRole::Constraint(valid) => *valid
}
}
}
#[derive(Clone)]
// `set_point_spec` is always a valid specification of `set_point`
// ┌────────────┬─────────────────────────────────────────────────────┐
// │`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 measurement: ReadSignal<f64>,
pub set_point: Signal<f64>,
pub set_point_text: Signal<String>,
pub role: Signal<RegulatorRole>
pub set_point: ReadSignal<Option<f64>>,
set_point_writable: Signal<Option<f64>>,
set_point_spec: Signal<String>
}
impl Regulator {
fn role_is_valid_constraint_untracked(&self) -> bool {
self.role.with_untracked(|role| role.is_valid_constraint())
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
}
}
}
}
@ -234,6 +244,54 @@ impl Assembly {
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.split().0;
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 ---
pub fn realize(&self) {
@ -250,11 +308,14 @@ impl Assembly {
let mut gram_to_be = PartialMatrix::new();
self.regulators.with_untracked(|regs| {
for (_, reg) in regs {
if reg.role_is_valid_constraint_untracked() {
let subjects = reg.subjects;
let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, reg.set_point.get_untracked());
match reg.set_point.get_untracked() {
Some(set_pt) => {
let subjects = reg.subjects;
let row = elts[subjects.0].column_index.unwrap();
let col = elts[subjects.1].column_index.unwrap();
gram_to_be.push_sym(row, col, set_pt);
},
None => ()
}
}
});

View file

@ -1,8 +1,6 @@
use itertools::Itertools;
use sycamore::prelude::*;
use web_sys::{
Event,
HtmlInputElement,
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
@ -14,7 +12,6 @@ use crate::{
assembly::{
Regulator,
RegulatorKey,
RegulatorRole::*,
ElementKey
}
};
@ -22,24 +19,46 @@ use crate::{
// an editable view of a regulator
#[component(inline_props)]
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! {
input(
r#type="text",
placeholder=regulator.measurement.with(|result| result.to_string()),
bind:value=regulator.set_point_text,
on:change=move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
let value = target.value();
if value.is_empty() {
regulator.role.set(Measurement);
class=move || {
if valid.get() {
match regulator.set_point.get() {
Some(_) => "regulator-input constraint",
None => "regulator-input"
}
} else {
match target.value().parse::<f64>() {
Ok(set_pt) => batch(|| {
regulator.set_point.set(set_pt);
regulator.role.set(Constraint(true));
}),
Err(_) => regulator.role.set(Constraint(false))
};
"regulator-input invalid"
}
},
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(),
_ => ()
}
}
}
)
@ -51,22 +70,15 @@ fn RegulatorInput(regulator: Regulator) -> View {
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let assembly = &state.assembly;
let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone());
let regulator = assembly.regulators.with(|regs| regs[regulator_key]);
let other_subject = if regulator.subjects.0 == element_key {
regulator.subjects.1
} else {
regulator.subjects.0
};
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
let class = regulator.role.map(
|role| match role {
Measurement => "regulator",
Constraint(true) => "regulator valid-constraint",
Constraint(false) => "regulator invalid-constraint"
}
);
view! {
li(class=class.get()) {
li(class="regulator") {
div(class="regulator-label") { (other_subject_label) }
div(class="regulator-type") { "Inversive distance" }
RegulatorInput(regulator=regulator)