diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_events_variables.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_events_variables.rs index 76393b0..6e01bb3 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_events_variables.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_events_variables.rs @@ -2,9 +2,12 @@ use crate::app::event::Event; use crate::app::state::{Consumed, SessionHelper}; use crate::app::{AeonError, DynError}; use crate::sketchbook::data_structs::{ - LayoutNodeData, LayoutNodeDataPrototype, ModelData, VariableData, VariableWithLayoutData, + ChangeIdData, LayoutNodeData, LayoutNodeDataPrototype, ModelData, VariableData, + VariableWithLayoutData, +}; +use crate::sketchbook::event_utils::{ + make_reversible, mk_model_event, mk_model_state_change, mk_stat_prop_event, }; -use crate::sketchbook::event_utils::{make_reversible, mk_model_event, mk_model_state_change}; use crate::sketchbook::ids::VarId; use crate::sketchbook::layout::NodePosition; use crate::sketchbook::model::{ModelState, UpdateFn, Variable}; @@ -12,18 +15,20 @@ use crate::sketchbook::JsonSerde; /* Constants for event path segments in `ModelState` related to variables. */ -// add new propared variable (and potentially change its position) +// add new propared variable (+ and potentially change its position) const ADD_VAR_PATH: &str = "add"; // add new default variable const ADD_DEFAULT_VAR_PATH: &str = "add_default"; // add new variable (without any additional changes) const ADD_RAW_VAR_PATH: &str = "add_raw"; -// remove variable (removing all its regulations and so on) +// remove variable (+ removing all its regulations and so on) const REMOVE_VAR_PATH: &str = "remove"; // set variable's data (name and annotation) const SET_DATA_PATH: &str = "set_data"; -// set variable's id +// set variable's id (+ update all static props) const SET_ID_PATH: &str = "set_id"; +// set variable's id +const SET_ID_RAW_PATH: &str = "set_id_raw"; // set variable's update fn const SET_UPDATE_FN_PATH: &str = "set_update_fn"; @@ -187,7 +192,7 @@ impl ModelState { // Note this check is performed also later by the manager, we just want to detect this ASAP. if self.is_var_contained_in_updates(&var_id) { return AeonError::throw(format!( - "Cannot remove variable `{var_id}`, it is still contained in an update function." + "Cannot remove variable `{var_id}`, it is still contained in some update functions." )); } @@ -266,6 +271,27 @@ impl ModelState { reverse_event.payload = Some(original_data.to_json_str()); Ok(make_reversible(state_change, event, reverse_event)) } else if Self::starts_with(SET_ID_PATH, at_path).is_some() { + // get the payload - a string for the "new_id" + let new_id = Self::clone_payload_str(event, component_name)?; + if var_id.as_str() == new_id.as_str() { + return Ok(Consumed::NoChange); + } + + // now we must handle the event itself, and all potential static property changes + let mut event_list = Vec::new(); + // the raw event of changing the var id (payload stays the same) + let var_id_event_path = ["variable", var_id.as_str(), "set_id_raw"]; + let var_id_event = mk_model_event(&var_id_event_path, Some(&new_id)); + event_list.push(var_id_event); + + // event for modifying all corresponding static property (we do it via single event) + // note we have checked that `var_id` and `new_id` are different + let id_change_data = ChangeIdData::new(var_id.as_str(), &new_id).to_json_str(); + let prop_event = mk_stat_prop_event(&["set_var_id_everywhere"], Some(&id_change_data)); + event_list.push(prop_event); + event_list.reverse(); // has to be reversed + Ok(Consumed::Restart(event_list)) + } else if Self::starts_with(SET_ID_RAW_PATH, at_path).is_some() { // get the payload - string for "new_id" let new_id = Self::clone_payload_str(event, component_name)?; if var_id.as_str() == new_id.as_str() { @@ -281,7 +307,7 @@ impl ModelState { let state_change = mk_model_state_change(&["variable", "set_id"], &model_data); // prepare the reverse event (the reverse event is as usual) - let reverse_at_path = ["variable", new_id.as_str(), "set_id"]; + let reverse_at_path = ["variable", new_id.as_str(), "set_id_raw"]; let reverse_event = mk_model_event(&reverse_at_path, Some(var_id.as_str())); Ok(make_reversible(state_change, event, reverse_event)) } else if Self::starts_with(SET_UPDATE_FN_PATH, at_path).is_some() { diff --git a/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs b/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs index dd02641..c937b32 100644 --- a/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs +++ b/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs @@ -3,7 +3,7 @@ use crate::sketchbook::ids::{ }; use crate::sketchbook::model::{Essentiality, Monotonicity}; use crate::sketchbook::properties::dynamic_props::are_same_dyn_variant; -use crate::sketchbook::properties::static_props::are_same_stat_variant; +use crate::sketchbook::properties::static_props::{are_same_stat_variant, StatPropertyType}; use crate::sketchbook::properties::{ DynPropIterator, DynProperty, PropertyManager, StatPropIterator, StatProperty, }; @@ -360,6 +360,44 @@ impl PropertyManager { self.stat_properties.remove(id).unwrap(); Ok(()) } + + /// Go through all static properties that are automatically generated from the regulation + /// graph and make their IDs consistent with the variables they reference. + /// + /// This is useful after we change the variable's ID, e.g., to ensure that monotonicity + /// properties still have IDs like `monotonicity_REGULATOR_TARGET`. + pub fn make_generated_reg_prop_ids_consistent(&mut self) { + // list of old-new IDs that must be changed + let mut id_change_list: Vec<(StatPropertyId, StatPropertyId)> = Vec::new(); + for (prop_id, prop) in self.stat_properties.iter() { + match prop.get_prop_data() { + StatPropertyType::RegulationEssential(p) => { + // this template always has both fields, we can unwrap + let expected_id = StatProperty::get_essentiality_prop_id( + p.input.as_ref().unwrap(), + p.target.as_ref().unwrap(), + ); + if prop_id != &expected_id { + id_change_list.push((prop_id.clone(), expected_id.clone())); + } + } + StatPropertyType::RegulationMonotonic(p) => { + // this template always has both fields, we can unwrap + let expected_id = StatProperty::get_monotonicity_prop_id( + p.input.as_ref().unwrap(), + p.target.as_ref().unwrap(), + ); + if prop_id != &expected_id { + id_change_list.push((prop_id.clone(), expected_id.clone())); + } + } + _ => {} + } + } + for (current_id, new_id) in id_change_list { + self.set_stat_id(¤t_id, new_id).unwrap(); + } + } } /// Internal assertion utilities. diff --git a/src-tauri/src/sketchbook/properties/_manager/_impl_session_state.rs b/src-tauri/src/sketchbook/properties/_manager/_impl_session_state.rs index 50fa02e..a599450 100644 --- a/src-tauri/src/sketchbook/properties/_manager/_impl_session_state.rs +++ b/src-tauri/src/sketchbook/properties/_manager/_impl_session_state.rs @@ -6,7 +6,7 @@ use crate::sketchbook::event_utils::{ make_refresh_event, make_reversible, mk_dyn_prop_event, mk_dyn_prop_state_change, mk_stat_prop_event, mk_stat_prop_state_change, }; -use crate::sketchbook::ids::{DynPropertyId, StatPropertyId}; +use crate::sketchbook::ids::{DynPropertyId, StatPropertyId, VarId}; use crate::sketchbook::properties::dynamic_props::SimpleDynPropertyType; use crate::sketchbook::properties::static_props::SimpleStatPropertyType; use crate::sketchbook::properties::{DynProperty, PropertyManager, StatProperty}; @@ -26,6 +26,8 @@ const ADD_DEFAULT_PATH: &str = "add_default"; const REMOVE_PATH: &str = "remove"; // set ID of a property const SET_ID_PATH: &str = "set_id"; +// change variable ID in all static properties referencing that variable +const SET_VAR_ID_EVERYWHERE_PATH: &str = "set_var_id_everywhere"; // set content of a property const SET_CONTENT_PATH: &str = "set_content"; // refresh all dynamic properties @@ -67,6 +69,46 @@ impl SessionState for PropertyManager { } else if Self::starts_with(ADD_PATH, at_path).is_some() { Self::assert_path_length(at_path, 1, component_name)?; self.event_add_static(event) + } else if Self::starts_with(SET_VAR_ID_EVERYWHERE_PATH, at_path).is_some() { + Self::assert_path_length(at_path, 1, component_name)?; + // get the payload - json string encoding the ID change data + let payload = Self::clone_payload_str(event, component_name)?; + let change_id_data = ChangeIdData::from_json_str(&payload)?; + let old_var_id = VarId::new(&change_id_data.original_id)?; + let new_var_id = VarId::new(&change_id_data.new_id)?; + + // change values of all properties that reference this variable (ignoring the rest) + for (_, prop) in self.stat_properties.iter_mut() { + let _ = prop.set_var_id_if_present(old_var_id.clone(), new_var_id.clone()); + } + self.make_generated_reg_prop_ids_consistent(); + + // the state change is just a list of all static properties + let mut properties_list: Vec = self + .stat_properties + .iter() + .map(|(id, prop)| StatPropertyData::from_property(id, prop)) + .collect(); + properties_list.sort_by(|a, b| a.id.cmp(&b.id)); + let state_change = Event { + path: vec![ + "sketch".to_string(), + "properties".to_string(), + "static".to_string(), + "set_var_id_everywhere".to_string(), + ], + payload: Some(serde_json::to_string(&properties_list)?), + }; + + // the reverse change the opposite ID exchange + let reverse_id_change_data = + ChangeIdData::new(&change_id_data.new_id, &change_id_data.original_id); + + // prepare the reverse event (setting the original ID back) + let payload = reverse_id_change_data.to_json_str(); + let reverse_event = + mk_stat_prop_event(&[SET_VAR_ID_EVERYWHERE_PATH], Some(&payload)); + Ok(make_reversible(state_change, event, reverse_event)) } else { Self::assert_path_length(at_path, 2, component_name)?; let prop_id_str = at_path.first().unwrap(); diff --git a/src-tauri/src/sketchbook/properties/static_props/_static_property.rs b/src-tauri/src/sketchbook/properties/static_props/_static_property.rs index 4c22d8b..572c6bb 100644 --- a/src-tauri/src/sketchbook/properties/static_props/_static_property.rs +++ b/src-tauri/src/sketchbook/properties/static_props/_static_property.rs @@ -481,6 +481,25 @@ impl StatProperty { )) } } + + /// If the property is referencing the given variable (as either regulator or target), + /// set that variable to the new value. + /// + /// If not applicable, return `Err`. + pub fn set_var_id_if_present(&mut self, old_id: VarId, new_id: VarId) -> Result<(), String> { + let (reg_var, target_var) = self.get_regulator_and_target()?; + if let Some(var_id) = reg_var { + if var_id == old_id { + self.set_input_var(new_id.clone())?; + } + } + if let Some(var_id) = target_var { + if var_id == old_id { + self.set_target_var(new_id)?; + } + } + Ok(()) + } } /// Observing static properties. @@ -534,6 +553,28 @@ impl StatProperty { } Ok(()) } + + /// Get property's sub-fields for regulator variable and target variable, where applicable. + /// If not applicable, return `Err`. + pub fn get_regulator_and_target(&mut self) -> Result<(Option, Option), String> { + match &mut self.variant { + StatPropertyType::RegulationMonotonic(prop) => { + Ok((prop.input.clone(), prop.target.clone())) + } + StatPropertyType::RegulationMonotonicContext(prop) => { + Ok((prop.input.clone(), prop.target.clone())) + } + StatPropertyType::RegulationEssential(prop) => { + Ok((prop.input.clone(), prop.target.clone())) + } + StatPropertyType::RegulationEssentialContext(prop) => { + Ok((prop.input.clone(), prop.target.clone())) + } + other_variant => Err(format!( + "{other_variant:?} does not have fields for both regulator and target variable." + )), + } + } } /// Static methods to automatically generate IDs to encode regulation properties. diff --git a/src/aeon_state.ts b/src/aeon_state.ts index 189a44d..d7fbbb9 100644 --- a/src/aeon_state.ts +++ b/src/aeon_state.ts @@ -464,6 +464,9 @@ interface AeonState { staticIdChanged: Observable /** Set ID of static property with given original ID to a new id. */ setStaticId: (originalId: string, newId: string) => void + /** List of all `StaticProperty` after variable's ID is changed. + * Since var ID change can affect any property, we "refresh" all data at once. */ + allStaticUpdated: Observable } } @@ -967,6 +970,7 @@ export const aeonState: AeonState = { staticContentChanged: new Observable(['sketch', 'properties', 'static', 'set_content']), staticRemoved: new Observable(['sketch', 'properties', 'static', 'remove']), staticIdChanged: new Observable(['sketch', 'properties', 'static', 'set_id']), + allStaticUpdated: new Observable(['sketch', 'properties', 'static', 'set_var_id_everywhere']), addDefaultDynamic (variant: DynamicPropertyType): void { aeonEvents.emitAction({ diff --git a/src/html/component-editor/properties-editor/properties-editor.ts b/src/html/component-editor/properties-editor/properties-editor.ts index 1818ee9..fb5b4bf 100644 --- a/src/html/component-editor/properties-editor/properties-editor.ts +++ b/src/html/component-editor/properties-editor/properties-editor.ts @@ -105,9 +105,10 @@ export default class PropertiesEditor extends LitElement { this.addEventListener('dynamic-property-edited', (e) => { void this.editDynProperty(e) }) this.addEventListener('static-property-edited', (e) => { void this.editStatProperty(e) }) - // refresh-event listeners + // refresh-event listeners (or listeners to events that update the whole list of props) aeonState.sketch.properties.staticPropsRefreshed.addEventListener(this.#onStaticRefreshed.bind(this)) aeonState.sketch.properties.dynamicPropsRefreshed.addEventListener(this.#onDynamicRefreshed.bind(this)) + aeonState.sketch.properties.allStaticUpdated.addEventListener(this.#onStaticRefreshed.bind(this)) // note that the refresh events are automatically triggered or handled (after app refresh) directly // from the root component (due to some dependency issues between different components) diff --git a/src/html/component-editor/properties-editor/static/abstract-static-property.ts b/src/html/component-editor/properties-editor/static/abstract-static-property.ts index 9b52d6c..22e1ac5 100644 --- a/src/html/component-editor/properties-editor/static/abstract-static-property.ts +++ b/src/html/component-editor/properties-editor/static/abstract-static-property.ts @@ -38,8 +38,7 @@ export default class abstractStaticProperty extends AbstractProperty {
- +