From fef4127f69cff3e86e5070aacd209c863993c09b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 17 Feb 2025 14:01:27 -0800 Subject: [PATCH 1/7] 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. --- app-proto/src/add_remove.rs | 10 +++------- app-proto/src/assembly.rs | 38 +++++++++++++----------------------- app-proto/src/outline.rs | 39 +++++++++++++------------------------ 3 files changed, 30 insertions(+), 57 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 3280dac..c96d5ab 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -7,7 +7,6 @@ use crate::{ assembly::{ Assembly, Regulator, - RegulatorRole, Element }, engine::Q @@ -209,14 +208,12 @@ pub fn AddRemove() -> View { reps.0.dot(&(&*Q * reps.1)) } ); - let set_point = create_signal(0.0); - let role = create_signal(RegulatorRole::Measurement); + let set_point = create_signal(None); state.assembly.insert_regulator(Regulator { subjects: subjects, measurement: measurement, set_point: set_point, - set_point_text: create_signal(String::new()), - role: role, + set_point_spec: create_signal(String::new()) }); state.selection.update(|sel| sel.clear()); @@ -241,8 +238,7 @@ pub fn AddRemove() -> View { 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()) { + if set_point.with(|set_pt| set_pt.is_some()) { state.assembly.realize(); } }); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c052a3b..c1eba1e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -111,32 +111,17 @@ 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)] +#[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, - pub set_point: Signal, - pub set_point_text: Signal, - pub role: Signal + pub set_point: Signal>, + pub set_point_spec: Signal } impl Regulator { - fn role_is_valid_constraint_untracked(&self) -> bool { - self.role.with_untracked(|role| role.is_valid_constraint()) + pub fn has_no_set_point_spec(&self) -> bool { + self.set_point_spec.with(|spec| spec.is_empty()) } } @@ -250,11 +235,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 => () } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8dcda93..d892f0f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -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,25 +19,17 @@ use crate::{ // an editable view of a regulator #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { + let value = create_signal(regulator.set_point_spec.get_clone_untracked()); + create_effect(move || value.set(regulator.set_point_spec.get_clone())); 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); - } else { - match target.value().parse::() { - Ok(set_pt) => batch(|| { - regulator.set_point.set(set_pt); - regulator.role.set(Constraint(true)); - }), - Err(_) => regulator.role.set(Constraint(false)) - }; - } + bind:value=value, + on:change=move |_| { + let value_val = value.get_clone_untracked(); + regulator.set_point.set(value_val.parse::().ok()); + regulator.set_point_spec.set(value_val); } ) } @@ -51,20 +40,20 @@ fn RegulatorInput(regulator: Regulator) -> View { fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); 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" + let class = create_memo(move || { + match regulator.set_point.get() { + None if regulator.has_no_set_point_spec() => "regulator", + None => "regulator invalid-constraint", + Some(_) => "regulator valid-constraint" } - ); + }); view! { li(class=class.get()) { div(class="regulator-label") { (other_subject_label) } From 302d93638d36ccb6a7b75aba1f9d3c06e7c6f81d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 01:27:11 -0800 Subject: [PATCH 2/7] 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*. --- app-proto/main.css | 15 +++++------ app-proto/src/assembly.rs | 7 +----- app-proto/src/outline.rs | 53 +++++++++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index d71ce4a..7ba1206 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -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:has(.invalid) > .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c1eba1e..e8a8dc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -111,6 +111,7 @@ impl Element { } } +// `set_point_spec` must always be a valid specification of `set_point` #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), @@ -119,12 +120,6 @@ pub struct Regulator { pub set_point_spec: Signal } -impl Regulator { - pub fn has_no_set_point_spec(&self) -> bool { - self.set_point_spec.with(|spec| spec.is_empty()) - } -} - // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index d892f0f..b0347f6 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -19,17 +19,55 @@ 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.set_point_spec.get_clone_untracked()); - create_effect(move || value.set(regulator.set_point_spec.get_clone())); + + // 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.set_point_spec.get_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() { + match regulator.set_point.get() { + Some(_) => "regulator-input constraint", + None => "regulator-input" + } + } else { + "regulator-input invalid" + } + }, placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { let value_val = value.get_clone_untracked(); - regulator.set_point.set(value_val.parse::().ok()); - regulator.set_point_spec.set(value_val); + match value_val.parse::() { + Err(_) if !value_val.is_empty() => valid.set(false), + set_pt => batch(|| { + regulator.set_point.set(set_pt.ok()); + regulator.set_point_spec.set(value_val); + valid.set(true); + }) + }; + }, + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Escape" => reset_value(), + _ => () + } + } } ) } @@ -47,15 +85,8 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = create_memo(move || { - match regulator.set_point.get() { - None if regulator.has_no_set_point_spec() => "regulator", - None => "regulator invalid-constraint", - Some(_) => "regulator valid-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) From bbd0835a8fee603a28de61054aa9637c74fdc08c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 12:28:55 -0800 Subject: [PATCH 3/7] Move set point spec validation into `Regulator` --- app-proto/src/assembly.rs | 13 +++++++++++++ app-proto/src/outline.rs | 14 +++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e8a8dc0..20b893d 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -120,6 +120,19 @@ pub struct Regulator { pub set_point_spec: Signal } +impl Regulator { + pub fn try_specify_set_point(&self, spec: String) -> bool { + match spec.parse::() { + Err(_) if !spec.is_empty() => false, + set_pt => { + self.set_point.set(set_pt.ok()); + self.set_point_spec.set(spec); + true + } + } + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index b0347f6..a48377b 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -50,17 +50,9 @@ fn RegulatorInput(regulator: Regulator) -> View { }, placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, - on:change=move |_| { - let value_val = value.get_clone_untracked(); - match value_val.parse::() { - Err(_) if !value_val.is_empty() => valid.set(false), - set_pt => batch(|| { - regulator.set_point.set(set_pt.ok()); - regulator.set_point_spec.set(value_val); - valid.set(true); - }) - }; - }, + on:change=move |_| valid.set( + regulator.try_specify_set_point(value.get_clone_untracked()) + ), on:keydown={ move |event: KeyboardEvent| { match event.key().as_str() { From f2e84fb64a87356e7ad43927dc3d2b7bd46bcd7b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 13:29:10 -0800 Subject: [PATCH 4/7] 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. --- app-proto/src/add_remove.rs | 47 +---------------------- app-proto/src/assembly.rs | 75 ++++++++++++++++++++++++++++++++++--- app-proto/src/outline.rs | 4 +- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index c96d5ab..cdc35e6 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -6,10 +6,8 @@ use crate::{ AppState, assembly::{ Assembly, - Regulator, Element - }, - engine::Q + } }; /* DEBUG */ @@ -199,49 +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(None); - state.assembly.insert_regulator(Regulator { - subjects: subjects, - measurement: measurement, - set_point: set_point, - set_point_spec: create_signal(String::new()) - }); + 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) - )); - if set_point.with(|set_pt| set_pt.is_some()) { - state.assembly.realize(); - } - }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 20b893d..21421b6 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -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,21 +111,38 @@ impl Element { } } -// `set_point_spec` must always be a valid specification of `set_point` +// `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, - pub set_point: Signal>, - pub set_point_spec: Signal + pub set_point: ReadSignal>, + + set_point_writable: Signal>, + set_point_spec: Signal } 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::() { Err(_) if !spec.is_empty() => false, set_pt => { - self.set_point.set(set_pt.ok()); + self.set_point_writable.set(set_pt.ok()); self.set_point_spec.set(spec); true } @@ -227,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) { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a48377b..4898aba 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -20,14 +20,14 @@ use crate::{ #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { let valid = create_signal(true); - let value = create_signal(regulator.set_point_spec.get_clone_untracked()); + 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.set_point_spec.get_clone()); + value.set(regulator.get_set_point_spec_clone()); }) }; From c54b6bc16599124f08686d52b3193422f140e2c9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 13:37:30 -0800 Subject: [PATCH 5/7] 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. --- app-proto/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/main.css b/app-proto/main.css index 7ba1206..4726a27 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -163,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.regulator:has(.invalid) > .status::after, details:has(.invalid):not([open]) .status::after { +.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } From befadd25c9d35c6a9e782d9ada981002ebc91fc4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 19 Feb 2025 01:22:42 -0800 Subject: [PATCH 6/7] Get the read-only set point signal more simply --- app-proto/src/assembly.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 21421b6..8403406 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -256,7 +256,7 @@ impl Assembly { } ); let set_point_writable = create_signal(None); - let set_point = set_point_writable.split().0; + let set_point = *set_point_writable; self.insert_regulator(Regulator { subjects: subjects, measurement: measurement, From 6c31a25822ca9d5b8dc3a50dc7ffac4e94f19afc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 23 Feb 2025 23:51:24 -0800 Subject: [PATCH 7/7] Consolidate set point data --- app-proto/src/assembly.rs | 127 +++++++++++++++++++++++++------------- app-proto/src/outline.rs | 23 ++++--- 2 files changed, 97 insertions(+), 53 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 8403406..2ccfc69 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,7 +1,11 @@ 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 */ @@ -111,41 +115,75 @@ impl Element { } } -// `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 │ -// └────────────┴─────────────────────────────────────────────────────┘ +// 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` +pub enum SpecifiedValue { + Absent, + Present { + spec: String, + value: f64 + } +} + +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 { + Absent => String::new(), + Present { spec, .. } => spec.clone() + } + } + + fn is_present(&self) -> bool { + match self { + Absent => false, + Present { .. } => true + } + } +} + +// 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 for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(Absent) + } else { + spec.parse::().map( + |value| Present { spec: spec, value: value } + ) + } + } +} + #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, - pub set_point: ReadSignal>, - - set_point_writable: Signal>, - set_point_spec: Signal + pub set_point: Signal } 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::() { - Err(_) if !spec.is_empty() => false, - set_pt => { - self.set_point_writable.set(set_pt.ok()); - self.set_point_spec.set(spec); + 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, } } } @@ -255,18 +293,15 @@ impl Assembly { reps.0.dot(&(&*Q * reps.1)) } ); - let set_point_writable = create_signal(None); - let set_point = *set_point_writable; + let set_point = create_signal(Absent); 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()) + set_point: set_point }); /* DEBUG */ - // print updated regulator list + // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with(|regs| { for (_, reg) in regs.into_iter() { @@ -275,7 +310,9 @@ impl Assembly { &JsValue::from(reg.subjects.0), &JsValue::from(reg.subjects.1), &JsValue::from(":"), - &JsValue::from(reg.set_point.get_untracked()) + &JsValue::from(reg.set_point.with_untracked( + |set_pt| set_pt.spec() + )) ); } }); @@ -286,7 +323,7 @@ impl Assembly { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - if set_point.with(|set_pt| set_pt.is_some()) { + if set_point.with(|set_pt| set_pt.is_present()) { self.realize(); } }); @@ -308,15 +345,17 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.regulators.with_untracked(|regs| { for (_, reg) in regs { - 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 => () - } + 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); + } + }; + }); } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4898aba..04bda9a 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -10,9 +10,10 @@ use crate::{ AppState, assembly, assembly::{ + ElementKey, Regulator, RegulatorKey, - ElementKey + SpecifiedValue::* } }; @@ -20,14 +21,16 @@ use crate::{ #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { let valid = create_signal(true); - let value = create_signal(regulator.get_set_point_spec_clone_untracked()); + 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, which is always a valid specification + // specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.get_set_point_spec_clone()); + value.set(regulator.set_point.with(|set_pt| set_pt.spec())); }) }; @@ -40,10 +43,12 @@ fn RegulatorInput(regulator: Regulator) -> View { r#type="text", class=move || { if valid.get() { - match regulator.set_point.get() { - Some(_) => "regulator-input constraint", - None => "regulator-input" - } + regulator.set_point.with(|set_pt| { + match set_pt { + Absent => "regulator-input", + Present { .. } => "regulator-input constraint" + } + }) } else { "regulator-input invalid" } @@ -51,7 +56,7 @@ fn RegulatorInput(regulator: Regulator) -> View { 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()) + regulator.try_set(value.get_clone_untracked()) ), on:keydown={ move |event: KeyboardEvent| {