Compare commits

...

7 commits

Author SHA1 Message Date
Aaron Fenyes
6c31a25822 Consolidate set point data 2025-02-23 23:51:24 -08:00
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
4 changed files with 173 additions and 106 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

@ -1,11 +1,15 @@
use nalgebra::{DMatrix, DVector, DVectorView, Vector3};
use rustc_hash::FxHashMap;
use slab::Slab;
use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}};
use std::{
collections::BTreeSet,
num::ParseFloatError,
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 +115,76 @@ impl Element {
}
}
pub enum RegulatorRole {
Measurement,
Constraint(bool)
// to construct a `SpecifiedValue` that might be `Present`, use the associated
// function `try_from`. this ensures that `spec` is always a valid specification
// of `value` according to the format discussed above the implementation of
// `TryFrom<String>`
pub enum SpecifiedValue {
Absent,
Present {
spec: String,
value: f64
}
}
impl RegulatorRole {
pub fn is_valid_constraint(&self) -> bool {
use SpecifiedValue::*;
impl SpecifiedValue {
// get the specification for this value. the associated function `try_from`
// is essentially a left inverse of this method:
//
// SpecifiedValue::try_from(x.spec()) == Ok(x)
//
pub fn spec(&self) -> String {
match self {
RegulatorRole::Measurement => false,
RegulatorRole::Constraint(valid) => *valid
Absent => String::new(),
Present { spec, .. } => spec.clone()
}
}
fn is_present(&self) -> bool {
match self {
Absent => false,
Present { .. } => true
}
}
}
#[derive(Clone)]
// we can try to turn a specification string into a `SpecifiedValue`. if the
// specification is empty, the `SpecifiedValue` is `Absent`. if the
// specification parses to a floating-point value `x`, the `SpecifiedValue` is
// `Present`, with a `value` of `x`, and the specification is stored in `spec`.
// these are the only valid specifications; any other produces an error
impl TryFrom<String> for SpecifiedValue {
type Error = ParseFloatError;
fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() {
Ok(Absent)
} else {
spec.parse::<f64>().map(
|value| Present { spec: spec, value: value }
)
}
}
}
#[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: Signal<SpecifiedValue>
}
impl Regulator {
fn role_is_valid_constraint_untracked(&self) -> bool {
self.role.with_untracked(|role| role.is_valid_constraint())
pub fn try_set(&self, set_pt_spec: String) -> bool {
match SpecifiedValue::try_from(set_pt_spec) {
Ok(set_pt) => {
self.set_point.set(set_pt);
true
}
Err(_) => false,
}
}
}
@ -234,6 +282,53 @@ 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 = create_signal(Absent);
self.insert_regulator(Regulator {
subjects: subjects,
measurement: measurement,
set_point: set_point
});
/* DEBUG */
// print an updated list of regulators
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.with_untracked(
|set_pt| set_pt.spec()
))
);
}
});
// 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_present()) {
self.realize();
}
});
}
// --- realization ---
pub fn realize(&self) {
@ -250,12 +345,17 @@ 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());
}
reg.set_point.with_untracked(|set_pt| {
match set_pt {
Absent => (),
Present { value, .. } => {
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, *value);
}
};
});
}
});

View file

@ -1,8 +1,6 @@
use itertools::Itertools;
use sycamore::prelude::*;
use web_sys::{
Event,
HtmlInputElement,
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
@ -12,34 +10,60 @@ use crate::{
AppState,
assembly,
assembly::{
ElementKey,
Regulator,
RegulatorKey,
RegulatorRole::*,
ElementKey
SpecifiedValue::*
}
};
// an editable view of a regulator
#[component(inline_props)]
fn RegulatorInput(regulator: Regulator) -> View {
let valid = create_signal(true);
let value = create_signal(
regulator.set_point.with_untracked(|set_pt| set_pt.spec())
);
// this closure resets the input value to the regulator's set point
// specification
let reset_value = move || {
batch(|| {
valid.set(true);
value.set(regulator.set_point.with(|set_pt| set_pt.spec()));
})
};
// 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() {
regulator.set_point.with(|set_pt| {
match set_pt {
Absent => "regulator-input",
Present { .. } => "regulator-input constraint"
}
})
} 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_set(value.get_clone_untracked())
),
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Escape" => reset_value(),
_ => ()
}
}
}
)
@ -51,22 +75,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)