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..ff6cb50 100644 --- a/oscps-gui/Cargo.toml +++ b/oscps-gui/Cargo.toml @@ -6,5 +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 6a18a06..e214cbf 100644 --- a/oscps-gui/src/flowsheet.rs +++ b/oscps-gui/src/flowsheet.rs @@ -1,21 +1,32 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; +use iced::widget::canvas::path::Builder; 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 std::time::{Duration, SystemTime}; + +use log::{debug, info, warn}; +use strum_macros::Display; #[derive(Default)] pub struct State { cache: canvas::Cache, - pub placement_mode: BlockPlacement, + 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], + simulation: &'a Simulation, + ) -> Element<'a, Component> { Canvas::new(Flowsheet { state: self, - curves, + components, + left_click_time: SystemTime::now(), + simulation, }) .width(Fill) .height(Fill) @@ -27,34 +38,350 @@ 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: Point) -> bool { + info!( + "Checking input bounds with cursor at ({}, {})", + cursor_position.x, cursor_position.y + ); + + // 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(); + match input { + Some(point) => self.is_in_bounds(cursor_position, point), + None => false, + } + } + + // 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(); + match output { + Some(point) => self.is_in_bounds(cursor_position, point), + None => false, + } + } + + 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], + left_click_time: SystemTime, + simulation: &'a Simulation, +} + +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> { +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); }; @@ -63,84 +390,43 @@ impl<'a> canvas::Program for Flowsheet<'a> { 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, - // }) - // } + 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!") } - }, - 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 + } + 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 { .. } => { + self.simulation; + Flowsheet::place_mixer(cursor_position) + } + Component::Source { .. } => Flowsheet::place_source(cursor_position), + Component::Sink { .. } => Flowsheet::place_sink(cursor_position), + } + } // 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,158 +439,72 @@ 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() - .with_width(20.0) + let content = self.state.cache.draw(renderer, bounds.size(), |frame| { + Component::draw_all(self.components, frame, theme); + // Border frame + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default() + .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, + state: &Self::State, bounds: Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { - if cursor.is_over(bounds) { - mouse::Interaction::Crosshair - } else { - mouse::Interaction::default() - } - } -} - -#[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 - } + let Some(cursor_position) = cursor.position_in(bounds) else { + return mouse::Interaction::default(); + }; - 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); + // 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) { + 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, } - }); - - frame.stroke( - &curves, - Stroke::default() - .with_width(2.0) - .with_color(theme.palette().text), - ); + } else { + mouse::Interaction::default() + } } } #[derive(Debug, Clone, Copy)] enum Pending { One { from: Point }, - // Two { from: Point, to: Point }, } impl Pending { @@ -323,17 +523,16 @@ 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; + 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 +540,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..19d8a6c 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -2,191 +2,219 @@ 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 oscps_lib::simulation::{self, Settings, Simulation}; + +use icon::Icon; + +use log::{debug, info}; pub fn main() -> iced::Result { - 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() + + 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() } +// 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, - curves: Vec, + components: Vec, + simulation: Simulation, } #[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 { - 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 settings = Settings::default(); MainWindow { // theme: Theme::default(), panes, focus: None, flowsheet: flowsheet::State::default(), - curves: Vec::default(), + components: Vec::default(), + simulation: Simulation::new(settings), } } 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(); + 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 => { 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> { + container( + 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)), + ) + } + 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, &self.simulation) + .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) @@ -195,13 +223,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() } } @@ -211,49 +233,108 @@ impl Default for MainWindow { } } -#[derive(Clone,Copy,Default)] +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::{Color, Element, Length, Rectangle, Size}; + + pub struct Icon { + // component: flowsheet::Component, + } + + impl Icon { + pub fn new(_component: flowsheet::Component) -> Self { + Self { + // component + } + } + } + + #[allow(dead_code)] + pub fn icon(component: flowsheet::Component) -> Icon { + Icon::new(component) + } + + impl Widget for Icon + where + Renderer: renderer::Renderer, + { + fn size(&self) -> Size { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + + 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: &widget::Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _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) + } + } +} + +#[derive(Clone, Copy, Default)] enum Pane { - Canvas - // { - // id: usize, - // is_pinned: bool, - // } - , + 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() } diff --git a/oscps-lib/src/blocks.rs b/oscps-lib/src/blocks.rs index ff99f2c..4206fcd 100644 --- a/oscps-lib/src/blocks.rs +++ b/oscps-lib/src/blocks.rs @@ -6,13 +6,15 @@ //! 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; 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/connector.rs b/oscps-lib/src/connector.rs deleted file mode 100644 index fa67621..0000000 --- a/oscps-lib/src/connector.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! # Connector -//! - -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, - /// 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. - /// ID of source block - pub from_block : String, - /// ID of destination block - pub to_block : String -} - - -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 - } - } -} 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..f10c491 100644 --- a/oscps-lib/src/simulation.rs +++ b/oscps-lib/src/simulation.rs @@ -2,7 +2,25 @@ //! //! Allows for the construction of a simulation object. TODO: Implement this. +use crate::blocks::{Block, Mixer}; +use crate::stream::Stream; // 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) { @@ -14,69 +32,139 @@ // fn compute_outlet_pressure(&self) { -// } -// #[derive(Debug, Clone)] -// struct Settings { -// // Add fields as needed // } -// #[derive(Debug, Clone)] -// struct SimulationState { -// // Add fields as needed -// } +/// A struct for storing settings of the simulation +#[derive(Debug, Clone, Default)] +pub struct Settings { + // Add fields as needed +} -// #[derive(Debug)] -// enum Err { -// BlockNotFound, -// ConnectorNotFound, -// BlockExists, -// ConnectorExists, -// Other(String), -// } +/// A struct for storing the current state of the simulation +#[derive(Debug, Clone, Default)] +pub struct SimulationState { + // Add fields as needed +} -// struct Simulation { -// blocks: HashMap, -// connectors: HashMap, -// settings: Settings, -// state: SimulationState, -// } +impl SimulationState { + /// Create a new SimulationState. + pub fn new() -> Self { + return SimulationState {}; + } +} + +/// 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)] +#[allow(dead_code)] +pub struct Simulation { + /// Stores all the blocks in the simulation + blocks: BTreeMap, + /// Stores all the streams in the simulation + streams: BTreeMap, + /// Stores simulation settings + settings: Settings, + /// Stores the state of the simlation + state: SimulationState, +} + +impl Simulation { + /// Create a new simulation + pub fn new(settings: Settings) -> Self { + Self { + blocks: BTreeMap::new(), + streams: BTreeMap::new(), + settings, + 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; + } + + /// 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.streams + .insert(id, Arc::new(RwLock::new(Box::new(Stream::new(from, to))))); + return id; + } + + // /// 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); + // } + // self.connectors.insert(connector_id, connector); + // Ok(()) + // } + + // pub fn remove_block(&mut self, block_id: u64) -> 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: u64) -> Result<(), Err> { + // if self.connectors.remove(&connector_id).is_none() { + // return Err(Err::ConnectorNotFound); + // } + // Ok(()) + // } +} diff --git a/oscps-lib/src/stream.rs b/oscps-lib/src/stream.rs new file mode 100644 index 0000000..e5ceaf5 --- /dev/null +++ b/oscps-lib/src/stream.rs @@ -0,0 +1,24 @@ +//! # Stream + +// 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 { + /// Instance of ThermoState struct that holds thermodynamic information. + // pub thermo: Option, // HACK: Temporarily disable to enable thread-safety. + /// ID of source block + pub from: BlockReference, + /// ID of destination block + pub to: BlockReference, +} + +impl Stream { + /// Constructor for 'Stream' struct + 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; 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!