diff --git a/src-tauri/src/app/state/editor/_state_editor_session.rs b/src-tauri/src/app/state/editor/_state_editor_session.rs index edb04bb..3237155 100644 --- a/src-tauri/src/app/state/editor/_state_editor_session.rs +++ b/src-tauri/src/app/state/editor/_state_editor_session.rs @@ -4,7 +4,7 @@ use crate::app::state::editor::TabBarState; use crate::app::state::{Consumed, Session, SessionHelper, SessionState}; use crate::app::{AeonError, DynError}; use crate::debug; -use crate::sketchbook::sketch::Sketch; +use crate::sketchbook::Sketch; /// The state of one editor session. /// diff --git a/src-tauri/src/sketchbook/sketch.rs b/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs similarity index 63% rename from src-tauri/src/sketchbook/sketch.rs rename to src-tauri/src/sketchbook/_sketch/_impl_session_state.rs index 7901f95..7688709 100644 --- a/src-tauri/src/sketchbook/sketch.rs +++ b/src-tauri/src/sketchbook/_sketch/_impl_session_state.rs @@ -2,39 +2,10 @@ use crate::app::event::Event; use crate::app::state::{Consumed, SessionHelper, SessionState}; use crate::app::DynError; use crate::sketchbook::data_structs::SketchData; -use crate::sketchbook::model::ModelState; -use crate::sketchbook::observations::ObservationManager; -use crate::sketchbook::properties::PropertyManager; -use crate::sketchbook::{JsonSerde, Manager}; -use serde::{Deserialize, Serialize}; +use crate::sketchbook::event_utils::make_state_change; +use crate::sketchbook::{JsonSerde, Sketch}; use std::fs::File; -use std::io::Write; - -/// Object encompassing all of the individual modules of the Boolean network sketch. -/// -/// Most of the actual functionality is implemented by the modules themselves, `Sketch` -/// currently only distributes events and handles situations when cooperation between -/// modules is needed. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Sketch { - model: ModelState, - observations: ObservationManager, - properties: PropertyManager, -} - -impl<'de> JsonSerde<'de> for Sketch {} -impl Manager for Sketch {} - -impl Default for Sketch { - /// Default empty sketch. - fn default() -> Sketch { - Sketch { - model: ModelState::default(), - observations: ObservationManager::default(), - properties: PropertyManager::default(), - } - } -} +use std::io::{Read, Write}; impl SessionHelper for Sketch {} @@ -47,6 +18,15 @@ impl SessionState for Sketch { self.observations.perform_event(event, at_path) } else if let Some(at_path) = Self::starts_with("properties", at_path) { self.properties.perform_event(event, at_path) + } else if Self::starts_with("new_sketch", at_path).is_some() { + self.set_to_empty(); + let sketch_data = SketchData::new(&self.model, &self.observations, &self.properties); + let state_change = make_state_change(&["sketch", "set_all"], &sketch_data); + // this is probably one of the real irreversible changes + Ok(Consumed::Irreversible { + state_change, + reset: true, + }) } else if Self::starts_with("export_sketch", at_path).is_some() { let sketch_data = SketchData::new(&self.model, &self.observations, &self.properties); let path = Self::clone_payload_str(event, "sketch")?; @@ -55,6 +35,23 @@ impl SessionState for Sketch { file.write_all(sketch_data.to_pretty_json_str().as_bytes()) .map_err(|e| e.to_string())?; Ok(Consumed::NoChange) + } else if Self::starts_with("import_sketch", at_path).is_some() { + let file_path = Self::clone_payload_str(event, "sketch")?; + // read the file contents + let mut file = File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + // parse the SketchData, modify the sketch + let sketch_data = SketchData::from_json_str(&contents)?; + self.modify_from_sketch_data(&sketch_data)?; + + let state_change = make_state_change(&["sketch", "set_all"], &sketch_data); + // this is probably one of the real irreversible changes + Ok(Consumed::Irreversible { + state_change, + reset: true, + }) } else { Self::invalid_path_error_generic(at_path) } diff --git a/src-tauri/src/sketchbook/_sketch/_impl_sketch.rs b/src-tauri/src/sketchbook/_sketch/_impl_sketch.rs new file mode 100644 index 0000000..d0f99d6 --- /dev/null +++ b/src-tauri/src/sketchbook/_sketch/_impl_sketch.rs @@ -0,0 +1,80 @@ +use crate::sketchbook::data_structs::SketchData; +use crate::sketchbook::model::ModelState; +use crate::sketchbook::observations::{Dataset, ObservationManager}; +use crate::sketchbook::properties::{DynProperty, PropertyManager, StatProperty}; +use crate::sketchbook::Sketch; + +impl Sketch { + /// Parse and validate all components of `Sketch` from a corresponding `SketchData` instance. + pub fn components_from_sketch_data( + sketch_data: &SketchData, + ) -> Result<(ModelState, ObservationManager, PropertyManager), String> { + let datasets = sketch_data + .datasets + .iter() + .map(|d| d.to_dataset()) + .collect::, String>>()?; + let dyn_properties = sketch_data + .dyn_properties + .iter() + .map(|prop_data| prop_data.to_property()) + .collect::, String>>()?; + let stat_properties = sketch_data + .stat_properties + .iter() + .map(|prop_data| prop_data.to_property()) + .collect::, String>>()?; + + let model = ModelState::new_from_model_data(&sketch_data.model)?; + let obs_manager = ObservationManager::from_datasets( + sketch_data + .datasets + .iter() + .map(|d| d.id.as_str()) + .zip(datasets) + .collect(), + )?; + let prop_manager = PropertyManager::new_from_properties( + sketch_data + .dyn_properties + .iter() + .map(|d| d.id.as_str()) + .zip(dyn_properties) + .collect(), + sketch_data + .dyn_properties + .iter() + .map(|d| d.id.as_str()) + .zip(stat_properties) + .collect(), + )?; + Ok((model, obs_manager, prop_manager)) + } + + /// Create a new `Sketch` instance given a corresponding `SketchData` object. + pub fn new_from_sketch_data(sketch_data: &SketchData) -> Result { + let (model, obs_manager, prop_manager) = Self::components_from_sketch_data(sketch_data)?; + Ok(Sketch { + model, + observations: obs_manager, + properties: prop_manager, + }) + } + + /// Modify this `Sketch` instance by loading all its components from a corresponding + /// `SketchData` instance. The original sketch information is forgotten. + pub fn modify_from_sketch_data(&mut self, sketch_data: &SketchData) -> Result<(), String> { + let (model, obs_manager, prop_manager) = Self::components_from_sketch_data(sketch_data)?; + self.model = model; + self.observations = obs_manager; + self.properties = prop_manager; + Ok(()) + } + + /// Modify this `Sketch` instance to a default (empty) settings. + pub fn set_to_empty(&mut self) { + self.model = ModelState::default(); + self.observations = ObservationManager::default(); + self.properties = PropertyManager::default(); + } +} diff --git a/src-tauri/src/sketchbook/_sketch/mod.rs b/src-tauri/src/sketchbook/_sketch/mod.rs new file mode 100644 index 0000000..c7492c3 --- /dev/null +++ b/src-tauri/src/sketchbook/_sketch/mod.rs @@ -0,0 +1,36 @@ +use crate::sketchbook::model::ModelState; +use crate::sketchbook::observations::ObservationManager; +use crate::sketchbook::properties::PropertyManager; +use crate::sketchbook::{JsonSerde, Manager}; +use serde::{Deserialize, Serialize}; + +/// **(internal)** Implementation of event-based API for the [SessionState] trait. +mod _impl_session_state; +/// **(internal)** Utility methods for `Sketch`. +mod _impl_sketch; + +/// Object encompassing all of the individual modules of the Boolean network sketch. +/// +/// Most of the actual functionality is implemented by the modules themselves, `Sketch` +/// currently only distributes events and handles situations when cooperation between +/// modules is needed. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Sketch { + model: ModelState, + observations: ObservationManager, + properties: PropertyManager, +} + +impl<'de> JsonSerde<'de> for Sketch {} +impl Manager for Sketch {} + +impl Default for Sketch { + /// Default empty sketch. + fn default() -> Sketch { + Sketch { + model: ModelState::default(), + observations: ObservationManager::default(), + properties: PropertyManager::default(), + } + } +} diff --git a/src-tauri/src/sketchbook/_tests_events/_model.rs b/src-tauri/src/sketchbook/_tests_events/_model.rs index 72e4585..38362f9 100644 --- a/src-tauri/src/sketchbook/_tests_events/_model.rs +++ b/src-tauri/src/sketchbook/_tests_events/_model.rs @@ -57,7 +57,7 @@ fn test_remove_var_complex() { model.update_position(layout_id, &var_a, 1., 1.).unwrap(); // expected result - let mut model_expected = ModelState::new(); + let mut model_expected = ModelState::new_empty(); model_expected.add_var_by_str("b", "b").unwrap(); model_expected.add_regulation_by_str("b -> b").unwrap(); @@ -136,7 +136,7 @@ fn test_set_update_fn() { #[test] /// Test that several kinds of invalid operations fail successfully. fn test_invalid_var_events() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let var_id = model.generate_var_id("a"); model.add_var(var_id.clone(), "a-name").unwrap(); let model_orig = model.clone(); @@ -232,7 +232,7 @@ fn test_remove_reg() { #[test] /// Test changing position of a layout node via event. fn test_change_position() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let layout_id = ModelState::get_default_layout_id(); let var_id = model.generate_var_id("a"); model.add_var(var_id.clone(), "a_name").unwrap(); @@ -253,9 +253,9 @@ fn test_change_position() { #[test] /// Test changing monotonicity and essentiality of uninterpreted function's argument via event. fn test_change_fn_arg_monotonicity_essentiality() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let f = model.generate_uninterpreted_fn_id("f"); - model.add_new_uninterpreted_fn(f.clone(), "f", 2).unwrap(); + model.add_empty_uninterpreted_fn(f.clone(), "f", 2).unwrap(); let model_orig = model.clone(); // test event for changing uninterpreted fn's monotonicity @@ -284,9 +284,9 @@ fn test_change_fn_arg_monotonicity_essentiality() { #[test] /// Test changing uninterpreted function's expression via event. fn test_change_fn_expression() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let f = model.generate_uninterpreted_fn_id("f"); - model.add_new_uninterpreted_fn(f.clone(), "f", 2).unwrap(); + model.add_empty_uninterpreted_fn(f.clone(), "f", 2).unwrap(); let model_orig = model.clone(); // test event for changing uninterpreted fn's expression diff --git a/src-tauri/src/sketchbook/data_structs/_layout_data.rs b/src-tauri/src/sketchbook/data_structs/_layout_data.rs index 419537a..57a73ee 100644 --- a/src-tauri/src/sketchbook/data_structs/_layout_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_layout_data.rs @@ -33,7 +33,7 @@ impl<'de> JsonSerde<'de> for LayoutData {} impl<'de> JsonSerde<'de> for LayoutMetaData {} impl LayoutData { - /// Create new `LayoutData` object given a `layout` and its id. + /// Create new `LayoutData` instance given a `layout` and its id. pub fn from_layout(layout_id: &LayoutId, layout: &Layout) -> LayoutData { let nodes = layout .layout_nodes() @@ -45,10 +45,20 @@ impl LayoutData { nodes, } } + + /// Extract new `Layout` instance from this data. + pub fn to_layout(&self) -> Result { + let var_node_pairs = self + .nodes + .iter() + .map(|node_data| (node_data.variable.as_str(), node_data.to_node())) + .collect(); + Layout::new(&self.name, var_node_pairs) + } } impl LayoutMetaData { - /// Create new `LayoutMetaData` object given a layout's name and id string slices. + /// Create new `LayoutMetaData` instance given a layout's name and id string slices. pub fn new(layout_id: &str, layout_name: &str) -> LayoutMetaData { LayoutMetaData { id: layout_id.to_string(), @@ -56,7 +66,7 @@ impl LayoutMetaData { } } - /// Create new `LayoutMetaData` object given a `layout` and its id. + /// Create new `LayoutMetaData` instance given a `layout` and its id. pub fn from_layout(layout_id: &LayoutId, layout: &Layout) -> LayoutMetaData { LayoutMetaData::new(layout_id.as_str(), layout.get_layout_name()) } diff --git a/src-tauri/src/sketchbook/data_structs/_layout_node_data.rs b/src-tauri/src/sketchbook/data_structs/_layout_node_data.rs index 0da31dc..f64b64e 100644 --- a/src-tauri/src/sketchbook/data_structs/_layout_node_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_layout_node_data.rs @@ -22,6 +22,7 @@ pub struct LayoutNodeData { impl<'de> JsonSerde<'de> for LayoutNodeData {} impl LayoutNodeData { + /// Create new `LayoutNodeData` instance given a node's layout ID, variable ID, and coordinates. pub fn new(layout_id: &str, var_id: &str, px: f32, py: f32) -> LayoutNodeData { LayoutNodeData { layout: layout_id.to_string(), @@ -31,6 +32,8 @@ impl LayoutNodeData { } } + /// Create new `LayoutNodeData` instance given a node's layout ID, variable ID, + /// and corresponding `LayoutNode`. pub fn from_node(layout_id: &LayoutId, var_id: &VarId, node: &LayoutNode) -> LayoutNodeData { LayoutNodeData::new( layout_id.as_str(), @@ -39,4 +42,9 @@ impl LayoutNodeData { node.get_py(), ) } + + /// Extract new `LayoutNode` instance from this data. + pub fn to_node(&self) -> LayoutNode { + LayoutNode::new(self.px, self.py) + } } diff --git a/src-tauri/src/sketchbook/data_structs/_model_data.rs b/src-tauri/src/sketchbook/data_structs/_model_data.rs index e5b8068..ab56d7c 100644 --- a/src-tauri/src/sketchbook/data_structs/_model_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_model_data.rs @@ -18,7 +18,7 @@ impl<'de> JsonSerde<'de> for ModelData {} impl ModelData { /// Create new `SketchData` instance given a reference to a model manager instance. - pub fn new(model: &ModelState) -> ModelData { + pub fn from_model(model: &ModelState) -> ModelData { let mut variables: Vec<_> = model .variables() .map(|(id, v)| VariableData::from_var(id, v, model.get_update_fn(id).unwrap())) diff --git a/src-tauri/src/sketchbook/data_structs/_observation_data.rs b/src-tauri/src/sketchbook/data_structs/_observation_data.rs index d73db8b..ecd7c7f 100644 --- a/src-tauri/src/sketchbook/data_structs/_observation_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_observation_data.rs @@ -18,7 +18,7 @@ pub struct ObservationData { impl<'de> JsonSerde<'de> for ObservationData {} impl ObservationData { - /// Create new `ObservationData` object given `id` and values string slices. + /// Create new `ObservationData` instance given `id` and values string slices. pub fn new(obs_id: &str, dataset_id: &str, values: &str) -> ObservationData { ObservationData { id: obs_id.to_string(), @@ -27,7 +27,7 @@ impl ObservationData { } } - /// Create new `ObservationData` object given a reference to a observation, and ID of + /// Create new `ObservationData` instance given a reference to a observation, and ID of /// its dataset. pub fn from_obs(obs: &Observation, dataset_id: &DatasetId) -> ObservationData { ObservationData::new( diff --git a/src-tauri/src/sketchbook/data_structs/_property_data.rs b/src-tauri/src/sketchbook/data_structs/_property_data.rs index a1d5f1f..8b2e6fa 100644 --- a/src-tauri/src/sketchbook/data_structs/_property_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_property_data.rs @@ -1,5 +1,5 @@ -use crate::sketchbook::ids::DynPropertyId; -use crate::sketchbook::properties::DynProperty; +use crate::sketchbook::ids::{DynPropertyId, StatPropertyId}; +use crate::sketchbook::properties::{DynProperty, StatProperty}; use crate::sketchbook::JsonSerde; use serde::{Deserialize, Serialize}; @@ -15,7 +15,7 @@ pub struct DynPropertyData { pub formula: String, } -/// Structure for sending data about dynamic properties to the frontend. +/// Structure for sending data about static properties to the frontend. /// /// Some fields simplified compared to original typesafe versions (e.g., pure `Strings` are used /// instead of more complex typesafe structs) to allow for easier (de)serialization. @@ -24,13 +24,14 @@ pub struct DynPropertyData { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StatPropertyData { pub id: String, + pub formula: String, } impl<'de> JsonSerde<'de> for DynPropertyData {} impl<'de> JsonSerde<'de> for StatPropertyData {} impl DynPropertyData { - /// Create new `DynPropertyData` object given a properties `id` and formula. + /// Create new `DynPropertyData` object given a properties `id` and `formula`. pub fn new(id: &str, formula: &str) -> DynPropertyData { DynPropertyData { id: id.to_string(), @@ -42,11 +43,29 @@ impl DynPropertyData { pub fn from_property(id: &DynPropertyId, property: &DynProperty) -> DynPropertyData { DynPropertyData::new(id.as_str(), property.get_formula()) } + + /// Extract the corresponding `DynProperty` instance from this `DynPropertyData`. + pub fn to_property(&self) -> Result { + DynProperty::try_from_str(&self.formula) + } } impl StatPropertyData { - /// Create new `StatPropertyData` object given a properties `id`. - pub fn new(id: &str) -> StatPropertyData { - StatPropertyData { id: id.to_string() } + /// Create new `StatPropertyData` object given a properties `id` and `formula`. + pub fn new(id: &str, formula: &str) -> StatPropertyData { + StatPropertyData { + id: id.to_string(), + formula: formula.to_string(), + } + } + + /// Create new `StatPropertyData` object given a reference to a property and its `id`. + pub fn from_property(id: &StatPropertyId, property: &StatProperty) -> StatPropertyData { + StatPropertyData::new(id.as_str(), property.get_formula()) + } + + /// Extract the corresponding `StatProperty` instance from this `StatPropertyData`. + pub fn to_property(&self) -> Result { + StatProperty::try_from_str(&self.formula) } } diff --git a/src-tauri/src/sketchbook/data_structs/_regulation_data.rs b/src-tauri/src/sketchbook/data_structs/_regulation_data.rs index 78e9c19..10b3d3e 100644 --- a/src-tauri/src/sketchbook/data_structs/_regulation_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_regulation_data.rs @@ -1,3 +1,4 @@ +use crate::sketchbook::ids::VarId; use crate::sketchbook::model::{Essentiality, Monotonicity, Regulation}; use crate::sketchbook::JsonSerde; use serde::{Deserialize, Serialize}; @@ -47,4 +48,14 @@ impl RegulationData { let regulation = Regulation::try_from_string(regulation_str)?; Ok(RegulationData::from_reg(®ulation)) } + + /// Extract new `Regulation` instance from this data. + pub fn to_reg(&self) -> Result { + Ok(Regulation::new( + VarId::new(&self.regulator)?, + VarId::new(&self.target)?, + self.essential, + self.sign, + )) + } } diff --git a/src-tauri/src/sketchbook/data_structs/_sketch_data.rs b/src-tauri/src/sketchbook/data_structs/_sketch_data.rs index 97e158d..9e9c851 100644 --- a/src-tauri/src/sketchbook/data_structs/_sketch_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_sketch_data.rs @@ -33,11 +33,11 @@ impl SketchData { .collect(); let stat_properties = properties .stat_props() - .map(|(p_id, _)| StatPropertyData::new(p_id.as_str())) + .map(|(p_id, p)| StatPropertyData::from_property(p_id, p)) .collect(); SketchData { - model: ModelData::new(model), + model: ModelData::from_model(model), datasets, dyn_properties, stat_properties, diff --git a/src-tauri/src/sketchbook/data_structs/_uninterpreted_fn_data.rs b/src-tauri/src/sketchbook/data_structs/_uninterpreted_fn_data.rs index e894ece..0dbf6b4 100644 --- a/src-tauri/src/sketchbook/data_structs/_uninterpreted_fn_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_uninterpreted_fn_data.rs @@ -1,5 +1,7 @@ use crate::sketchbook::ids::UninterpretedFnId; -use crate::sketchbook::model::{Essentiality, FnArgument, Monotonicity, UninterpretedFn}; +use crate::sketchbook::model::{ + Essentiality, FnArgument, ModelState, Monotonicity, UninterpretedFn, +}; use crate::sketchbook::JsonSerde; use serde::{Deserialize, Serialize}; @@ -51,4 +53,23 @@ impl UninterpretedFnData { uninterpreted_fn.get_fn_expression(), ) } + + /// Extract new `UninterpretedFn` instance from this data (if the function's expression + /// is valid). + /// + /// Model is given for validity check during parsing the function's expression. + pub fn to_uninterpreted_fn(&self, model: &ModelState) -> Result { + let arguments = self + .arguments + .iter() + .map(|(m, e)| FnArgument::new(*e, *m)) + .collect(); + UninterpretedFn::new( + &self.name, + &self.expression, + arguments, + model, + &model.get_uninterpreted_fn_id(&self.id)?, + ) + } } diff --git a/src-tauri/src/sketchbook/data_structs/_variable_data.rs b/src-tauri/src/sketchbook/data_structs/_variable_data.rs index 4865ee8..fa862c7 100644 --- a/src-tauri/src/sketchbook/data_structs/_variable_data.rs +++ b/src-tauri/src/sketchbook/data_structs/_variable_data.rs @@ -36,4 +36,9 @@ impl VariableData { update_fn.get_fn_expression(), ) } + + /// Extract new `Variable` instance from this data. + pub fn to_var(&self) -> Result { + Variable::new(self.name.as_str()) + } } diff --git a/src-tauri/src/sketchbook/ids.rs b/src-tauri/src/sketchbook/ids.rs index 2de6994..3be437f 100644 --- a/src-tauri/src/sketchbook/ids.rs +++ b/src-tauri/src/sketchbook/ids.rs @@ -13,7 +13,7 @@ lazy_static! { /// **(internal)** A base class to derive type-safe identifiers from (using a macro below). #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] -struct BaseId { +pub struct BaseId { id: String, } diff --git a/src-tauri/src/sketchbook/layout/_layout/_impl_layout.rs b/src-tauri/src/sketchbook/layout/_layout/_impl_layout.rs index 718a8ea..65330da 100644 --- a/src-tauri/src/sketchbook/layout/_layout/_impl_layout.rs +++ b/src-tauri/src/sketchbook/layout/_layout/_impl_layout.rs @@ -6,19 +6,33 @@ use std::collections::HashMap; /// Methods for safely constructing or mutating instances of `Layout`. impl Layout { /// Create new empty `Layout` (i.e., with no nodes) with a given name. - pub fn new_empty(name_str: &str) -> Result { - assert_name_valid(name_str)?; + pub fn new_empty(name: &str) -> Result { + assert_name_valid(name)?; Ok(Layout { - name: name_str.to_string(), + name: name.to_string(), nodes: HashMap::new(), }) } + /// Create new `Layout` with a given name and nodes. + pub fn new(name: &str, var_node_pairs: Vec<(&str, LayoutNode)>) -> Result { + // before making any changes, check that all IDs are actually valid and unique + let var_ids: Vec<&str> = var_node_pairs.iter().map(|pair| pair.0).collect(); + assert_ids_unique(&var_ids)?; + + // now we can safely add them + let mut layout = Layout::new_empty(name)?; + for (var_id, node) in var_node_pairs { + layout.add_node(VarId::new(var_id)?, node)?; + } + Ok(layout) + } + /// Create new `Layout` with a given name, that is a direct copy of another existing /// valid `template_layout`. - pub fn new_from_another_copy(name_str: &str, template_layout: &Layout) -> Layout { + pub fn new_from_another_copy(name: &str, template_layout: &Layout) -> Layout { Layout { - name: name_str.to_string(), + name: name.to_string(), nodes: template_layout.nodes.clone(), } } @@ -27,24 +41,32 @@ impl Layout { /// all of the nodes will be located at a default position. /// /// Returns `Error` if given ids contain duplicates. - pub fn new_from_vars_default( - name_str: &str, - variable_ids: Vec, - ) -> Result { + pub fn new_from_vars_default(name: &str, variables: Vec) -> Result { // before making any changes, check that all IDs are actually valid and unique - assert_ids_unique(&variable_ids)?; + assert_ids_unique(&variables)?; // now we can safely add them - let mut layout = Layout::new_empty(name_str)?; - for var_id in variable_ids { + let mut layout = Layout::new_empty(name)?; + for var_id in variables { layout.add_default_node(var_id.clone())?; } Ok(layout) } /// Rename this `Layout`. - pub fn set_layout_name(&mut self, name_str: &str) -> Result<(), String> { - assert_name_valid(name_str)?; - self.name = name_str.to_string(); + pub fn set_layout_name(&mut self, name: &str) -> Result<(), String> { + assert_name_valid(name)?; + self.name = name.to_string(); + Ok(()) + } + + /// Add a new (pre-generated) node. + /// + /// You must ensure that the `variable` is valid before adding it to the layout. + /// + /// Returns `Err` if there already is a node for this variable. + pub fn add_node(&mut self, var: VarId, node: LayoutNode) -> Result<(), String> { + self.assert_no_variable(&var)?; + self.nodes.insert(var, node); Ok(()) } @@ -54,11 +76,9 @@ impl Layout { /// You must ensure that the `variable` is valid before adding it to the layout. /// /// Returns `Err` if there already is a node for this variable. - pub fn add_node(&mut self, variable: VarId, p_x: f32, p_y: f32) -> Result<(), String> { - if self.nodes.contains_key(&variable) { - return Err(format!("Layout data for {variable} already exist.")); - } - self.nodes.insert(variable, LayoutNode::new(p_x, p_y)); + pub fn add_node_by_coords(&mut self, var: VarId, p_x: f32, p_y: f32) -> Result<(), String> { + self.assert_no_variable(&var)?; + self.nodes.insert(var, LayoutNode::new(p_x, p_y)); Ok(()) } @@ -68,9 +88,7 @@ impl Layout { /// /// Returns `Err` if there already is a node for this variable. pub fn add_default_node(&mut self, variable: VarId) -> Result<(), String> { - if self.nodes.contains_key(&variable) { - return Err(format!("Layout data for {variable} already exist.")); - } + self.assert_no_variable(&variable)?; self.nodes.insert(variable, LayoutNode::default()); Ok(()) } @@ -84,11 +102,10 @@ impl Layout { new_x: f32, new_y: f32, ) -> Result<(), String> { + self.assert_valid_variable(variable)?; self.nodes .get_mut(variable) - .ok_or(format!( - "Variable {variable} doesn't have a layout information to remove." - ))? + .unwrap() .change_position(new_x, new_y); Ok(()) } @@ -97,27 +114,42 @@ impl Layout { /// /// Return `Err` if variable did not have a corresponding node in this layout. pub fn remove_node(&mut self, variable: &VarId) -> Result<(), String> { - if self.nodes.remove(variable).is_none() { - return Err(format!( - "Variable {variable} doesn't have a layout information to remove." - )); - } + self.assert_valid_variable(variable)?; + self.nodes.remove(variable); Ok(()) } /// Change id of a variable with `original_id` to `new_id`. pub fn change_node_id(&mut self, original_id: &VarId, new_id: VarId) -> Result<(), String> { + self.assert_valid_variable(original_id)?; if let Some(node_layout) = self.nodes.remove(original_id) { self.nodes.insert(new_id.clone(), node_layout); - } else { - return Err(format!( - "Variable {original_id} doesn't have a layout information to remove." - )); } Ok(()) } } +/// Utility methods to assert (non-)existence of nodes in the layout. +impl Layout { + /// **(internal)** Utility method to ensure there is no node for the variable with given Id yet. + fn assert_no_variable(&self, var_id: &VarId) -> Result<(), String> { + if self.nodes.contains_key(var_id) { + Err(format!("Layout node for {var_id} already exists.")) + } else { + Ok(()) + } + } + + /// **(internal)** Utility method to ensure there is a node for a variable with given Id. + fn assert_valid_variable(&self, var_id: &VarId) -> Result<(), String> { + if self.nodes.contains_key(var_id) { + Ok(()) + } else { + Err(format!("Layout node for {var_id} does not exist.")) + } + } +} + /// Methods for observing instances of `ModelState` (various getters, etc.). impl Layout { /// Layout information regarding the node for a particular variable. @@ -172,8 +204,8 @@ mod tests { // add node v1, node v2, and try adding v1 again (should fail) layout.add_default_node(var_id1.clone()).unwrap(); assert_eq!(layout.get_node(&var_id1).unwrap(), &default_node); - layout.add_node(var_id2.clone(), 1., 2.).unwrap(); - assert!(layout.add_node(var_id1_again, 1., 2.).is_err()); + layout.add_node_by_coords(var_id2.clone(), 1., 2.).unwrap(); + assert!(layout.add_node_by_coords(var_id1_again, 1., 2.).is_err()); assert_eq!(layout.get_num_nodes(), 2); // change position of node v1, and try changing position of node thats not in the network diff --git a/src-tauri/src/sketchbook/layout/_layout/_impl_layout_serde.rs b/src-tauri/src/sketchbook/layout/_layout/_impl_layout_serde.rs index f3f6d07..9d14af6 100644 --- a/src-tauri/src/sketchbook/layout/_layout/_impl_layout_serde.rs +++ b/src-tauri/src/sketchbook/layout/_layout/_impl_layout_serde.rs @@ -121,7 +121,9 @@ mod tests { #[test] fn test_layout_serde() { let mut layout = Layout::new_empty("layout_name").unwrap(); - layout.add_node(VarId::new("v1").unwrap(), 1., 1.).unwrap(); + layout + .add_node_by_coords(VarId::new("v1").unwrap(), 1., 1.) + .unwrap(); // Serialization let layout_serialized = serde_json::to_string(&layout).unwrap(); diff --git a/src-tauri/src/sketchbook/layout/_layout/mod.rs b/src-tauri/src/sketchbook/layout/_layout/mod.rs index bd45e45..fcd048c 100644 --- a/src-tauri/src/sketchbook/layout/_layout/mod.rs +++ b/src-tauri/src/sketchbook/layout/_layout/mod.rs @@ -1,5 +1,6 @@ use crate::sketchbook::ids::VarId; use crate::sketchbook::layout::LayoutNode; +use crate::sketchbook::Manager; use std::collections::HashMap; use std::fmt::{Display, Error, Formatter}; use std::str::FromStr; @@ -17,6 +18,8 @@ pub struct Layout { nodes: HashMap, } +impl Manager for Layout {} + impl Display for Layout { /// Use json serialization to convert `Layout` to string. fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { diff --git a/src-tauri/src/sketchbook/mod.rs b/src-tauri/src/sketchbook/mod.rs index d81ff8f..852a256 100644 --- a/src-tauri/src/sketchbook/mod.rs +++ b/src-tauri/src/sketchbook/mod.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::hash::Hash; use std::str::FromStr; +use utils::assert_ids_unique; /// Structs and utility methods that can be used for communication with frontend. pub mod data_structs; @@ -13,9 +16,9 @@ pub mod model; pub mod observations; /// Classes and utility methods regarding properties. pub mod properties; -/// The main `Sketch` manager object and its utilities. -pub mod sketch; +/// The main `Sketch` manager object and its utilities. +mod _sketch; /// **(internal)** Utility functions specifically related to events. mod event_utils; /// **(internal)** General utilities used throughout the module (e.g., serialization @@ -26,6 +29,8 @@ mod utils; #[cfg(test)] mod _tests_events; +pub use crate::sketchbook::_sketch::Sketch; + /// Trait that implements `to_json_str` and `from_json_str` wrappers to serialize and /// deserialize objects, utilizing [serde_json]. /// @@ -67,7 +72,7 @@ pub trait Manager { ) -> T where T: FromStr, - ::Err: std::fmt::Debug, + ::Err: Debug, { // first try to use the `ideal_id` if let Ok(id) = T::from_str(ideal_id) { @@ -106,4 +111,49 @@ pub trait Manager { // this must be valid, we already tried more than `max_idx` options T::from_str(format!("{}_{}", transformed_id, max_idx).as_str()).unwrap() } + + /// Check that the list of (typesafe or string) IDs contains only unique IDs (no duplicates), + /// and check that all of the IDs are already managed by the manager instance (this is + /// important, for instance, when we need to change already existing elements). + /// + /// Manager class' method to assert ID validity must be provided. + fn assert_ids_unique_and_used( + &self, + id_list: &Vec<&str>, + assert_id_is_managed: &dyn Fn(&Self, &T) -> Result<(), String>, + ) -> Result<(), String> + where + T: Eq + Hash + Debug + FromStr, + ::Err: Debug, + { + assert_ids_unique(id_list)?; + for &id_str in id_list { + let id = T::from_str(id_str).map_err(|e| format!("{e:?}"))?; + assert_id_is_managed(self, &id)?; + } + Ok(()) + } + + /// Check that the list of (typesafe or string) IDs contains only unique IDs (no duplicates), + /// and check that all of the IDs are NOT yet managed by the manager instance, i.e., + /// they are fresh new values (this is important, for instance, when we need to add several new + /// elements). + /// + /// Manager class' method to assert ID validity must be provided. + fn assert_ids_unique_and_new( + &self, + id_list: &Vec<&str>, + assert_id_is_new: &dyn Fn(&Self, &T) -> Result<(), String>, + ) -> Result<(), String> + where + T: Eq + Hash + Debug + FromStr, + ::Err: Debug, + { + assert_ids_unique(id_list)?; + for &id_str in id_list { + let id = T::from_str(id_str).map_err(|e| format!("{e:?}"))?; + assert_id_is_new(self, &id)?; + } + Ok(()) + } } diff --git a/src-tauri/src/sketchbook/model/_function_tree.rs b/src-tauri/src/sketchbook/model/_function_tree.rs index 5f3be3a..44d78a0 100644 --- a/src-tauri/src/sketchbook/model/_function_tree.rs +++ b/src-tauri/src/sketchbook/model/_function_tree.rs @@ -321,7 +321,7 @@ mod tests { #[test] /// Test parsing of a valid uninterpreted function's expression. fn test_valid_uninterpreted_fn() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let arity = 2; model.add_uninterpreted_fn_by_str("f", "f", arity).unwrap(); model.add_uninterpreted_fn_by_str("g", "g", arity).unwrap(); diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_convert_reg_graph.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_convert_reg_graph.rs index 86a84b5..113df8c 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_convert_reg_graph.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_convert_reg_graph.rs @@ -50,7 +50,7 @@ impl ModelState { /// /// Note that only the default layout (all nodes at 0,0) is created for the `ModelState`. pub fn from_reg_graph(reg_graph: RegulatoryGraph) -> Result { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); // variables for v in reg_graph.variables() { diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_editing.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_editing.rs index f971a74..ef2f9ca 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_editing.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_editing.rs @@ -1,9 +1,12 @@ +use crate::sketchbook::data_structs::ModelData; use crate::sketchbook::ids::{LayoutId, UninterpretedFnId, VarId}; use crate::sketchbook::layout::Layout; use crate::sketchbook::model::{ - Essentiality, ModelState, Monotonicity, Regulation, UninterpretedFn, UpdateFn, Variable, + Essentiality, FnArgument, ModelState, Monotonicity, Regulation, UninterpretedFn, UpdateFn, + Variable, }; use crate::sketchbook::utils::assert_ids_unique; +use crate::sketchbook::Manager; use std::cmp::max; use std::collections::{HashMap, HashSet}; @@ -14,7 +17,7 @@ use std::collections::{HashMap, HashSet}; impl ModelState { /// Create a new `ModelState` that does not contain any `Variables`, `Uninterpreted Functions`, /// or `Regulations` yet. It contains a single empty default `Layout`. - pub fn new() -> ModelState { + pub fn new_empty() -> ModelState { let default_layout_id = ModelState::get_default_layout_id(); // `get_default_layout_name()` returns a valid name, so we can safely unwrap let default_layout = Layout::new_empty(ModelState::get_default_layout_name()).unwrap(); @@ -28,6 +31,63 @@ impl ModelState { } } + /// Create a new `ModelState` given a corresponding `ModelData` instance. + /// + /// It is not that efficient currently, but should result in correct/consistent model. + /// TODO: try to rewrite more efficiently without compromising the correctness. + pub fn new_from_model_data(model_data: &ModelData) -> Result { + // start with variables and plain function symbols (so that they can be used in expressions later) + let variables = model_data + .variables + .iter() + .map(|v| (v.id.as_str(), v.name.as_str())) + .collect(); + let plain_functions = model_data + .uninterpreted_fns + .iter() + .map(|f| (f.id.as_str(), f.name.as_str(), f.arguments.len())) + .collect(); + let mut model = ModelState::new_from_vars(variables)?; + model.add_multiple_uninterpreted_fns(plain_functions)?; + + // add regulations + model_data.regulations.iter().try_for_each(|r| { + model.add_regulation( + VarId::new(&r.regulator)?, + VarId::new(&r.target)?, + r.essential, + r.sign, + ) + })?; + + // add layouts + for layout_data in &model_data.layouts { + let layout = layout_data.to_layout()?; + // check layout has valid variables + model.add_or_update_layout_raw(LayoutId::new(&layout_data.id)?, layout)?; + } + + // set update fns + let update_fns = model_data + .variables + .iter() + .map(|v| (v.id.as_str(), v.update_fn.as_str())) + .collect(); + model.set_multiple_update_fns(update_fns)?; + + // set expressions and arguments for uninterpreted fns + for f in &model_data.uninterpreted_fns { + model.set_uninterpreted_fn_expression_by_str(&f.id, &f.expression)?; + let arguments = f + .arguments + .iter() + .map(|(m, e)| FnArgument::new(*e, *m)) + .collect(); + model.set_uninterpreted_fn_all_args_by_str(&f.id, arguments)?; + } + Ok(model) + } + /// Create new `ModelState` using provided variable ID-name pairs, both strings. /// All variables have default (empty) update functions. /// Result will contain no `UninterpretedFns` or `Regulations`, and a single default `Layout`. @@ -37,7 +97,7 @@ impl ModelState { /// /// Return `Err` in case the IDs are not unique. pub fn new_from_vars(variables: Vec<(&str, &str)>) -> Result { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let var_ids = variables.iter().map(|pair| pair.0).collect(); assert_ids_unique(&var_ids)?; @@ -90,8 +150,8 @@ impl ModelState { id_name_pairs: Vec<(&str, &str)>, ) -> Result<(), String> { // before making any changes, check that all IDs are actually valid and unique - let var_ids = id_name_pairs.iter().map(|pair| pair.0).collect(); - assert_ids_unique(&var_ids)?; + let var_ids: Vec<&str> = id_name_pairs.iter().map(|pair| pair.0).collect(); + self.assert_ids_unique_and_new(&var_ids, &(Self::assert_no_variable))?; for id in var_ids { let var_id = VarId::new(id)?; self.assert_no_variable(&var_id)?; @@ -103,12 +163,28 @@ impl ModelState { Ok(()) } + /// Add a new uninterpreted fn given by its components. + pub fn add_uninterpreted_fn( + &mut self, + fn_id: UninterpretedFnId, + name: &str, + arguments: Vec, + expression: &str, + ) -> Result<(), String> { + self.assert_no_uninterpreted_fn(&fn_id)?; + let arity = arguments.len(); + let f = UninterpretedFn::new(name, expression, arguments, self, &fn_id)?; + self.uninterpreted_fns.insert(fn_id, f); + self.add_placeholder_vars_if_needed(arity); + Ok(()) + } + /// Add a new uninterpreted fn with given `id`, `name` and `arity` to this `ModelState`. /// Note that constraints regarding monotonicity or essentiality must be added separately. /// /// The ID must be valid identifier that is not already used by some other uninterpreted fn. /// Returns `Err` in case the `id` is already being used. - pub fn add_new_uninterpreted_fn( + pub fn add_empty_uninterpreted_fn( &mut self, fn_id: UninterpretedFnId, name: &str, @@ -116,7 +192,7 @@ impl ModelState { ) -> Result<(), String> { self.assert_no_uninterpreted_fn(&fn_id)?; self.uninterpreted_fns.insert( - fn_id.clone(), + fn_id, UninterpretedFn::new_without_constraints(name, arity)?, ); self.add_placeholder_vars_if_needed(arity); @@ -134,7 +210,7 @@ impl ModelState { arity: usize, ) -> Result<(), String> { let fn_id = UninterpretedFnId::new(id)?; - self.add_new_uninterpreted_fn(fn_id, name, arity) + self.add_empty_uninterpreted_fn(fn_id, name, arity) } /// Shorthand to add a list of new uninterpreted fns, each with a string ID, name, and arity, @@ -151,11 +227,7 @@ impl ModelState { .iter() .map(|triplet| triplet.0) .collect(); - assert_ids_unique(&fn_ids)?; - for id in fn_ids { - let fn_id = UninterpretedFnId::new(id)?; - self.assert_no_uninterpreted_fn(&fn_id)?; - } + self.assert_ids_unique_and_new(&fn_ids, &(Self::assert_no_uninterpreted_fn))?; // now we can safely add them for (id, name, arity) in id_name_arity_tuples { self.add_uninterpreted_fn_by_str(id, name, arity)?; @@ -487,6 +559,28 @@ impl ModelState { self.set_uninterpreted_fn_monotonicity(&fn_id, monotonicity, index) } + /// Set constraints on all arguments of given uninterpreted fn. + pub fn set_uninterpreted_fn_all_args( + &mut self, + fn_id: &UninterpretedFnId, + fn_arguments: Vec, + ) -> Result<(), String> { + self.assert_valid_uninterpreted_fn(fn_id)?; + let uninterpreted_fn = self.uninterpreted_fns.get_mut(fn_id).unwrap(); + uninterpreted_fn.set_all_arguments(fn_arguments)?; + Ok(()) + } + + /// Set constraints on all arguments of given uninterpreted fn. + pub fn set_uninterpreted_fn_all_args_by_str( + &mut self, + id: &str, + fn_arguments: Vec, + ) -> Result<(), String> { + let fn_id = UninterpretedFnId::new(id)?; + self.set_uninterpreted_fn_all_args(&fn_id, fn_arguments) + } + /// Set the id of an uninterpreted fn with `original_id` to `new_id`. /// /// Note that this operation may be costly as it affects several components of the state. @@ -647,6 +741,28 @@ impl ModelState { Ok(()) } + /// Set update functions for multiple variables (given ID-function pairs). + /// The IDs must be unique valid identifiers. + pub fn set_multiple_update_fns( + &mut self, + update_functions: Vec<(&str, &str)>, + ) -> Result<(), String> { + // before making any changes, we must perform all validity checks + // -> check IDs are unique, correspond to existing variables, and that expressions are valid + let var_ids = update_functions.iter().map(|pair| pair.0).collect(); + self.assert_ids_unique_and_used(&var_ids, &(Self::assert_valid_variable))?; + let parsed_fns = update_functions + .iter() + .map(|(_, expression)| UpdateFn::try_from_str(expression, self)) + .collect::, String>>()?; + + // now we can just simply add them all + for (i, parsed_fn) in parsed_fns.into_iter().enumerate() { + self.update_fns.insert(VarId::new(var_ids[i])?, parsed_fn); + } + Ok(()) + } + /// **(internal)** Utility method to add as many placeholder variables as is required by /// an addition (or update) of an uninterpreted fn of given arity. fn add_placeholder_vars_if_needed(&mut self, arity: usize) { @@ -703,6 +819,20 @@ impl ModelState { Ok(()) } + /// Add a new (pre-generated) `Layout` with given `id` to this `ModelState`, or update + /// existing if the `id` is already used. The layout must contain nodes for exactly all model's + /// variables. + pub fn add_or_update_layout_raw(&mut self, id: LayoutId, layout: Layout) -> Result<(), String> { + let model_vars: HashSet<_> = self.variables.keys().collect(); + let layout_vars: HashSet<_> = layout.layout_nodes().map(|(v, _)| v).collect(); + if model_vars != layout_vars { + return Err("Model variables and layout variables are different.".to_string()); + } + + self.layouts.insert(id, layout); + Ok(()) + } + /// Add a new `Layout` with given `layout_id` and `name` to this `ModelState`. The layout /// will be a direct copy of another existing layout given by id `template_layout_id`. /// @@ -902,7 +1032,7 @@ mod tests { /// Test generating new default variant of the `ModelState`. #[test] fn test_new_default() { - let model = ModelState::new(); + let model = ModelState::new_empty(); assert_eq!(model.num_vars(), 0); assert_eq!(model.num_uninterpreted_fns(), 0); assert_eq!(model.num_placeholder_vars(), 0); @@ -933,7 +1063,7 @@ mod tests { /// Test adding variables (both incrementally and at once). #[test] fn test_adding_variables() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); // one by one add variables a, b, c model.add_var_by_str("a", "a_name").unwrap(); @@ -952,7 +1082,7 @@ mod tests { /// Test adding uninterpreted functions (both incrementally and at once). #[test] fn test_adding_uninterpreted_fns() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); model.add_uninterpreted_fn_by_str("f", "f", 1).unwrap(); model.add_uninterpreted_fn_by_str("g", "g", 0).unwrap(); @@ -1000,7 +1130,7 @@ mod tests { /// covered by other tests. #[test] fn test_manually_editing_regulation_graph() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); // add variables a, b, c let variables = vec![("a", "a_name"), ("b", "b_name"), ("c", "c_name")]; @@ -1033,7 +1163,7 @@ mod tests { /// Test manually creating `ModelState` and mutating it by adding/removing uninterpreted fns. #[test] fn test_manually_editing_uninterpreted_fns() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let uninterpreted_fns = vec![("f", "f", 4), ("g", "g", 1)]; model .add_multiple_uninterpreted_fns(uninterpreted_fns) @@ -1118,7 +1248,7 @@ mod tests { /// Test adding invalid variables. #[test] fn test_add_invalid_vars() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); // same names should not be an issue let variables = vec![("a", "a_name"), ("b", "b_name")]; model.add_multiple_variables(variables).unwrap(); @@ -1137,7 +1267,7 @@ mod tests { /// Test adding invalid regulations. #[test] fn test_add_invalid_regs() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let variables = vec![("a", "a_name"), ("b", "b_name")]; model.add_multiple_variables(variables).unwrap(); let var_a = model.get_var_id("a").unwrap(); @@ -1165,7 +1295,7 @@ mod tests { /// Test that changing variable's name works correctly. #[test] fn test_var_name_change() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let variables = vec![("a", "a_name"), ("b", "b_name")]; model.add_multiple_variables(variables).unwrap(); let var_a = model.get_var_id("a").unwrap(); @@ -1182,7 +1312,7 @@ mod tests { /// Test that changing variable's ID works correctly. #[test] fn test_var_id_change() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let var_a = model.generate_var_id("a"); model.add_var(var_a.clone(), "a_name").unwrap(); let var_b = model.generate_var_id("b"); diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_id_generating.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_id_generating.rs index f6bd120..5ffdf71 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_id_generating.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_id_generating.rs @@ -83,7 +83,7 @@ mod tests { #[test] fn test_layout_id_generating() { - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let layout_id = LayoutId::new("l_0").unwrap(); let default_layout_id = ModelState::get_default_layout_id(); model.add_layout_simple(layout_id, "name").unwrap(); diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_serde.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_serde.rs index eaf51e5..7c8176c 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_serde.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_serde.rs @@ -195,7 +195,7 @@ mod tests { #[test] fn test_model_state_serde() { // test on very simple `ModelState` with one var and no regulations - let mut model = ModelState::new(); + let mut model = ModelState::new_empty(); let var_id = VarId::new("a").unwrap(); model.add_var(var_id, "a").unwrap(); diff --git a/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_refresh_events.rs b/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_refresh_events.rs index f1efb77..ff4d56f 100644 --- a/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_refresh_events.rs +++ b/src-tauri/src/sketchbook/model/_model_state/_impl_session_state/_refresh_events.rs @@ -12,7 +12,7 @@ use crate::sketchbook::JsonSerde; impl ModelState { /// Get a whole model. pub(super) fn refresh_whole_model(&self, full_path: &[String]) -> Result { - let model_data = ModelData::new(self); + let model_data = ModelData::from_model(self); Ok(Event { path: full_path.to_vec(), payload: Some(model_data.to_json_str()), diff --git a/src-tauri/src/sketchbook/model/_model_state/mod.rs b/src-tauri/src/sketchbook/model/_model_state/mod.rs index 776a29d..7debfe5 100644 --- a/src-tauri/src/sketchbook/model/_model_state/mod.rs +++ b/src-tauri/src/sketchbook/model/_model_state/mod.rs @@ -41,6 +41,6 @@ impl Default for ModelState { /// Default model object with no Variables, Uninterpreted Functions, or Regulations yet. /// It contains a single empty default Layout. fn default() -> ModelState { - ModelState::new() + ModelState::new_empty() } } diff --git a/src-tauri/src/sketchbook/model/_uninterpreted_fn.rs b/src-tauri/src/sketchbook/model/_uninterpreted_fn.rs index ffdb969..026cdef 100644 --- a/src-tauri/src/sketchbook/model/_uninterpreted_fn.rs +++ b/src-tauri/src/sketchbook/model/_uninterpreted_fn.rs @@ -30,6 +30,23 @@ impl UninterpretedFn { }) } + /// Create new `UninterpretedFn` instance given its components. + /// Model and ID are used for validity check during argument parsing. + pub fn new( + name: &str, + expression: &str, + arguments: Vec, + model: &ModelState, + own_id: &UninterpretedFnId, + ) -> Result { + assert_name_valid(name)?; + let arity = arguments.len(); + let mut f = UninterpretedFn::new_without_constraints(name, arity)?; + f.set_all_arguments(arguments)?; + f.set_fn_expression(expression, model, own_id)?; + Ok(f) + } + /// Create uninterpreted function using another one as a template, but changing the expression. /// The provided original function object is consumed. pub fn with_new_expression( @@ -297,7 +314,7 @@ mod tests { // this test is a hack, normally just edit the function's expression through the `ModelState` // object that owns it - let mut context = ModelState::new(); + let mut context = ModelState::new_empty(); context.add_uninterpreted_fn_by_str("f", "f", 3).unwrap(); let fn_id = UninterpretedFnId::new("f").unwrap(); diff --git a/src-tauri/src/sketchbook/properties/_dynamic_property.rs b/src-tauri/src/sketchbook/properties/_dynamic_property.rs index 36dcac3..9d54124 100644 --- a/src-tauri/src/sketchbook/properties/_dynamic_property.rs +++ b/src-tauri/src/sketchbook/properties/_dynamic_property.rs @@ -11,9 +11,9 @@ pub struct DynProperty { formula: String, } -/// Creating properties. +/// Creating dynamic properties. impl DynProperty { - /// Create ` DynProperty` object directly from a formula, which must be in a correct format. + /// Create `DynProperty` object directly from a formula, which must be in a correct format. /// /// TODO: add syntax check. pub fn try_from_str(formula: &str) -> Result { @@ -21,7 +21,7 @@ impl DynProperty { Ok(DynProperty::new_raw(formula)) } - /// **internal** Create ` DynProperty` object directly from a string formula, + /// **internal** Create `DynProperty` object directly from a string formula, /// without any syntax checks on it. fn new_raw(formula: &str) -> Self { DynProperty { @@ -61,12 +61,12 @@ impl DynProperty { } } -/// Editing properties. +/// Editing dynamic properties. impl DynProperty { // TODO } -/// Observing properties. +/// Observing dynamic properties. impl DynProperty { pub fn get_formula(&self) -> &str { &self.formula diff --git a/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs b/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs index 5af59fb..a16c8f0 100644 --- a/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs +++ b/src-tauri/src/sketchbook/properties/_manager/_impl_manager.rs @@ -1,8 +1,9 @@ use crate::sketchbook::ids::{DynPropertyId, StatPropertyId}; use crate::sketchbook::properties::{ - DynPropIterator, DynProperty, PropertyManager, StatPropIterator, + DynPropIterator, DynProperty, PropertyManager, StatPropIterator, StatProperty, }; -use std::collections::{HashMap, HashSet}; +use crate::sketchbook::utils::assert_ids_unique; +use std::collections::HashMap; impl PropertyManager { /// Instantiate `PropertyManager` with empty sets of properties. @@ -13,26 +14,57 @@ impl PropertyManager { } } - /// Instantiate `PropertyManager` with dynamic properties given as a list of ID-formula pairs. - pub fn new_from_dyn_properties( - properties: Vec<(&str, &str)>, + /// Instantiate `PropertyManager` with dynamic and static properties given as a list + /// of ID-formula pairs. + pub fn new_from_formulae( + dyn_properties: Vec<(&str, &str)>, + stat_properties: Vec<(&str, &str)>, ) -> Result { let mut manager = PropertyManager::new_empty(); - let prop_id_set = properties.iter().map(|pair| pair.0).collect::>(); - if prop_id_set.len() != properties.len() { - return Err(format!( - "Properties {:?} contain duplicate IDs.", - properties - )); - } + let dyn_prop_ids = dyn_properties.iter().map(|pair| pair.0).collect(); + assert_ids_unique(&dyn_prop_ids)?; + + let stat_prop_ids = stat_properties.iter().map(|pair| pair.0).collect(); + assert_ids_unique(&stat_prop_ids)?; - for (id, formula) in properties { + for (id, formula) in dyn_properties { let prop_id = DynPropertyId::new(id)?; manager .dyn_properties .insert(prop_id, DynProperty::try_from_str(formula)?); } + for (id, formula) in stat_properties { + let prop_id = StatPropertyId::new(id)?; + manager + .stat_properties + .insert(prop_id, StatProperty::try_from_str(formula)?); + } + Ok(manager) + } + + /// Instantiate `PropertyManager` with dynamic and static properties given as a list + /// of ID-property pairs. + pub fn new_from_properties( + dyn_properties: Vec<(&str, DynProperty)>, + stat_properties: Vec<(&str, StatProperty)>, + ) -> Result { + let mut manager = PropertyManager::new_empty(); + + let dyn_prop_ids = dyn_properties.iter().map(|pair| pair.0).collect(); + assert_ids_unique(&dyn_prop_ids)?; + + let stat_prop_ids = stat_properties.iter().map(|pair| pair.0).collect(); + assert_ids_unique(&stat_prop_ids)?; + + for (id, prop) in dyn_properties { + manager.dyn_properties.insert(DynPropertyId::new(id)?, prop); + } + for (id, prop) in stat_properties { + manager + .stat_properties + .insert(StatPropertyId::new(id)?, prop); + } Ok(manager) } } diff --git a/src-tauri/src/sketchbook/properties/_static_property.rs b/src-tauri/src/sketchbook/properties/_static_property.rs index 4629289..d66cd2f 100644 --- a/src-tauri/src/sketchbook/properties/_static_property.rs +++ b/src-tauri/src/sketchbook/properties/_static_property.rs @@ -4,18 +4,37 @@ use serde::{Deserialize, Serialize}; /// /// TODO: Currently, this is just a placeholder. #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct StatProperty {} +pub struct StatProperty { + formula: String, +} /// Creating properties. impl StatProperty { - /// TODO - just a placeholder for now - pub fn new() -> StatProperty { - StatProperty {} + /// Create `StatProperty` object directly from a formula, which must be in a correct format. + /// + /// TODO: add syntax check. + pub fn try_from_str(formula: &str) -> Result { + // todo: syntax check + Ok(StatProperty::new_raw(formula)) + } + + /// **internal** Create `StatProperty` object directly from a string formula, + /// without any syntax checks on it. + fn new_raw(formula: &str) -> Self { + StatProperty { + formula: formula.to_string(), + } } } -impl Default for StatProperty { - fn default() -> Self { - Self::new() +/// Editing static properties. +impl StatProperty { + // TODO +} + +/// Observing static properties. +impl StatProperty { + pub fn get_formula(&self) -> &str { + &self.formula } } diff --git a/src/aeon_events.ts b/src/aeon_events.ts index 9359c6f..b09af25 100644 --- a/src/aeon_events.ts +++ b/src/aeon_events.ts @@ -46,8 +46,15 @@ interface AeonState { sketchRefreshed: Observable /** Refresh the whole sketch. */ refreshSketch: () => void + /** Export the sketch data to a file. */ exportSketch: (path: string) => void + /** Import the sketch data from a file. */ + importSketch: (path: string) => void + /** Set the sketch to a "default" mode, essentially emptying it and starting anew. */ + newSketch: () => void + /** The whole modified sketch instance (after importing or starting a new sketch). */ + sketchReplaced: Observable /** The state of the main model. */ model: { @@ -365,6 +372,7 @@ export interface DynPropertyData { /** A PLACEHOLDER object representing a static property. */ export interface StatPropertyData { id: string + formula: string } /** An object representing information needed for variable id change. */ @@ -801,6 +809,19 @@ export const aeonState: AeonState = { payload: path }) }, + sketchReplaced: new Observable(['sketch', 'set_all']), + importSketch (path: string): void { + aeonEvents.emitAction({ + path: ['sketch', 'import_sketch'], + payload: path + }) + }, + newSketch (): void { + aeonEvents.emitAction({ + path: ['sketch', 'new_sketch'], + payload: null + }) + }, model: { modelRefreshed: new Observable(['sketch', 'model', 'get_whole_model']), diff --git a/src/html/component/menu/menu.ts b/src/html/component/menu/menu.ts index 0ce3ad7..bd2f1e0 100644 --- a/src/html/component/menu/menu.ts +++ b/src/html/component/menu/menu.ts @@ -4,6 +4,10 @@ import style_less from './menu.less?inline' import { map } from 'lit/directives/map.js' import { open, save } from '@tauri-apps/api/dialog' import { appWindow } from '@tauri-apps/api/window' +import { + aeonState +} from '../../../aeon_events' +import { dialog } from '@tauri-apps/api' // TODO: close menu when clicked outside @@ -15,23 +19,31 @@ export default class Menu extends LitElement { @state() menuItems: IMenuItem[] = [ { label: 'New sketch', - action: () => { console.log('new') } + action: () => { void this.newSketch() } }, { label: 'Import...', - action: () => { void this.import() } + action: () => { void this.importSketch() } }, { label: 'Export...', - action: () => { void this.export() } + action: () => { void this.exportSketch() } }, { label: 'Quit', - action: this.quit + action: () => { void this.quit() } } ] - async import (): Promise { + async importSketch (): Promise { + const confirmation = await dialog.ask('Are you sure? This operation is irreversible.', { + type: 'warning', + okLabel: 'Import', + cancelLabel: 'Cancel', + title: 'Import new sketch' + }) + if (!confirmation) return + let selected = await open({ title: 'Import sketch...', multiple: false, @@ -46,9 +58,10 @@ export default class Menu extends LitElement { } console.log('importing', selected) + aeonState.sketch.importSketch(selected) } - async export (): Promise { + async exportSketch (): Promise { const filePath = await save({ title: 'Export sketch...', filters: [{ @@ -60,9 +73,31 @@ export default class Menu extends LitElement { if (filePath === null) return console.log('exporting to', filePath) + aeonState.sketch.exportSketch(filePath) + } + + async newSketch (): Promise { + const confirmation = await dialog.ask('Are you sure? This operation is irreversible.', { + type: 'warning', + okLabel: 'New sketch', + cancelLabel: 'Cancel', + title: 'Start new sketch' + }) + if (!confirmation) return + + console.log('loading new sketch') + aeonState.sketch.newSketch() } - quit (): void { + async quit (): Promise { + const confirmation = await dialog.ask('Are you sure? This operation is irreversible.', { + type: 'warning', + okLabel: 'Quit', + cancelLabel: 'Cancel', + title: 'Quit' + }) + if (!confirmation) return + void appWindow.close() } diff --git a/src/html/component/observations-editor/observations-editor.ts b/src/html/component/observations-editor/observations-editor.ts index a6a8eff..9d0720d 100644 --- a/src/html/component/observations-editor/observations-editor.ts +++ b/src/html/component/observations-editor/observations-editor.ts @@ -14,7 +14,8 @@ import { type DatasetData, type DatasetIdUpdateData, type ObservationData, - type ObservationIdUpdateData + type ObservationIdUpdateData, + type SketchData } from '../../../aeon_events' @customElement('observations-editor') @@ -42,6 +43,9 @@ export default class ObservationsEditor extends LitElement { // refresh-event listeners aeonState.sketch.observations.datasetsRefreshed.addEventListener(this.#onDatasetsRefreshed.bind(this)) + // when refreshing/replacing whole sketch, this component is responsible for updating the `Datasets` part + aeonState.sketch.sketchRefreshed.addEventListener(this.#onSketchRefreshed.bind(this)) + aeonState.sketch.sketchReplaced.addEventListener(this.#onSketchRefreshed.bind(this)) // refreshing content from backend aeonState.sketch.observations.refreshDatasets() @@ -87,6 +91,11 @@ export default class ObservationsEditor extends LitElement { } } + #onSketchRefreshed (sketch: SketchData): void { + // when refreshing/replacing whole sketch, this component is responsible for updating the `Datasets` part + this.#onDatasetsRefreshed(sketch.datasets) + } + #onDatasetsRefreshed (refreshedDatasets: DatasetData[]): void { const datasets = refreshedDatasets.map(d => this.convertToIObservationSet(d)) this.index = datasets.length diff --git a/src/html/component/root-component/root-component.ts b/src/html/component/root-component/root-component.ts index 289d177..44d468c 100644 --- a/src/html/component/root-component/root-component.ts +++ b/src/html/component/root-component/root-component.ts @@ -11,6 +11,7 @@ import { type LayoutNodeDataPrototype, type ModelData, type RegulationData, + type SketchData, type UninterpretedFnData, type VariableData, type VariableIdUpdateData @@ -80,6 +81,9 @@ export default class RootComponent extends LitElement { aeonState.sketch.model.variablesRefreshed.addEventListener(this.#onVariablesRefreshed.bind(this)) aeonState.sketch.model.layoutNodesRefreshed.addEventListener(this.#onLayoutNodesRefreshed.bind(this)) aeonState.sketch.model.regulationsRefreshed.addEventListener(this.#onRegulationsRefreshed.bind(this)) + // when refreshing/replacing whole sketch, this component is responsible for updating the `Model` part + aeonState.sketch.sketchRefreshed.addEventListener(this.#onSketchRefreshed.bind(this)) + aeonState.sketch.sketchReplaced.addEventListener(this.#onSketchRefreshed.bind(this)) // event listener to capture changes from FunctionEditor with updated uninterpreted functions this.addEventListener('save-functions', this.saveFunctionData.bind(this)) @@ -355,6 +359,11 @@ export default class RootComponent extends LitElement { } } + #onSketchRefreshed (sketch: SketchData): void { + // when refreshing/replacing whole sketch, this component is responsible for updating the `Model` part + this.#onModelRefreshed(sketch.model) + } + #onModelRefreshed (model: ModelData): void { const functions = model.uninterpreted_fns.map((data): IFunctionData => { return this.convertToIFunction(data)