From 092806a82595c5356cc32a05a6c97c44f44959b8 Mon Sep 17 00:00:00 2001 From: --add Date: Sat, 10 May 2025 16:14:47 -0700 Subject: [PATCH 1/5] changed connector to stream --- Cargo.toml | 3 +- oscps-gui/Cargo.toml | 1 + oscps-gui/src/flowsheet.rs | 156 +++++++++++----------- oscps-gui/src/main.rs | 1 + oscps-lib/src/blocks.rs | 2 +- oscps-lib/src/lib.rs | 3 +- oscps-lib/src/simulation.rs | 113 ++++++++-------- oscps-lib/src/{connector.rs => stream.rs} | 29 ++-- 8 files changed, 157 insertions(+), 151 deletions(-) rename oscps-lib/src/{connector.rs => stream.rs} (62%) diff --git a/Cargo.toml b/Cargo.toml index 7ca18e5..7d654db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" -members = [ "oscps-gui", +members = [ + "oscps-gui", "oscps-lib", ] diff --git a/oscps-gui/Cargo.toml b/oscps-gui/Cargo.toml index 0822647..32cb601 100644 --- a/oscps-gui/Cargo.toml +++ b/oscps-gui/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" env_logger = "0.11.6" iced = {version = "0.13.1", features = ["canvas", "debug", "lazy"]} log = "0.4" +oscps-lib = { path = "../oscps-lib" } diff --git a/oscps-gui/src/flowsheet.rs b/oscps-gui/src/flowsheet.rs index 6a18a06..73a77c1 100644 --- a/oscps-gui/src/flowsheet.rs +++ b/oscps-gui/src/flowsheet.rs @@ -2,8 +2,9 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; +use oscps_lib::simulation::Simulation; -use log::{info, debug}; +use log::{debug, info}; #[derive(Default)] pub struct State { @@ -58,6 +59,8 @@ impl<'a> canvas::Program for Flowsheet<'a> { let Some(cursor_position) = cursor.position_in(bounds) else { return (event::Status::Ignored, None); }; + // TODO: Do not allow a user to connect the input of a stream to the + // output of a block and vice-versa. match event { Event::Mouse(mouse_event) => { let message = match mouse_event { @@ -72,10 +75,12 @@ impl<'a> canvas::Program for Flowsheet<'a> { from: cursor_position, }); for curve in self.curves { - if !matches!(curve, Curve::Connector{..}) && curve.on_output_connector(cursor_position) { + if !matches!(curve, Curve::Connector { .. }) + && curve.on_output_connector(cursor_position) + { info!("Connected to input!"); result = Some(Pending::One { - from: curve.get_output_point() + from: curve.get_output_point(), }); break; } @@ -87,60 +92,60 @@ impl<'a> canvas::Program for Flowsheet<'a> { info!("Created connector."); *state = None; let mut result = Some(Curve::Connector { - from, - to: cursor_position - }); + from, + to: cursor_position, + }); for curve in self.curves { - if !matches!(curve, Curve::Connector{..}) && curve.on_input_connector(cursor_position) { + if !matches!(curve, Curve::Connector { .. }) + && curve.on_input_connector(cursor_position) + { info!("Connected to input!"); result = Some(Curve::Connector { from, - to: curve.get_input_point() + to: curve.get_input_point(), }); break; } } result - } - // Some(Pending::Two { from, to }) => { - // *state = None; - // Some(Curve::Connector { - // from, - // to, - // }) - // } + } // Some(Pending::Two { from, to }) => { + // *state = None; + // Some(Curve::Connector { + // from, + // to, + // }) + // } } - }, - BlockPlacement::Mixer => { - let input_point = Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); - let output_point = Point::new(cursor_position.x + 105.0, cursor_position.y + 50.0); + } + BlockPlacement::Mixer => { + let input_point = + Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); + let output_point = + Point::new(cursor_position.x + 105.0, cursor_position.y + 50.0); info!("Creating mixer at ({}, {}) with input at ({}, {}) and output at ({}, {})", cursor_position.x, cursor_position.y, input_point.x, input_point.y, output_point.x, output_point.y); Some(Curve::Mixer { at: cursor_position, input_point, output_point, }) - }, + } BlockPlacement::Default => { // TODO: Add code for selecting stuff None } } - - }, + } // Right click should cancel placement. mouse::Event::ButtonPressed(mouse::Button::Right) => { info!("Right mouse button clicked"); - *state = None; + *state = None; None } _ => None, }; (event::Status::Captured, message) - }, - Event::Keyboard(_) => { - (event::Status::Captured, None) } + Event::Keyboard(_) => (event::Status::Captured, None), _ => (event::Status::Ignored, None), } } @@ -153,16 +158,15 @@ impl<'a> canvas::Program for Flowsheet<'a> { bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec { - let content = - self.state.cache.draw(renderer, bounds.size(), |frame| { - Curve::draw_all(self.curves, frame, theme); - frame.stroke( - &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default() + let content = self.state.cache.draw(renderer, bounds.size(), |frame| { + Curve::draw_all(self.curves, frame, theme); + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default() .with_width(20.0) .with_color(theme.palette().text), - ); - }); + ); + }); if let Some(pending) = state { vec![content, pending.draw(renderer, theme, bounds, cursor)] } else { @@ -193,22 +197,22 @@ pub enum Curve { at: Point, input_point: Point, output_point: Point, - } + }, } impl Curve { - fn on_input_connector( - &self, - cursor_position: Point - ) -> bool { + fn on_input_connector(&self, cursor_position: Point) -> bool { // TODO: Fix arbitrary 5-pixel bounding box. Ideally use a circular bound. let input = self.get_input_point(); - info!("Checking input bounds with cursor at ({}, {})", cursor_position.x, cursor_position.y); + info!( + "Checking input bounds with cursor at ({}, {})", + cursor_position.x, cursor_position.y + ); if cursor_position.x > input.x - 5.0 && cursor_position.x < input.x + 5.0 { debug!("Bound x match!"); if cursor_position.y > input.y - 5.0 && cursor_position.y < input.y + 5.0 { info!("Bounds match!"); - return true + return true; } } false @@ -216,26 +220,22 @@ impl Curve { fn get_input_point(&self) -> Point { return match self { - Curve::Connector{from, ..} => { - *from - }, - Curve::Mixer{input_point, ..} => { - *input_point - }, - } + Curve::Connector { from, .. } => *from, + Curve::Mixer { input_point, .. } => *input_point, + }; } - fn on_output_connector( - &self, - cursor_position: Point - ) -> bool { + fn on_output_connector(&self, cursor_position: Point) -> bool { let output = self.get_output_point(); - info!("Checking output bounds with cursor at ({}, {})", cursor_position.x, cursor_position.y); + info!( + "Checking output bounds with cursor at ({}, {})", + cursor_position.x, cursor_position.y + ); if cursor_position.x > output.x - 5.0 && cursor_position.x < output.x + 5.0 { debug!("Bound x match!"); if cursor_position.y > output.y - 5.0 && cursor_position.y < output.y + 5.0 { info!("Bounds match!"); - return true + return true; } } false @@ -243,45 +243,45 @@ impl Curve { fn get_output_point(&self) -> Point { return match self { - Curve::Connector{to, ..} => { - *to - }, - Curve::Mixer{output_point, ..} => { - *output_point - }, - } + Curve::Connector { to, .. } => *to, + Curve::Mixer { output_point, .. } => *output_point, + }; } fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { let curves = Path::new(|p| { for curve in curves { match curve { - Curve::Connector{ from, to } => { + Curve::Connector { from, to } => { debug!("Drawing connector"); p.move_to(*from); // p.quadratic_curve_to(*control, *to); - let half_x_coord = from.x + (to.x - from.x)/2.0; + let half_x_coord = from.x + (to.x - from.x) / 2.0; p.line_to(Point::new(half_x_coord, from.y)); p.line_to(Point::new(half_x_coord, to.y)); p.line_to(Point::new(to.x, to.y)); let mut arrow_offset_x = -10.0; - let arrow_offset_y = 5.0; + let arrow_offset_y = 5.0; if to.x < from.x { - arrow_offset_x *= -1.0; - } + arrow_offset_x *= -1.0; + } p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); p.line_to(Point::new(to.x, to.y)); } - Curve::Mixer{at, input_point, output_point} => { + Curve::Mixer { + at, + input_point, + output_point, + } => { debug!("Drawing mixer."); - p.move_to(*at); + p.move_to(*at); // p.rectangle(*at, Size::new(200.0, 200.0)); let bottom_point = Point::new(at.x, at.y + 100.0); let middle_point = Point::new(at.x + 100.0, at.y + 50.0); p.line_to(bottom_point); p.line_to(middle_point); p.line_to(*at); - // Draw a circle for input connectors + // Draw a circle for input connectors p.move_to(*at); p.circle(*input_point, 5.0); // Another circle for output connectors @@ -295,8 +295,8 @@ impl Curve { frame.stroke( &curves, Stroke::default() - .with_width(2.0) - .with_color(theme.palette().text), + .with_width(2.0) + .with_color(theme.palette().text), ); } } @@ -324,16 +324,16 @@ impl Pending { let line = Path::new(|p| { p.move_to(from); // p.quadratic_curve_to(*control, *to); - let half_x_coord = from.x + (to.x - from.x)/2.0; + let half_x_coord = from.x + (to.x - from.x) / 2.0; p.line_to(Point::new(half_x_coord, from.y)); p.line_to(Point::new(half_x_coord, to.y)); p.line_to(Point::new(to.x, to.y)); let mut arrow_offset_x = -10.0; - let arrow_offset_y = 5.0; + let arrow_offset_y = 5.0; if to.x < from.x { - arrow_offset_x *= -1.0; - } + arrow_offset_x *= -1.0; + } p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); p.line_to(Point::new(to.x, to.y)); @@ -341,8 +341,8 @@ impl Pending { frame.stroke( &line, Stroke::default() - .with_width(2.0) - .with_color(theme.palette().text), + .with_width(2.0) + .with_color(theme.palette().text), ); } }; diff --git a/oscps-gui/src/main.rs b/oscps-gui/src/main.rs index 5fc3d3c..17add1d 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -8,6 +8,7 @@ use iced::{Center, Element, Fill, Size, Theme}; use log::{info, debug}; pub fn main() -> iced::Result { + // Start the GUI env_logger::init(); info!("Starting application"); iced::application("Open Source Chemical Process Simulator", MainWindow::update, MainWindow::view) diff --git a/oscps-lib/src/blocks.rs b/oscps-lib/src/blocks.rs index ff99f2c..b40c738 100644 --- a/oscps-lib/src/blocks.rs +++ b/oscps-lib/src/blocks.rs @@ -6,7 +6,7 @@ //! For example, if a block is a simple mixer, then it will implement the //! MassBalance trait but not the EnergyBalance. -use crate::connector::Stream; +use crate::stream::Stream; use once_cell::sync::Lazy; use uom::si::energy::joule; use uom::si::f64::Energy; diff --git a/oscps-lib/src/lib.rs b/oscps-lib/src/lib.rs index e81652e..9255cc1 100644 --- a/oscps-lib/src/lib.rs +++ b/oscps-lib/src/lib.rs @@ -7,7 +7,6 @@ pub mod blocks; pub mod component; -pub mod connector; pub mod simulation; +pub mod stream; pub mod thermodynamics; - diff --git a/oscps-lib/src/simulation.rs b/oscps-lib/src/simulation.rs index 251ed28..cc918b0 100644 --- a/oscps-lib/src/simulation.rs +++ b/oscps-lib/src/simulation.rs @@ -2,7 +2,10 @@ //! //! Allows for the construction of a simulation object. TODO: Implement this. -// use std::collections::HashMap; +use crate::blocks::Block; +use crate::stream::Stream; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; // fn compute_outlet_phase_fractions(&self) { @@ -14,16 +17,17 @@ // fn compute_outlet_pressure(&self) { -// } -// #[derive(Debug, Clone)] -// struct Settings { -// // Add fields as needed // } -// #[derive(Debug, Clone)] -// struct SimulationState { -// // Add fields as needed -// } +#[derive(Debug, Clone)] +struct Settings { + // Add fields as needed +} + +#[derive(Debug, Clone)] +struct SimulationState { + // Add fields as needed +} // #[derive(Debug)] // enum Err { @@ -34,49 +38,50 @@ // Other(String), // } -// struct Simulation { -// blocks: HashMap, -// connectors: HashMap, -// settings: Settings, -// state: SimulationState, -// } +pub struct Simulation { + blocks: HashMap>>>, + streams: HashMap>>>, + settings: Settings, + state: SimulationState, +} + +impl Simulation { + pub fn new(settings: Settings, state: SimulationState) -> Self { + Self { + blocks: HashMap::new(), + streams: HashMap::new(), + settings, + state, + } + } + + // pub fn add_block(&mut self, block_id: i32, block: Block) -> Result<(), Err> { + // if self.blocks.contains_key(&block_id) { + // return Err(Err::BlockExists); + // } + // self.blocks.insert(block_id, block); + // Ok(()) + // } + + // pub fn add_connector(&mut self, connector_id: i32, connector: Connector) -> Result<(), Err> { + // if self.connectors.contains_key(&connector_id) { + // return Err(Err::ConnectorExists); + // } + // self.connectors.insert(connector_id, connector); + // Ok(()) + // } + + // pub fn remove_block(&mut self, block_id: i32) -> Result<(), Err> { + // if self.blocks.remove(&block_id).is_none() { + // return Err(Err::BlockNotFound); + // } + // Ok(()) + // } -// impl Simulation { -// pub fn new(settings: Settings, state: SimulationState) -> Self { -// Self { -// blocks: HashMap::new(), -// connectors: HashMap::new(), -// settings, -// state, -// } -// } - -// pub fn add_block(&mut self, block_id: i32, block: Block) -> Result<(), Err> { -// if self.blocks.contains_key(&block_id) { -// return Err(Err::BlockExists); -// } -// self.blocks.insert(block_id, block); -// Ok(()) -// } - -// pub fn add_connector(&mut self, connector_id: i32, connector: Connector) -> Result<(), Err> { -// if self.connectors.contains_key(&connector_id) { -// return Err(Err::ConnectorExists); -// } -// self.connectors.insert(connector_id, connector); -// Ok(()) -// } - -// pub fn remove_block(&mut self, block_id: i32) -> Result<(), Err> { -// if self.blocks.remove(&block_id).is_none() { -// return Err(Err::BlockNotFound); -// } -// Ok(()) -// } - -// pub fn remove_connector(&mut self, connector_id: i32) -> Result<(), Err> { -// if self.connectors.remove(&connector_id).is_none() { -// return Err(Err::ConnectorNotFound); -// } -// Ok(()) -// } + // pub fn remove_connector(&mut self, connector_id: i32) -> Result<(), Err> { + // if self.connectors.remove(&connector_id).is_none() { + // return Err(Err::ConnectorNotFound); + // } + // Ok(()) + // } +} diff --git a/oscps-lib/src/connector.rs b/oscps-lib/src/stream.rs similarity index 62% rename from oscps-lib/src/connector.rs rename to oscps-lib/src/stream.rs index fa67621..30c57cf 100644 --- a/oscps-lib/src/connector.rs +++ b/oscps-lib/src/stream.rs @@ -1,37 +1,36 @@ -//! # Connector +//! # Stream //! -use crate::thermodynamics::ThermoState; +use crate::thermodynamics::ThermoState; /// # Stream -/// +/// /// Struct to hold stream information pub struct Stream { // TODO: IDs must be unique within the flowsheet. Consider using integers // as IDs and having a separate field for the name of a connector. Adopt // a similar scheme for blocks. - /// ID of the stream. - pub s_id : String, + /// ID of the stream. + pub s_id: String, /// Instance of ThermoState struct that holds thermodynamic information. - pub thermo : Option, - // TODO: Change these from strings to integers, or better yet, + pub thermo: Option, + // TODO: Change these from strings to integers, or better yet, // references to the source and destination blocks, to minimize // computation time spent on looking for sources and destinations. /// ID of source block - pub from_block : String, + pub from_block: String, /// ID of destination block - pub to_block : String + pub to_block: String, } - impl Stream { /// Constructor for 'Stream' struct - pub fn new(id: String, from_blk_id : String, to_blk_id : String) -> Stream { + pub fn new(id: String, from_blk_id: String, to_blk_id: String) -> Stream { Stream { - s_id : id, - thermo : None, - from_block : from_blk_id, - to_block : to_blk_id + s_id: id, + thermo: None, + from_block: from_blk_id, + to_block: to_blk_id, } } } From 0780f841507d8d5cac0158544505daea5fddf6f5 Mon Sep 17 00:00:00 2001 From: --add Date: Sat, 7 Jun 2025 15:42:31 -0700 Subject: [PATCH 2/5] simulation updates --- oscps-gui/src/flowsheet.rs | 1 + oscps-lib/src/simulation.rs | 58 +++++++++++++++++++++++------------ q.log | 60 +++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 q.log diff --git a/oscps-gui/src/flowsheet.rs b/oscps-gui/src/flowsheet.rs index 73a77c1..cc83621 100644 --- a/oscps-gui/src/flowsheet.rs +++ b/oscps-gui/src/flowsheet.rs @@ -10,6 +10,7 @@ use log::{debug, info}; pub struct State { cache: canvas::Cache, pub placement_mode: BlockPlacement, + pub simulation: Simulation, } impl State { diff --git a/oscps-lib/src/simulation.rs b/oscps-lib/src/simulation.rs index cc918b0..f9c3868 100644 --- a/oscps-lib/src/simulation.rs +++ b/oscps-lib/src/simulation.rs @@ -19,33 +19,48 @@ use std::sync::{Arc, RwLock}; // } -#[derive(Debug, Clone)] -struct Settings { +/// A struct for storing settings of the simulation +#[derive(Debug, Clone, Default)] +pub struct Settings { // Add fields as needed } -#[derive(Debug, Clone)] -struct SimulationState { +/// A struct for storing the current state of the simulation +#[derive(Debug, Clone, Default)] +pub struct SimulationState { // Add fields as needed } -// #[derive(Debug)] -// enum Err { -// BlockNotFound, -// ConnectorNotFound, -// BlockExists, -// ConnectorExists, -// Other(String), -// } +/// An enum used to represent errors. +#[derive(Debug)] +pub enum Err { + /// Error when a block is not found + BlockNotFound, + /// Error when a connector is not found + ConnectorNotFound, + /// Error when a block with a matching ID is already in the simulation + BlockExists, + /// Error when a connector with a matching ID is already in the simulation + ConnectorExists, + /// Any other error + Other(String), +} +/// The Simulation struct stores information pertaining to blocks and streams +#[derive(Default)] pub struct Simulation { + /// Stores all the blocks in the simulation blocks: HashMap>>>, + /// Stores all the streams in the simulation streams: HashMap>>>, + /// Stores simulation settings settings: Settings, + /// Stores the state of the simlation state: SimulationState, } impl Simulation { + /// Create a new simultion pub fn new(settings: Settings, state: SimulationState) -> Self { Self { blocks: HashMap::new(), @@ -55,13 +70,18 @@ impl Simulation { } } - // pub fn add_block(&mut self, block_id: i32, block: Block) -> Result<(), Err> { - // if self.blocks.contains_key(&block_id) { - // return Err(Err::BlockExists); - // } - // self.blocks.insert(block_id, block); - // Ok(()) - // } + /// Add a block to the simulation. + pub fn add_block( + &mut self, + block_id: i32, + block: Arc>>, + ) -> Result<(), Err> { + if self.blocks.contains_key(&block_id) { + return Err(Err::BlockExists); + } + self.blocks.insert(block_id, block); + Ok(()) + } // pub fn add_connector(&mut self, connector_id: i32, connector: Connector) -> Result<(), Err> { // if self.connectors.contains_key(&connector_id) { diff --git a/q.log b/q.log new file mode 100644 index 0000000..4efa4ce --- /dev/null +++ b/q.log @@ -0,0 +1,60 @@ +This is LuaHBTeX, Version 1.22.0 (TeX Live 2025) (format=lualatex 2025.4.27) 10 MAY 2025 16:48 + restricted system commands enabled. +**q +(c:/texlive/2025/texmf-dist/tex/latex/tools/q.tex +LaTeX2e <2024-11-01> patch level 2 +L3 programming layer <2025-03-26> +Lua module: luaotfload 2024-12-03 v3.29 Lua based OpenType font support +Lua module: lualibs 2023-07-13 v2.76 ConTeXt Lua standard libraries. +Lua module: lualibs-extended 2023-07-13 v2.76 ConTeXt Lua libraries -- extended +collection. +luaotfload | conf : Root cache directory is "C:/texlive/2025/texmf-var/luatex-ca +che/generic/names". +luaotfload | init : Loading fontloader "fontloader-2023-12-28.lua" from kpse-res +olved path "c:/texlive/2025/texmf-dist/tex/luatex/luaotfload/fontloader-2023-12- +28.lua". +Lua-only attribute luaotfload@noligature = 1 +luaotfload | init : Context OpenType loader version 3.134 +Inserting `luaotfload.node_processor' in `pre_linebreak_filter'. +Inserting `luaotfload.node_processor' in `hpack_filter'. +Inserting `luaotfload.glyph_stream' in `glyph_stream_provider'. +Inserting `luaotfload.define_font' in `define_font'. +Lua-only attribute luaotfload_color_attribute = 2 +luaotfload | conf : Root cache directory is "C:/texlive/2025/texmf-var/luatex-ca +che/generic/names". +Inserting `luaotfload.harf.strip_prefix' in `find_opentype_file'. +Inserting `luaotfload.harf.strip_prefix' in `find_truetype_file'. +Removing `luaotfload.glyph_stream' from `glyph_stream_provider'. +Inserting `luaotfload.harf.glyphstream' in `glyph_stream_provider'. +Inserting `luaotfload.harf.finalize_vlist' in `post_linebreak_filter'. +Inserting `luaotfload.harf.finalize_hlist' in `hpack_filter'. +Inserting `luaotfload.cleanup_files' in `wrapup_run'. +Inserting `luaotfload.harf.finalize_unicode' in `finish_pdffile'. +Inserting `luaotfload.glyphinfo' in `glyph_info'. +Lua-only attribute luaotfload.letterspace_done = 3 +Inserting `luaotfload.aux.set_sscale_dimens' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.set_font_index' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.patch_cambria_domh' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.fixup_fontdata' in `luaotfload.patch_font_unsafe'. +Inserting `luaotfload.aux.set_capheight' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.set_xheight' in `luaotfload.patch_font'. +Inserting `luaotfload.rewrite_fontname' in `luaotfload.patch_font'. +Inserting `tracingstacklevels' in `input_level_string'. File ignored +) +! Emergency stop. +<*> q + +*** (job aborted, no legal \end found) + + + +Here is how much of LuaTeX's memory you used: + 16 strings out of 471941 + 100000,662416 words of node,token memory allocated 301 words of node memory still in use: + 1 hlist, 1 dir, 3 kern, 1 glyph, 1 attribute, 39 glue_spec, 1 attribute_list +nodes + avail lists: 2:10,3:3,4:1 + 26706 multiletter control sequences out of 65536+600000 + 14 fonts using 591679 bytes + 12i,0n,13p,80b,15s stack positions out of 10000i,1000n,20000p,200000b,200000s +! ==> Fatal error occurred, no output PDF file produced! From 8b8d8abd2634cce3c853d71a920bf5a15f46161d Mon Sep 17 00:00:00 2001 From: Nathaniel Thomas Date: Sun, 6 Jul 2025 22:05:54 -0500 Subject: [PATCH 3/5] cleaned up GUI code, added source/sink, wip button icons --- oscps-gui/Cargo.toml | 4 +- oscps-gui/src/flowsheet.rs | 554 +++++++++++++++++++++++-------------- oscps-gui/src/main.rs | 446 +++++++++++++++++------------ 3 files changed, 622 insertions(+), 382 deletions(-) diff --git a/oscps-gui/Cargo.toml b/oscps-gui/Cargo.toml index 32cb601..ff6cb50 100644 --- a/oscps-gui/Cargo.toml +++ b/oscps-gui/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] env_logger = "0.11.6" -iced = {version = "0.13.1", features = ["canvas", "debug", "lazy"]} +iced = { version = "0.13.1", features = ["advanced", "canvas", "debug", "lazy"] } log = "0.4" oscps-lib = { path = "../oscps-lib" } +strum = "0.27.1" +strum_macros = "0.27.1" diff --git a/oscps-gui/src/flowsheet.rs b/oscps-gui/src/flowsheet.rs index cc83621..5cb4175 100644 --- a/oscps-gui/src/flowsheet.rs +++ b/oscps-gui/src/flowsheet.rs @@ -1,23 +1,23 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; +use iced::widget::canvas::path::Builder; +use iced::widget::canvas::{self, Cache, Canvas, Frame, Geometry, Path, Stroke}; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; -use oscps_lib::simulation::Simulation; use log::{debug, info}; +use strum_macros::Display; #[derive(Default)] pub struct State { cache: canvas::Cache, - pub placement_mode: BlockPlacement, - pub simulation: Simulation, + pub placement_mode: Component, } impl State { - pub fn view<'a>(&'a self, curves: &'a [Curve]) -> Element<'a, Curve> { + pub fn view<'a>(&'a self, components: &'a [Component]) -> Element<'a, Component> { Canvas::new(Flowsheet { state: self, - curves, + components, }) .width(Fill) .height(Fill) @@ -29,111 +29,362 @@ impl State { } } -// Allows detection of "block placement mode", as well as which block to place. -#[derive(Debug, Clone, Copy)] -pub enum BlockPlacement { - Connector, - Mixer, - Default, +#[derive(Display, Debug, Clone, Copy, PartialEq)] +pub enum Component { + Connector { + from: Option, + to: Option, + }, + Mixer { + at: Option, + input: Option, + output: Option, + }, + Source { + at: Option, + output: Option, + }, + Sink { + at: Option, + input: Option, + }, +} + +impl Component { + pub fn connector() -> Self { + Component::Connector { + from: None, + to: None, + } + } + + pub fn source() -> Self { + Component::Source { + at: None, + output: None, + } + } + pub fn sink() -> Self { + Component::Sink { + at: None, + input: None, + } + } + pub fn mixer() -> Self { + Component::Mixer { + at: None, + input: None, + output: None, + } + } + + // Determine if cursor is within 5 pixels of a given point. + fn is_in_bounds(&self, cursor_position: Point, input: Option) -> bool { + info!( + "Checking input bounds with cursor at ({}, {})", + cursor_position.x, cursor_position.y + ); + + match input { + None => false, // If the input is none, it is not in bounds. + Some(input) => { + // TODO: (minor) Fix arbitrary 5-pixel bounding box. Make dynamic/program setting. + let bound = 5.0; + if cursor_position.x > input.x - bound && cursor_position.x < input.x + bound { + debug!("Bound x match!"); + if cursor_position.y > input.y - bound && cursor_position.y < input.y + bound { + info!("Bounds match!"); + return true; + } + } + false + } + } + } + + // Determine if the cursor is in bounds of the input + fn on_input(&self, cursor_position: Point) -> bool { + let input = self.get_input(); + self.is_in_bounds(cursor_position, input) + } + + // Determine if the cursor is in bounds of the output + fn get_input(&self) -> Option { + return match self { + Component::Connector { from, .. } => *from, + Component::Mixer { input, .. } => *input, + Component::Sink { input, .. } => *input, + Component::Source { .. } => None, // Source does not have an input + }; + } + + fn on_output(&self, cursor_position: Point) -> bool { + let output = self.get_output(); + self.is_in_bounds(cursor_position, output) + } + + fn get_output(&self) -> Option { + return match self { + Component::Connector { to, .. } => *to, + Component::Mixer { output, .. } => *output, + Component::Source { output, .. } => *output, + Component::Sink { .. } => None, // Sink does not have an output + }; + } + + fn draw_all(components: &[Component], frame: &mut Frame, theme: &Theme) { + // TODO: (minor) Nitpicky, but this uses dynamic memory uncessesarily. + // Consider changing the function name fetching to a macro approach. + let function_name = std::any::type_name::() + .split("::") + .last() + .unwrap_or("unknown"); + let expect_string = format!( + "{} should should only be called with existing points.", + function_name + ); + let components = Path::new(|p| { + for component in components { + match component { + Component::Connector { from, to } => { + let from = from.expect(&expect_string); + let to = to.expect(&expect_string); + + Component::draw_connector(p, to, from) + } + Component::Mixer { at, input, output } => { + let at = at.expect(&expect_string); + let input = input.expect(&expect_string); + let output = output.expect(&expect_string); + + Component::draw_mixer(p, at, input, output) + } + Component::Source { at, output } => { + let at = at.expect(&expect_string); + let output = output.expect(&expect_string); + + Component::draw_source(p, at, output) + } + Component::Sink { at, input } => { + let at = at.expect(&expect_string); + let input = input.expect(&expect_string); + + Component::draw_sink(p, at, input) + } + } + } + }); + + frame.stroke( + &components, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); + } + + pub fn draw_connector(p: &mut Builder, to: Point, from: Point) { + debug!("Drawing connector"); + p.move_to(from); + let half_x_coord = from.x + (to.x - from.x) / 2.0; + p.line_to(Point::new(half_x_coord, from.y)); + p.line_to(Point::new(half_x_coord, to.y)); + p.line_to(Point::new(to.x, to.y)); + let mut arrow_offset_x = -10.0; + let arrow_offset_y = 5.0; + if to.x < from.x { + arrow_offset_x *= -1.0; + } + p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); + p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); + p.line_to(Point::new(to.x, to.y)); + } + + pub fn draw_mixer(p: &mut Builder, at: Point, input: Point, output: Point) { + debug!("Drawing mixer."); + + p.move_to(at); + let bottom_point = Point::new(at.x, at.y + 100.0); + let middle_point = Point::new(at.x + 100.0, at.y + 50.0); + p.line_to(bottom_point); + p.line_to(middle_point); + p.line_to(at); + + // Draw a circle for input connectors + p.move_to(at); + p.circle(input, 5.0); + // Another circle for output connectors + p.move_to(at); + p.circle(output, 5.0); + } + + pub fn draw_source(p: &mut Builder, at: Point, output: Point) { + debug!("Drawing source."); + + p.move_to(at); + p.rectangle(at, (50.0, 100.0).into()); + + // Circle for output + p.circle(output, 5.0); + } + + pub fn draw_sink(p: &mut Builder, at: Point, input: Point) { + debug!("Drawing sink."); + + p.move_to(at); + p.rectangle(at, (50.0, 100.0).into()); + + // Circle for input + p.move_to(at); + p.circle(input, 5.0); + } } -impl Default for BlockPlacement { +// Declare the default block to be the humble connector. +impl Default for Component { fn default() -> Self { - BlockPlacement::Default + Component::Connector { + from: None, + to: None, + } } } struct Flowsheet<'a> { state: &'a State, - curves: &'a [Curve], + components: &'a [Component], } -impl<'a> canvas::Program for Flowsheet<'a> { +impl<'a> Flowsheet<'a> { + fn place_sink(cursor_position: Point) -> Option { + let input = Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); + info!( + "Creating source at ({}, {}) with input at ({}, {}).", + cursor_position.x, cursor_position.y, input.x, input.y + ); + Some(Component::Sink { + at: Some(cursor_position), + input: Some(input), + }) + } + + fn place_source(cursor_position: Point) -> Option { + let output = Point::new(cursor_position.x + 55.0, cursor_position.y + 50.0); + info!( + "Creating source at ({}, {}) with output at ({}, {}).", + cursor_position.x, cursor_position.y, output.x, output.y + ); + Some(Component::Source { + at: Some(cursor_position), + output: Some(output), + }) + } + + // Helper function to place a mixer block + fn place_mixer(cursor_position: Point) -> Option { + let input = Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); + let output = Point::new(cursor_position.x + 105.0, cursor_position.y + 50.0); + info!( + "Creating mixer at ({}, {}) with input at ({}, {}) and output at ({}, {})", + cursor_position.x, cursor_position.y, input.x, input.y, output.x, output.y + ); + Some(Component::Mixer { + at: Some(cursor_position), + input: Some(input), + output: Some(output), + }) + } + + // Helper function to connect a connector to an input/output. + fn place_connector( + &self, + state: &mut Option, + cursor_position: Point, + ) -> Option { + let floating_connectors = false; // HACK: Disallow floating connectors + match state { + None => { + info!("Beginning creation of connector..."); + let mut result = Some(Pending::One { + from: cursor_position, + }); + + for component in self.components { + if !matches!(component, Component::Connector { .. }) + && component.on_output(cursor_position) + { + info!("Connected to input!"); + result = Some(Pending::One { + // NOTE: Should be safe. This must be Some(..) if + // on_output returned true. + from: component.get_output().unwrap(), + }); + *state = result; + return None; + } + } + if floating_connectors { + *state = result; + } + None + } + Some(Pending::One { from }) => { + info!("Created connector."); + let from = *from; + let mut result = Some(Component::Connector { + from: Some(from), + to: Some(cursor_position), + }); + for component in self.components { + if !matches!(component, Component::Connector { .. }) + && component.on_input(cursor_position) + { + info!("Connected to input!"); + result = Some(Component::Connector { + from: Some(from), + // NOTE: Should be safe, on_input() returned true. + to: Some(component.get_input().unwrap()), + }); + *state = None; + return result; + } + } + if floating_connectors { + *state = None; + result + } else { + None + } + } + } + } +} + +impl<'a> canvas::Program for Flowsheet<'a> { type State = Option; + fn update( &self, state: &mut Self::State, event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { let Some(cursor_position) = cursor.position_in(bounds) else { return (event::Status::Ignored, None); }; - // TODO: Do not allow a user to connect the input of a stream to the - // output of a block and vice-versa. match event { Event::Mouse(mouse_event) => { let message = match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { info!("Click detected at ({})", cursor_position); match self.state.placement_mode { - BlockPlacement::Connector => { - match *state { - None => { - info!("Beginning creation of connector..."); - let mut result = Some(Pending::One { - from: cursor_position, - }); - for curve in self.curves { - if !matches!(curve, Curve::Connector { .. }) - && curve.on_output_connector(cursor_position) - { - info!("Connected to input!"); - result = Some(Pending::One { - from: curve.get_output_point(), - }); - break; - } - } - *state = result; - None - } - Some(Pending::One { from }) => { - info!("Created connector."); - *state = None; - let mut result = Some(Curve::Connector { - from, - to: cursor_position, - }); - for curve in self.curves { - if !matches!(curve, Curve::Connector { .. }) - && curve.on_input_connector(cursor_position) - { - info!("Connected to input!"); - result = Some(Curve::Connector { - from, - to: curve.get_input_point(), - }); - break; - } - } - result - } // Some(Pending::Two { from, to }) => { - // *state = None; - // Some(Curve::Connector { - // from, - // to, - // }) - // } - } - } - BlockPlacement::Mixer => { - let input_point = - Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); - let output_point = - Point::new(cursor_position.x + 105.0, cursor_position.y + 50.0); - info!("Creating mixer at ({}, {}) with input at ({}, {}) and output at ({}, {})", cursor_position.x, cursor_position.y, input_point.x, input_point.y, output_point.x, output_point.y); - Some(Curve::Mixer { - at: cursor_position, - input_point, - output_point, - }) - } - BlockPlacement::Default => { - // TODO: Add code for selecting stuff - None + Component::Connector { .. } => { + Flowsheet::place_connector(&self, state, cursor_position) } + Component::Mixer { .. } => Flowsheet::place_mixer(cursor_position), + Component::Source { .. } => Flowsheet::place_source(cursor_position), + Component::Sink { .. } => Flowsheet::place_sink(cursor_position), } } // Right click should cancel placement. @@ -160,27 +411,38 @@ impl<'a> canvas::Program for Flowsheet<'a> { cursor: mouse::Cursor, ) -> Vec { let content = self.state.cache.draw(renderer, bounds.size(), |frame| { - Curve::draw_all(self.curves, frame, theme); + Component::draw_all(self.components, frame, theme); + // Border frame frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), Stroke::default() - .with_width(20.0) + .with_width(10.0) .with_color(theme.palette().text), ); }); if let Some(pending) = state { - vec![content, pending.draw(renderer, theme, bounds, cursor)] + vec![content, pending.draw(renderer, theme, bounds, cursor)] // Connector being drawn } else { - vec![content] + vec![content] // Just draw current content. } } + fn mouse_interaction( &self, _state: &Self::State, bounds: Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { + let Some(cursor_position) = cursor.position_in(bounds) else { + return mouse::Interaction::default(); + }; + if cursor.is_over(bounds) { + for component in self.components { + if component.on_input(cursor_position) || component.on_output(cursor_position) { + return mouse::Interaction::Grab; + } + } mouse::Interaction::Crosshair } else { mouse::Interaction::default() @@ -188,124 +450,9 @@ impl<'a> canvas::Program for Flowsheet<'a> { } } -#[derive(Debug, Clone, Copy)] -pub enum Curve { - Connector { - from: Point, - to: Point, - }, - Mixer { - at: Point, - input_point: Point, - output_point: Point, - }, -} - -impl Curve { - fn on_input_connector(&self, cursor_position: Point) -> bool { - // TODO: Fix arbitrary 5-pixel bounding box. Ideally use a circular bound. - let input = self.get_input_point(); - info!( - "Checking input bounds with cursor at ({}, {})", - cursor_position.x, cursor_position.y - ); - if cursor_position.x > input.x - 5.0 && cursor_position.x < input.x + 5.0 { - debug!("Bound x match!"); - if cursor_position.y > input.y - 5.0 && cursor_position.y < input.y + 5.0 { - info!("Bounds match!"); - return true; - } - } - false - } - - fn get_input_point(&self) -> Point { - return match self { - Curve::Connector { from, .. } => *from, - Curve::Mixer { input_point, .. } => *input_point, - }; - } - - fn on_output_connector(&self, cursor_position: Point) -> bool { - let output = self.get_output_point(); - info!( - "Checking output bounds with cursor at ({}, {})", - cursor_position.x, cursor_position.y - ); - if cursor_position.x > output.x - 5.0 && cursor_position.x < output.x + 5.0 { - debug!("Bound x match!"); - if cursor_position.y > output.y - 5.0 && cursor_position.y < output.y + 5.0 { - info!("Bounds match!"); - return true; - } - } - false - } - - fn get_output_point(&self) -> Point { - return match self { - Curve::Connector { to, .. } => *to, - Curve::Mixer { output_point, .. } => *output_point, - }; - } - fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { - let curves = Path::new(|p| { - for curve in curves { - match curve { - Curve::Connector { from, to } => { - debug!("Drawing connector"); - p.move_to(*from); - // p.quadratic_curve_to(*control, *to); - let half_x_coord = from.x + (to.x - from.x) / 2.0; - p.line_to(Point::new(half_x_coord, from.y)); - p.line_to(Point::new(half_x_coord, to.y)); - p.line_to(Point::new(to.x, to.y)); - let mut arrow_offset_x = -10.0; - let arrow_offset_y = 5.0; - if to.x < from.x { - arrow_offset_x *= -1.0; - } - p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); - p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); - p.line_to(Point::new(to.x, to.y)); - } - Curve::Mixer { - at, - input_point, - output_point, - } => { - debug!("Drawing mixer."); - p.move_to(*at); - // p.rectangle(*at, Size::new(200.0, 200.0)); - let bottom_point = Point::new(at.x, at.y + 100.0); - let middle_point = Point::new(at.x + 100.0, at.y + 50.0); - p.line_to(bottom_point); - p.line_to(middle_point); - p.line_to(*at); - // Draw a circle for input connectors - p.move_to(*at); - p.circle(*input_point, 5.0); - // Another circle for output connectors - p.move_to(*at); - p.circle(*output_point, 5.0); - } - } - } - }); - - frame.stroke( - &curves, - Stroke::default() - .with_width(2.0) - .with_color(theme.palette().text), - ); - } -} - #[derive(Debug, Clone, Copy)] enum Pending { One { from: Point }, - // Two { from: Point, to: Point }, } impl Pending { @@ -324,7 +471,6 @@ impl Pending { let to = cursor_position; let line = Path::new(|p| { p.move_to(from); - // p.quadratic_curve_to(*control, *to); let half_x_coord = from.x + (to.x - from.x) / 2.0; p.line_to(Point::new(half_x_coord, from.y)); p.line_to(Point::new(half_x_coord, to.y)); diff --git a/oscps-gui/src/main.rs b/oscps-gui/src/main.rs index 17add1d..aa22538 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -2,192 +2,209 @@ mod flowsheet; mod style; use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{button, column, container, horizontal_space, hover, responsive, row, text}; -use iced::{Center, Element, Fill, Size, Theme}; +use iced::widget::{button, column, container, horizontal_space, hover, responsive, text}; +use iced::{Center, Element, Fill, Length, Theme}; -use log::{info, debug}; +use crate::icon::State; +use log::{debug, info}; pub fn main() -> iced::Result { - // Start the GUI - env_logger::init(); + // Start the GUI env_logger::init(); info!("Starting application"); - iced::application("Open Source Chemical Process Simulator", MainWindow::update, MainWindow::view) - .theme(|_| Theme::CatppuccinMocha) - .antialiasing(true) - .centered() - .run() -} -struct MainWindow { - // theme: Theme, - panes: pane_grid::State, - focus: Option, - flowsheet: flowsheet::State, - curves: Vec, + let mut settings = iced::window::Settings::default(); + settings.size = (1920.0, 1080.0).into(); + settings.min_size = Some((480.0, 720.0).into()); + + let application = iced::application( + "Open Source Chemical Process Simulator", + MainWindow::update, + MainWindow::view, + ) + .window(settings) + .theme(|_| Theme::CatppuccinMocha) + .antialiasing(true) + .centered(); + + application.run() } #[derive(Debug, Clone, Copy)] enum Message { - AddCurve(flowsheet::Curve), + AddedComponent(flowsheet::Component), Clear, - PlaceComponent(flowsheet::BlockPlacement), + PlaceComponent(flowsheet::Component), Clicked(pane_grid::Pane), Dragged(pane_grid::DragEvent), Resized(pane_grid::ResizeEvent), } -impl MainWindow { +// These are the structures which make up the main window +struct MainWindow { + // theme: Theme, + panes: pane_grid::State, + focus: Option, + flowsheet: flowsheet::State, + components: Vec, + state: icon::State, + state_component: flowsheet::Component, +} +impl MainWindow { fn new() -> Self { - let (mut panes, pane) = pane_grid::State::new(Pane::new_canvas()); - panes.split(pane_grid::Axis::Horizontal, pane, Pane::new_selection()); + let (mut panes, pane) = pane_grid::State::new(Pane::new_selection()); + if let Some((_, split)) = panes.split(pane_grid::Axis::Vertical, pane, Pane::new_canvas()) { + panes.resize(split, 0.2); + } + + let state_component = flowsheet::Component::mixer(); + let state = State::new(state_component); MainWindow { // theme: Theme::default(), panes, focus: None, flowsheet: flowsheet::State::default(), - curves: Vec::default(), + components: Vec::default(), + state, + state_component, } } fn update(&mut self, message: Message) { match message { - Message::AddCurve(curve) => { - info!("Adding curve"); - self.curves.push(curve); + Message::AddedComponent(component) => { + info!("Added component"); + self.components.push(component); self.flowsheet.request_redraw(); } + // TODO: Make the clear option more deliberate (2 clicks at least) Message::Clear => { self.flowsheet = flowsheet::State::default(); - self.curves.clear(); + self.components.clear(); } // Default placement mode should be 'None' Message::PlaceComponent(component) => { - match component { // TODO: Modify to do more work other than a simple assignment. - flowsheet::BlockPlacement::Default => { - info!("Setting to default placement mode."); - self.flowsheet.placement_mode = flowsheet::BlockPlacement::default(); - }, - flowsheet::BlockPlacement::Connector => { + match component { + // TODO: Modify to do more work other than a simple assignment. + flowsheet::Component::Connector { .. } => { info!("Setting to connector placement mode."); - self.flowsheet.placement_mode = flowsheet::BlockPlacement::Connector; - }, - flowsheet::BlockPlacement::Mixer => { + self.flowsheet.placement_mode = flowsheet::Component::connector(); + } + flowsheet::Component::Mixer { .. } => { info!("Setting to mixer placement mode."); - self.flowsheet.placement_mode = flowsheet::BlockPlacement::Mixer; - }, + self.flowsheet.placement_mode = flowsheet::Component::mixer(); + } + flowsheet::Component::Source { .. } => { + info!("Setting to source placement mode."); + self.flowsheet.placement_mode = flowsheet::Component::source(); + } + flowsheet::Component::Sink { .. } => { + info!("Setting to sink placement mode."); + self.flowsheet.placement_mode = flowsheet::Component::sink(); + } } - }, + } Message::Clicked(pane) => { self.focus = Some(pane); info!("You clicked on a pane!") - }, - Message::Dragged(pane_grid::DragEvent::Dropped{ pane, target }) => { // pane, target + } + Message::Dragged(pane_grid::DragEvent::Dropped { pane, target }) => { self.panes.drop(pane, target); - println!("You dragged a pane!") - }, + info!("You dragged a pane!") + } Message::Dragged(_) => { - println!("You dragged, but did not drop a pane!") - }, - Message::Resized(pane_grid::ResizeEvent { split, ratio } ) => { + info!("You dragged, but did not drop a pane!") + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { self.panes.resize(split, ratio); - println!("You resized a pane!") - }, + info!("You resized a pane!") + } } } + // Create a button to add a certain component + fn placement_button<'a>( + &'a self, + target_mode: flowsheet::Component, + ) -> impl Into> { + let col = column![ + view_content(self.state.view(target_mode).map(Message::AddedComponent)), + text(target_mode.to_string()) + ]; + + container( + button(container(col)) + .style(match self.flowsheet.placement_mode { + mode if mode == target_mode => button::danger, + _ => button::secondary, + }) + .on_press(Message::PlaceComponent(target_mode)), + ) + } + fn view(&self) -> Element { let focus = self.focus; - let total_panes = self.panes.len(); - let pane_grid = PaneGrid::new(&self.panes, |id, pane, _is_maximized| { - match pane { - Pane::Canvas - // { id: _, is_pinned: _} - => { - debug!("Found canvas!"); - } - Pane::UnitSelection => { - debug!("Found Selection!"); - return row![ - container( - button("Place Connector") - .style( - match self.flowsheet.placement_mode { - flowsheet::BlockPlacement::Connector => button::danger, - _ => button::secondary, - } - ) - .on_press( - match self.flowsheet.placement_mode { - flowsheet::BlockPlacement::Connector => Message::PlaceComponent(flowsheet::BlockPlacement::Default), - _ => Message::PlaceComponent(flowsheet::BlockPlacement::Connector) - } - ) - ), - container( - button("Place Mixer") - .style( - match self.flowsheet.placement_mode { - flowsheet::BlockPlacement::Mixer => button::danger, - _ => button::secondary, - } - ) - .on_press( - match self.flowsheet.placement_mode { - flowsheet::BlockPlacement::Mixer => Message::PlaceComponent(flowsheet::BlockPlacement::Default), - _ => Message::PlaceComponent(flowsheet::BlockPlacement::Mixer) - } - ) - ), - ].into() + let is_focused = focus == Some(id); + match pane { + Pane::ComponentSelection => { + debug!("Found Selection!"); + return column![ + container(text("Component Selection")) + .padding(5) + .width(Length::Fill) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }), + self.placement_button(flowsheet::Component::source()).into(), + self.placement_button(flowsheet::Component::sink()).into(), + self.placement_button(flowsheet::Component::connector()) + .into(), + self.placement_button(flowsheet::Component::mixer()).into(), + ] + .width(Length::Fill) + .into(); + } + Pane::Canvas => { + debug!("Found canvas!"); + + let flowsheet_title_bar = pane_grid::TitleBar::new("Flowsheet") + .padding(10) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }); + + pane_grid::Content::new(responsive(move |_size| { + view_content(hover( + self.flowsheet + .view(&self.components) + .map(Message::AddedComponent), + if self.components.is_empty() { + container(horizontal_space()) + } else { + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear), + ) + .padding(10) + .align_top(Fill) + }, + )) + })) + .title_bar(flowsheet_title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + } } - } - let is_focused = focus == Some(id); - - let title = row![ - "Flowsheet", - ] - .spacing(5); - - let title_bar = pane_grid::TitleBar::new(title) - .padding(10) - .style(if is_focused { - style::title_bar_focused - } else { - style::title_bar_active - }); - - pane_grid::Content::new(responsive(move |size| { - view_content( - id, - total_panes, - false, - size, - hover( - self.flowsheet.view(&self.curves).map(Message::AddCurve), - if self.curves.is_empty() { - container(horizontal_space()) - } else { - container( - button("Clear") - .style(button::danger) - .on_press(Message::Clear), - ) - .padding(10) - .align_top(Fill) - }, - ), - - ) - })) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) }) .width(Fill) .height(Fill) @@ -196,13 +213,7 @@ impl MainWindow { .on_drag(Message::Dragged) .on_resize(10, Message::Resized); - container( - column![ - pane_grid, - ] - ) - .padding(20) - .into() + container(column![pane_grid,]).padding(20).into() } } @@ -212,49 +223,130 @@ impl Default for MainWindow { } } -#[derive(Clone,Copy,Default)] -enum Pane { - Canvas +mod icon { + + use crate::flowsheet; + use iced::mouse; + use iced::widget::canvas::{self, Canvas, Stroke}; + use iced::{Element, Rectangle}; + use iced::{Fill, Renderer, Theme}; + + pub struct State { + // TODO: Find a simpler way to do icons. + cache: canvas::Cache, + component: flowsheet::Component, + } + + impl State { + pub fn new(component: flowsheet::Component) -> Self { + State { + cache: canvas::Cache::default(), + component, + } + } + + pub fn view<'a>( + &'a self, + component: flowsheet::Component, + ) -> Element<'a, flowsheet::Component> { + Canvas::new(Icon { + state: self, + component, + }) + .width(Fill) + .height(Fill) + .height(Fill) + .into() + } + + pub fn request_redraw(&mut self) { + self.cache.clear(); + } + } + pub struct Icon<'a> { + state: &'a State, + component: flowsheet::Component, + } + + impl<'a> canvas::Program for Icon<'a> { + type State = Option; + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let content = self.state.cache.draw(renderer, bounds.size(), |frame| { + let mut builder = canvas::path::Builder::new(); + + match self.component { + flowsheet::Component::Source { .. } => flowsheet::Component::draw_source( + &mut builder, + (0.0, 0.0).into(), + (50.0, 50.0).into(), + ), + flowsheet::Component::Sink { .. } => flowsheet::Component::draw_sink( + &mut builder, + (0.0, 0.0).into(), + (0.0, 50.0).into(), + ), + flowsheet::Component::Mixer { .. } => flowsheet::Component::draw_mixer( + &mut builder, + (0.0, 0.0).into(), + (-50.0, 50.0).into(), + (50.0, 50.0).into(), + ), + flowsheet::Component::Connector { .. } => println!("Naw"), + } + + let path = builder.build(); + + frame.stroke( + &path, + Stroke::default() + .with_width(10.0) + .with_color(theme.palette().text), + ); + }); + + vec![content] + } + } + + // impl From for Element<'_, Message, Theme, Renderer> + // where + // Renderer: renderer::Renderer, // { - // id: usize, - // is_pinned: bool, + // fn from(icon: Icon) -> Self { + // Self::new(icon) + // } // } - , +} + +#[derive(Clone, Copy, Default)] +enum Pane { + Canvas, + #[default] - UnitSelection, + ComponentSelection, } impl Pane { - fn new_canvas( - // id: usize - ) -> Self { - Pane::Canvas - // { - // id, - // is_pinned: false, - // } - } - fn new_selection() -> Self { - Pane::UnitSelection + Pane::ComponentSelection + } + fn new_canvas() -> Self { + Pane::Canvas } - } -fn view_content<'a>( - _pane: pane_grid::Pane, - _total_panes: usize, - _is_pinned: bool, - size: Size, - flowsheet: Element<'a, Message>, -) -> Element<'a, Message> { - let content = - column![flowsheet, text!("{}x{}", size.width, size.height).size(24), ] // controls, - .spacing(10) - .align_x(Center); - - container(content) - .center_y(Fill) - .padding(5) - .into() +fn view_content<'a>(flowsheet: Element<'a, Message>) -> Element<'a, Message> { + let content = column![flowsheet] // controls, + .spacing(10) + .align_x(Center); + + container(content).center_y(Fill).padding(5).into() } From a12f0ff153735a98dfcf518afb3398662d548bcb Mon Sep 17 00:00:00 2001 From: Nathaniel Thomas Date: Sun, 6 Jul 2025 23:34:36 -0500 Subject: [PATCH 4/5] wait for better custom widget support --- oscps-gui/src/main.rs | 192 +++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 113 deletions(-) diff --git a/oscps-gui/src/main.rs b/oscps-gui/src/main.rs index aa22538..608f55f 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -3,9 +3,9 @@ mod style; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{button, column, container, horizontal_space, hover, responsive, text}; -use iced::{Center, Element, Fill, Length, Theme}; +use iced::{Center, Element, Fill, Length, Settings, Theme}; -use crate::icon::State; +use icon::Icon; use log::{debug, info}; pub fn main() -> iced::Result { @@ -29,6 +29,15 @@ pub fn main() -> iced::Result { application.run() } +// These are the structures which make up the main window +struct MainWindow { + // theme: Theme, + panes: pane_grid::State, + focus: Option, + flowsheet: flowsheet::State, + components: Vec, +} + #[derive(Debug, Clone, Copy)] enum Message { AddedComponent(flowsheet::Component), @@ -39,17 +48,6 @@ enum Message { Resized(pane_grid::ResizeEvent), } -// These are the structures which make up the main window -struct MainWindow { - // theme: Theme, - panes: pane_grid::State, - focus: Option, - flowsheet: flowsheet::State, - components: Vec, - state: icon::State, - state_component: flowsheet::Component, -} - impl MainWindow { fn new() -> Self { let (mut panes, pane) = pane_grid::State::new(Pane::new_selection()); @@ -57,25 +55,20 @@ impl MainWindow { panes.resize(split, 0.2); } - let state_component = flowsheet::Component::mixer(); - let state = State::new(state_component); - MainWindow { // theme: Theme::default(), panes, focus: None, flowsheet: flowsheet::State::default(), components: Vec::default(), - state, - state_component, } } fn update(&mut self, message: Message) { match message { - Message::AddedComponent(component) => { + Message::AddedComponent(curve) => { info!("Added component"); - self.components.push(component); + self.components.push(curve); self.flowsheet.request_redraw(); } // TODO: Make the clear option more deliberate (2 clicks at least) @@ -128,18 +121,16 @@ impl MainWindow { &'a self, target_mode: flowsheet::Component, ) -> impl Into> { - let col = column![ - view_content(self.state.view(target_mode).map(Message::AddedComponent)), - text(target_mode.to_string()) - ]; - container( - button(container(col)) - .style(match self.flowsheet.placement_mode { - mode if mode == target_mode => button::danger, - _ => button::secondary, - }) - .on_press(Message::PlaceComponent(target_mode)), + button(container(column![ + Icon::new(target_mode), + text(target_mode.to_string()) + ])) + .style(match self.flowsheet.placement_mode { + mode if mode == target_mode => button::danger, + _ => button::secondary, + }) + .on_press(Message::PlaceComponent(target_mode)), ) } @@ -224,106 +215,81 @@ impl Default for MainWindow { } mod icon { - use crate::flowsheet; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::border; use iced::mouse; - use iced::widget::canvas::{self, Canvas, Stroke}; - use iced::{Element, Rectangle}; - use iced::{Fill, Renderer, Theme}; + use iced::{Color, Element, Length, Rectangle, Size}; - pub struct State { - // TODO: Find a simpler way to do icons. - cache: canvas::Cache, + pub struct Icon { component: flowsheet::Component, } - impl State { + impl Icon { pub fn new(component: flowsheet::Component) -> Self { - State { - cache: canvas::Cache::default(), - component, - } + Self { component } } + } - pub fn view<'a>( - &'a self, - component: flowsheet::Component, - ) -> Element<'a, flowsheet::Component> { - Canvas::new(Icon { - state: self, - component, - }) - .width(Fill) - .height(Fill) - .height(Fill) - .into() - } + pub fn icon(component: flowsheet::Component) -> Icon { + Icon::new(component) + } - pub fn request_redraw(&mut self) { - self.cache.clear(); + impl Widget for Icon + where + Renderer: renderer::Renderer, + { + fn size(&self) -> Size { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } - } - pub struct Icon<'a> { - state: &'a State, - component: flowsheet::Component, - } - impl<'a> canvas::Program for Icon<'a> { - type State = Option; + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let hard_size = 100.0; // HACK: Temporary, figure out a more elegant solution later. + layout::Node::new(Size::new(hard_size, hard_size)) + } fn draw( &self, - _state: &Self::State, - renderer: &Renderer, - theme: &Theme, - bounds: Rectangle, + state: &widget::Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, _cursor: mouse::Cursor, - ) -> Vec { - let content = self.state.cache.draw(renderer, bounds.size(), |frame| { - let mut builder = canvas::path::Builder::new(); - - match self.component { - flowsheet::Component::Source { .. } => flowsheet::Component::draw_source( - &mut builder, - (0.0, 0.0).into(), - (50.0, 50.0).into(), - ), - flowsheet::Component::Sink { .. } => flowsheet::Component::draw_sink( - &mut builder, - (0.0, 0.0).into(), - (0.0, 50.0).into(), - ), - flowsheet::Component::Mixer { .. } => flowsheet::Component::draw_mixer( - &mut builder, - (0.0, 0.0).into(), - (-50.0, 50.0).into(), - (50.0, 50.0).into(), - ), - flowsheet::Component::Connector { .. } => println!("Naw"), - } - - let path = builder.build(); - - frame.stroke( - &path, - Stroke::default() - .with_width(10.0) - .with_color(theme.palette().text), - ); - }); - - vec![content] + _viewport: &Rectangle, + ) { + let hard_size = 50.0; // HACK: Again, temporary + + // TODO: Placeholder for when custom widgets have better support. + + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border: border::rounded(hard_size), + ..renderer::Quad::default() + }, + Color::BLACK, + ); + } + } + impl From for Element<'_, Message, Theme, Renderer> + where + Renderer: renderer::Renderer, + { + fn from(icon: Icon) -> Self { + Self::new(icon) } } - - // impl From for Element<'_, Message, Theme, Renderer> - // where - // Renderer: renderer::Renderer, - // { - // fn from(icon: Icon) -> Self { - // Self::new(icon) - // } - // } } #[derive(Clone, Copy, Default)] From ed999f515c5162c0695e00d224b9d5496a9cbdd7 Mon Sep 17 00:00:00 2001 From: Nathaniel Thomas Date: Sat, 12 Jul 2025 17:32:20 -0500 Subject: [PATCH 5/5] Mixer partially integrated --- oscps-gui/src/flowsheet.rs | 102 ++++++++++++++----- oscps-gui/src/main.rs | 38 +++++-- oscps-lib/src/blocks.rs | 21 ++-- oscps-lib/src/simulation.rs | 107 ++++++++++++++++---- oscps-lib/src/stream.rs | 28 ++--- oscps-lib/src/thermodynamics/srk_package.rs | 32 +++--- 6 files changed, 226 insertions(+), 102 deletions(-) diff --git a/oscps-gui/src/flowsheet.rs b/oscps-gui/src/flowsheet.rs index 5cb4175..e214cbf 100644 --- a/oscps-gui/src/flowsheet.rs +++ b/oscps-gui/src/flowsheet.rs @@ -1,10 +1,13 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::path::Builder; -use iced::widget::canvas::{self, Cache, Canvas, Frame, Geometry, Path, Stroke}; +use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; +use oscps_lib::simulation::Simulation; -use log::{debug, info}; +use std::time::{Duration, SystemTime}; + +use log::{debug, info, warn}; use strum_macros::Display; #[derive(Default)] @@ -14,10 +17,16 @@ pub struct State { } impl State { - pub fn view<'a>(&'a self, components: &'a [Component]) -> Element<'a, Component> { + pub fn view<'a>( + &'a self, + components: &'a [Component], + simulation: &'a Simulation, + ) -> Element<'a, Component> { Canvas::new(Flowsheet { state: self, components, + left_click_time: SystemTime::now(), + simulation, }) .width(Fill) .height(Fill) @@ -79,33 +88,31 @@ impl Component { } // Determine if cursor is within 5 pixels of a given point. - fn is_in_bounds(&self, cursor_position: Point, input: Option) -> bool { + fn is_in_bounds(&self, cursor_position: Point, input: Point) -> bool { info!( "Checking input bounds with cursor at ({}, {})", cursor_position.x, cursor_position.y ); - match input { - None => false, // If the input is none, it is not in bounds. - Some(input) => { - // TODO: (minor) Fix arbitrary 5-pixel bounding box. Make dynamic/program setting. - let bound = 5.0; - if cursor_position.x > input.x - bound && cursor_position.x < input.x + bound { - debug!("Bound x match!"); - if cursor_position.y > input.y - bound && cursor_position.y < input.y + bound { - info!("Bounds match!"); - return true; - } - } - false + // TODO: (minor) Fix arbitrary 5-pixel bounding box. Make dynamic/program setting. + let bound = 5.0; + if cursor_position.x > input.x - bound && cursor_position.x < input.x + bound { + debug!("Bound x match!"); + if cursor_position.y > input.y - bound && cursor_position.y < input.y + bound { + info!("Bounds match!"); + return true; } } + false } // Determine if the cursor is in bounds of the input fn on_input(&self, cursor_position: Point) -> bool { let input = self.get_input(); - self.is_in_bounds(cursor_position, input) + match input { + Some(point) => self.is_in_bounds(cursor_position, point), + None => false, + } } // Determine if the cursor is in bounds of the output @@ -120,7 +127,10 @@ impl Component { fn on_output(&self, cursor_position: Point) -> bool { let output = self.get_output(); - self.is_in_bounds(cursor_position, output) + match output { + Some(point) => self.is_in_bounds(cursor_position, point), + None => false, + } } fn get_output(&self) -> Option { @@ -253,6 +263,8 @@ impl Default for Component { struct Flowsheet<'a> { state: &'a State, components: &'a [Component], + left_click_time: SystemTime, + simulation: &'a Simulation, } impl<'a> Flowsheet<'a> { @@ -378,11 +390,27 @@ impl<'a> canvas::Program for Flowsheet<'a> { let message = match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { info!("Click detected at ({})", cursor_position); + let current_time = SystemTime::now(); + + match current_time.duration_since(self.left_click_time) { + Ok(elapsed) => { + if elapsed < Duration::from_millis(200) { + println!("Double click!") + } + } + Err(e) => { + warn!("Error {} when detecting double click.", e) + } + } + match self.state.placement_mode { Component::Connector { .. } => { Flowsheet::place_connector(&self, state, cursor_position) } - Component::Mixer { .. } => Flowsheet::place_mixer(cursor_position), + Component::Mixer { .. } => { + self.simulation; + Flowsheet::place_mixer(cursor_position) + } Component::Source { .. } => Flowsheet::place_source(cursor_position), Component::Sink { .. } => Flowsheet::place_sink(cursor_position), } @@ -395,6 +423,7 @@ impl<'a> canvas::Program for Flowsheet<'a> { } _ => None, }; + (event::Status::Captured, message) } Event::Keyboard(_) => (event::Status::Captured, None), @@ -429,7 +458,7 @@ impl<'a> canvas::Program for Flowsheet<'a> { fn mouse_interaction( &self, - _state: &Self::State, + state: &Self::State, bounds: Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { @@ -437,13 +466,36 @@ impl<'a> canvas::Program for Flowsheet<'a> { return mouse::Interaction::default(); }; + // Only display a grab icon if placing a connector, and + // the connector is hovering over an input when in input mode, or over + // an output when in output mode. if cursor.is_over(bounds) { - for component in self.components { - if component.on_input(cursor_position) || component.on_output(cursor_position) { - return mouse::Interaction::Grab; + match self.state.placement_mode { + Component::Connector { .. } => { + for component in self.components { + match component { + Component::Connector { .. } => (), + _ => match state { + Some(Pending::One { .. }) => { + if component.on_input(cursor_position) { + println!("Some"); + return mouse::Interaction::Grab; + } + } + None => { + if component.on_output(cursor_position) { + println!("Component: {}", component); + println!("None"); + return mouse::Interaction::Grab; + } + } + }, + } + } + mouse::Interaction::Crosshair } + _ => mouse::Interaction::Crosshair, } - mouse::Interaction::Crosshair } else { mouse::Interaction::default() } diff --git a/oscps-gui/src/main.rs b/oscps-gui/src/main.rs index 608f55f..19d8a6c 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -3,11 +3,14 @@ mod style; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{button, column, container, horizontal_space, hover, responsive, text}; -use iced::{Center, Element, Fill, Length, Settings, Theme}; +use iced::{Center, Element, Fill, Length, Theme}; + +use oscps_lib::simulation::{self, Settings, Simulation}; use icon::Icon; use log::{debug, info}; + pub fn main() -> iced::Result { // Start the GUI env_logger::init(); info!("Starting application"); @@ -30,12 +33,14 @@ pub fn main() -> iced::Result { } // These are the structures which make up the main window +#[allow(dead_code)] struct MainWindow { // theme: Theme, panes: pane_grid::State, focus: Option, flowsheet: flowsheet::State, components: Vec, + simulation: Simulation, } #[derive(Debug, Clone, Copy)] @@ -55,21 +60,35 @@ impl MainWindow { panes.resize(split, 0.2); } + let settings = Settings::default(); + MainWindow { // theme: Theme::default(), panes, focus: None, flowsheet: flowsheet::State::default(), components: Vec::default(), + simulation: Simulation::new(settings), } } fn update(&mut self, message: Message) { match message { - Message::AddedComponent(curve) => { + Message::AddedComponent(component) => { info!("Added component"); - self.components.push(curve); + self.components.push(component); self.flowsheet.request_redraw(); + match component { + flowsheet::Component::Source{ ..} => todo!(), + flowsheet::Component::Sink{ ..} => todo!(), + flowsheet::Component::Mixer{ ..} => { + self.simulation.add_block(simulation::BlockType::Mixer); + }, + flowsheet::Component::Connector{ .. } => { + // self.simulation.add_stream(simulation::BlockType::Mixer); + todo!(); + }, + } } // TODO: Make the clear option more deliberate (2 clicks at least) Message::Clear => { @@ -173,7 +192,7 @@ impl MainWindow { pane_grid::Content::new(responsive(move |_size| { view_content(hover( self.flowsheet - .view(&self.components) + .view(&self.components, &self.simulation) .map(Message::AddedComponent), if self.components.is_empty() { container(horizontal_space()) @@ -224,15 +243,18 @@ mod icon { use iced::{Color, Element, Length, Rectangle, Size}; pub struct Icon { - component: flowsheet::Component, + // component: flowsheet::Component, } impl Icon { - pub fn new(component: flowsheet::Component) -> Self { - Self { component } + pub fn new(_component: flowsheet::Component) -> Self { + Self { + // component + } } } + #[allow(dead_code)] pub fn icon(component: flowsheet::Component) -> Icon { Icon::new(component) } @@ -260,7 +282,7 @@ mod icon { fn draw( &self, - state: &widget::Tree, + _state: &widget::Tree, renderer: &mut Renderer, _theme: &Theme, _style: &renderer::Style, diff --git a/oscps-lib/src/blocks.rs b/oscps-lib/src/blocks.rs index b40c738..4206fcd 100644 --- a/oscps-lib/src/blocks.rs +++ b/oscps-lib/src/blocks.rs @@ -13,6 +13,8 @@ use uom::si::f64::Energy; use uom::si::f64::Mass; use uom::si::mass::kilogram; +use crate::simulation::StreamReference; + /// # Block /// /// A trait that all blocks must implement. @@ -42,6 +44,7 @@ pub trait Block { /// /// A Separator block that allows components of a stream to be separated. /// Allows for a single input and an arbitrary number of outputs. +#[allow(dead_code)] struct Separator { id: u64, input: Option, // An option is used in case there is no input stream @@ -49,6 +52,7 @@ struct Separator { // TODO: Add additional fields that controls how components are separated } +#[allow(dead_code)] impl Separator { fn new(id: u64) -> Self { Separator { @@ -65,30 +69,25 @@ impl Separator { /// A block used for simple stream mixing operations. Spacial information /// is not stored in the case that non-gui applications use this backend. pub struct Mixer { - /// The ID of the block. - pub id: u64, /// Set of inlet streams for the mixer - pub inputs: Vec>, - /// outlet stream for the mixer block - pub output: Option>, + pub inputs: Option>, + /// Outlet stream for the mixer block + pub output: Option, } #[allow(dead_code)] /// Implementations of the mixer block. impl Mixer { /// Create a new mixer block. TODO: Figure out importance of lifetimes - pub fn new<'a>(id: u64) -> Mixer { + pub fn new() -> Mixer { Mixer { - id, - inputs: Vec::new(), + inputs: None, output: None, } } // TODO: Uncomment once desired base functionality is achieved - // /// Execute the mixer block (calculate balances, output streams, etc) - // /// This function still needs to be implemented - // pub fn execute_block(&mut self) { + // /// Execute the mixer block (calculate balances, output streams, etc) /// This function still needs to be implemented pub fn execute_block(&mut self) { // self.outlet_stream = Some(connector::Stream { // s_id: String::from("Mass_Outlet"), // thermo: None, diff --git a/oscps-lib/src/simulation.rs b/oscps-lib/src/simulation.rs index f9c3868..f10c491 100644 --- a/oscps-lib/src/simulation.rs +++ b/oscps-lib/src/simulation.rs @@ -2,10 +2,25 @@ //! //! Allows for the construction of a simulation object. TODO: Implement this. -use crate::blocks::Block; +use crate::blocks::{Block, Mixer}; use crate::stream::Stream; -use std::collections::HashMap; +// use std::collections::HashMap; +use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; +/// An Arc, RwLock, Box reference for threadsafe Block interactions. +pub type BlockReference = Arc>>; +/// An Arc, RwLock, Box reference for threadsafe Stream interactions. +pub type StreamReference = Arc>>; + +/// Used to tell functions what type of block to add. +pub enum BlockType { + /// Mix multiple streams into a single output stream. + Mixer, + /// Source for streams. + Source, + /// Sink for streams. + Sink, +} // fn compute_outlet_phase_fractions(&self) { @@ -31,6 +46,13 @@ pub struct SimulationState { // Add fields as needed } +impl SimulationState { + /// Create a new SimulationState. + pub fn new() -> Self { + return SimulationState {}; + } +} + /// An enum used to represent errors. #[derive(Debug)] pub enum Err { @@ -48,11 +70,12 @@ pub enum Err { /// The Simulation struct stores information pertaining to blocks and streams #[derive(Default)] +#[allow(dead_code)] pub struct Simulation { /// Stores all the blocks in the simulation - blocks: HashMap>>>, + blocks: BTreeMap, /// Stores all the streams in the simulation - streams: HashMap>>>, + streams: BTreeMap, /// Stores simulation settings settings: Settings, /// Stores the state of the simlation @@ -60,30 +83,70 @@ pub struct Simulation { } impl Simulation { - /// Create a new simultion - pub fn new(settings: Settings, state: SimulationState) -> Self { + /// Create a new simulation + pub fn new(settings: Settings) -> Self { Self { - blocks: HashMap::new(), - streams: HashMap::new(), + blocks: BTreeMap::new(), + streams: BTreeMap::new(), settings, - state, + state: SimulationState::new(), + } + } + + /// Adds a block to the simulation and returns the ID of + /// the block. + #[allow(dead_code)] + pub fn add_block(&mut self, block: BlockType) -> u64 { + // Start with a block id of 1. + let mut id = 1; + while self.blocks.contains_key(&id) { + id += 1; + } + + match block { + BlockType::Mixer => { + self.blocks + .insert(id, Arc::new(RwLock::new(Box::new(Mixer::new())))); + } + BlockType::Source => { + todo!() + } + BlockType::Sink => { + todo!() + } } + + return id; } - /// Add a block to the simulation. - pub fn add_block( - &mut self, - block_id: i32, - block: Arc>>, - ) -> Result<(), Err> { - if self.blocks.contains_key(&block_id) { - return Err(Err::BlockExists); + /// Adds a stream to the simulation and returns the ID of + /// the stream. + #[allow(dead_code)] + pub fn add_stream(&mut self, from: BlockReference, to: BlockReference) -> u64 { + // Start with a stream ID of 1. + let mut id = 1; + while self.streams.contains_key(&id) { + id += 1; } - self.blocks.insert(block_id, block); - Ok(()) + self.streams + .insert(id, Arc::new(RwLock::new(Box::new(Stream::new(from, to))))); + return id; } - // pub fn add_connector(&mut self, connector_id: i32, connector: Connector) -> Result<(), Err> { + // /// Add a block to the simulation. + // fn add_block( + // &mut self, + // block_id: u64, + // block: Arc>>, + // ) -> Result<(), Err> { + // if self.blocks.contains_key(&block_id) { + // return Err(Err::BlockExists); + // } + // self.blocks.insert(block_id, block); + // Ok(()) + // } + + // pub fn add_connector(&mut self, connector_id: u64, connector: Connector) -> Result<(), Err> { // if self.connectors.contains_key(&connector_id) { // return Err(Err::ConnectorExists); // } @@ -91,14 +154,14 @@ impl Simulation { // Ok(()) // } - // pub fn remove_block(&mut self, block_id: i32) -> Result<(), Err> { + // pub fn remove_block(&mut self, block_id: u64) -> Result<(), Err> { // if self.blocks.remove(&block_id).is_none() { // return Err(Err::BlockNotFound); // } // Ok(()) // } - // pub fn remove_connector(&mut self, connector_id: i32) -> Result<(), Err> { + // pub fn remove_connector(&mut self, connector_id: u64) -> Result<(), Err> { // if self.connectors.remove(&connector_id).is_none() { // return Err(Err::ConnectorNotFound); // } diff --git a/oscps-lib/src/stream.rs b/oscps-lib/src/stream.rs index 30c57cf..e5ceaf5 100644 --- a/oscps-lib/src/stream.rs +++ b/oscps-lib/src/stream.rs @@ -1,36 +1,24 @@ //! # Stream -//! -use crate::thermodynamics::ThermoState; +// NOTE: Temporarily disabled until the thermodynamics crate is thread-safe. +// use crate::thermodynamics::ThermoState; +use crate::simulation::BlockReference; /// # Stream /// /// Struct to hold stream information pub struct Stream { - // TODO: IDs must be unique within the flowsheet. Consider using integers - // as IDs and having a separate field for the name of a connector. Adopt - // a similar scheme for blocks. - /// ID of the stream. - pub s_id: String, /// Instance of ThermoState struct that holds thermodynamic information. - pub thermo: Option, - // TODO: Change these from strings to integers, or better yet, - // references to the source and destination blocks, to minimize - // computation time spent on looking for sources and destinations. + // pub thermo: Option, // HACK: Temporarily disable to enable thread-safety. /// ID of source block - pub from_block: String, + pub from: BlockReference, /// ID of destination block - pub to_block: String, + pub to: BlockReference, } impl Stream { /// Constructor for 'Stream' struct - pub fn new(id: String, from_blk_id: String, to_blk_id: String) -> Stream { - Stream { - s_id: id, - thermo: None, - from_block: from_blk_id, - to_block: to_blk_id, - } + pub fn new(from: BlockReference, to: BlockReference) -> Stream { + Stream { from, to } } } diff --git a/oscps-lib/src/thermodynamics/srk_package.rs b/oscps-lib/src/thermodynamics/srk_package.rs index 94a196a..312a815 100644 --- a/oscps-lib/src/thermodynamics/srk_package.rs +++ b/oscps-lib/src/thermodynamics/srk_package.rs @@ -1,17 +1,17 @@ +// // NOTE: Commented out to prevent warnings. -///#SRKPackage -/// -///Will contain equations relating to the SRK Equation of state - - -use crate::thermodynamics::*; -use std::sync::Arc; -use uom::si::f64::*; -use uom::si::molar_energy; -use uom::si::molar_heat_capacity; -use uom::si::pressure; -use uom::si::thermodynamic_temperature; -use uom::si::energy; -use uom::si::amount_of_substance; -use uom::si::volume; -use uom::si::ratio; +// ///#SRKPackage +// /// +// ///Will contain equations relating to the SRK Equation of state +// use crate::thermodynamics::*; +// use std::sync::Arc; +// use std::sync::{Arc, RwLock}; +// use uom::si::amount_of_substance; +// use uom::si::energy; +// use uom::si::f64::*; +// use uom::si::molar_energy; +// use uom::si::molar_heat_capacity; +// use uom::si::pressure; +// use uom::si::ratio; +// use uom::si::thermodynamic_temperature; +// use uom::si::volume;