From 934570cf27696d680bcac7fa455da7e32f07d243 Mon Sep 17 00:00:00 2001 From: Krish Joshi Date: Fri, 29 May 2026 18:32:31 +0530 Subject: [PATCH 1/2] Added option to delete components by selecting it and clicking backspace or delete on keyboard --- src/main/Simulator/Simulator/Flowsheet.mo | 39 ++++++++------ src/main/Simulator/Simulator/simulateEQN.mos | 2 +- src/main/python/mainApp.py | 2 +- src/main/python/utils/Container.py | 51 +++++++++++++++++-- src/main/python/utils/Graphics.py | 18 +++++-- src/main/python/utils/UnitOperations.py | 8 +++ undo_redo/Undo.pkl | Bin 108 -> 1567 bytes 7 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/main/Simulator/Simulator/Flowsheet.mo b/src/main/Simulator/Simulator/Flowsheet.mo index 2a7478a..2eb8349 100644 --- a/src/main/Simulator/Simulator/Flowsheet.mo +++ b/src/main/Simulator/Simulator/Flowsheet.mo @@ -1,30 +1,37 @@ -package Splitter1 +package Mixer1 model ms extends Simulator.Streams.MaterialStream; extends Simulator.Files.ThermodynamicPackages.RaoultsLaw; end ms; - model Splitter1Simulation + model Mixer1Simulation import data = Simulator.Files.ChemsepDatabase; - parameter data.Bromine Bromine; - parameter data.Carbontetrachloride Carbontetrachloride; - parameter Integer Nc = 2; - parameter data.GeneralProperties C[Nc] = {Bromine, Carbontetrachloride}; - ms MaterialStream1(Nc = 2, C = {Bromine, Carbontetrachloride}); - Simulator.UnitOperations.Splitter Splitter1(Nc = 2,C = {Bromine, Carbontetrachloride}, No = 2, CalcType = "Split_Ratio", SpecVal_s = {0.5, 0.5}); + parameter Integer Nc = 0; + parameter data.GeneralProperties C[Nc] = {}; + Simulator.UnitOperations.Mixer Mixer1(Nc = 2,C = {Water, Ethanol}, NI = 2, outPress = "Inlet_Average"); - Simulator.UnitOperations.Cooler Cooler1(Nc = 2,C = {Bromine, Carbontetrachloride}, Pdel = 0, Eff = 1); + ms MaterialStream1(Nc = 0, C = {}); + ms MaterialStream2(Nc = 0, C = {}); + ms MaterialStream3(Nc = 0, C = {}); + Simulator.UnitOperations.Heater Heater1(Nc = 2,C = {Water, Ethanol}, Pdel = 0, Eff = 1); equation +connect(MaterialStream1.Out, Mixer1.In[1]); +connect(MaterialStream2.Out, Mixer1.In[2]); +connect(Mixer1.Out, MaterialStream3.In); MaterialStream1.P = 101325; MaterialStream1.T = 300; MaterialStream1.F_p[1] = 100; -connect(Splitter1.In, MaterialStream1.Out); -connect(Splitter1.Out, Cooler1.In); -connect(Cooler1.In, Splitter1.Out); -connect(Cooler1.Out, MaterialStream1.In); -Cooler1.Q = None; - end Splitter1Simulation; -end Splitter1; +MaterialStream2.P = 101325; +MaterialStream2.T = 300; +MaterialStream2.F_p[1] = 100; +MaterialStream3.P = 101325; +MaterialStream3.T = 300; +MaterialStream3.F_p[1] = 100; +// Warning: Heater1 has no input streams +// Warning: Heater1 has no output streams +Heater1.Q = None; + end Mixer1Simulation; +end Mixer1; diff --git a/src/main/Simulator/Simulator/simulateEQN.mos b/src/main/Simulator/Simulator/simulateEQN.mos index 8f300c3..9f18c77 100644 --- a/src/main/Simulator/Simulator/simulateEQN.mos +++ b/src/main/Simulator/Simulator/simulateEQN.mos @@ -1,4 +1,4 @@ loadModel(Modelica); loadFile("Simulator/package.mo"); loadFile("Simulator/Flowsheet.mo"); -simulate(Splitter1.Splitter1Simulation, outputFormat="csv", stopTime=1.0, numberOfIntervals=1); +simulate(Mixer1.Mixer1Simulation, outputFormat="csv", stopTime=1.0, numberOfIntervals=1); diff --git a/src/main/python/mainApp.py b/src/main/python/mainApp.py index 7c4ff1e..3575676 100644 --- a/src/main/python/mainApp.py +++ b/src/main/python/mainApp.py @@ -590,7 +590,7 @@ def new(self): ''' def delete_call(self,event): try: - if event.key() == QtCore.Qt.Key_Delete: + if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Backspace: l=self.scene.selectedItems() self.container.delete(l) except Exception as e: diff --git a/src/main/python/utils/Container.py b/src/main/python/utils/Container.py index 796280f..85c0b97 100644 --- a/src/main/python/utils/Container.py +++ b/src/main/python/utils/Container.py @@ -14,7 +14,7 @@ from python.OMChem.Flowsheet import Flowsheet from python.utils.undo_manager import * from python.utils.ComponentSelector import * -from python.utils.Graphics import NodeItem, Graphics, dock_widget_lst +from python.utils.Graphics import NodeItem, NodeLine, Graphics, dock_widget_lst, lst from python.DockWidgets.DockWidget import DockWidget # ----------------- Signals ----------------- @@ -122,10 +122,55 @@ def delete(self, l): # --- Now actually perform deletions --- items_to_delete = list(l) + + # 1. Collect all connected lines if we're deleting nodes + additional_lines = set() + for item in items_to_delete: + if isinstance(item, NodeItem): + for socket in (item.input + item.output): + for line in (socket.in_lines + socket.out_lines): + if line not in items_to_delete: + additional_lines.add(line) + items_to_delete.extend(list(additional_lines)) + + # 2. Process deletions for item in items_to_delete: try: - # (your deletion code here — unchanged) - ... + if isinstance(item, NodeLine): + # Logical cleanup + if item.source and item.source.parent and hasattr(item.source.parent.obj, "remove_connection"): + item.source.parent.obj.remove_connection(0, item.source.id) + if item.target and item.target.parent and hasattr(item.target.parent.obj, "remove_connection"): + item.target.parent.obj.remove_connection(1, item.target.id) + + # Graphical cleanup + if item.source and item in item.source.out_lines: + item.source.out_lines.remove(item) + if item.target and item in item.target.in_lines: + item.target.in_lines.remove(item) + + if item.scene(): + item.scene().removeItem(item) + + elif isinstance(item, NodeItem): + # Remove from model + if item.obj in self.unit_operations: + self.unit_operations.remove(item.obj) + + # Cleanup DockWidget + if hasattr(item, "dock_widget") and item.dock_widget: + item.dock_widget.hide() + if item.dock_widget in dock_widget_lst: + dock_widget_lst.remove(item.dock_widget) + + # Remove from global tracking + if item in lst: + lst.remove(item) + + # Remove from scene + if item.scene(): + item.scene().removeItem(item) + except Exception as e: print(f"[DEBUG] delete: error deleting item {getattr(item, 'name', None)}: {e}") diff --git a/src/main/python/utils/Graphics.py b/src/main/python/utils/Graphics.py index 903442e..abc3541 100644 --- a/src/main/python/utils/Graphics.py +++ b/src/main/python/utils/Graphics.py @@ -51,7 +51,7 @@ def __init__(self, container, graphicsView=None): else: self.graphicsView = getattr(container, "graphicsView", None) - self.pos = None + self.current_pos = None if self.graphicsView is not None: try: @@ -913,9 +913,9 @@ def mouseMoveEvent(self, event): line.pointB = line.target.get_center() # --- Update node position in model object (if linked) --- - self.pos = event.scenePos() + self.current_pos = event.scenePos() if hasattr(self, "obj") and self.obj is not None: - self.obj.set_pos(self.pos) + self.obj.set_pos(self.current_pos) @@ -983,6 +983,18 @@ def itemChange(self, change, value): newPos.setY(min(rect.bottom()-height-35, max(newPos.y(), rect.top()))) self.obj.set_pos(newPos) return super(NodeItem,self).itemChange(change, newPos) + + def contextMenuEvent(self, event): + menu = QMenu() + delete_action = menu.addAction("Delete") + action = menu.exec_(event.screenPos()) + if action == delete_action: + if hasattr(self, "container") and self.container: + self.container.delete([self]) + else: + main_window = findMainWindow(self) + if main_window and hasattr(main_window, "container"): + main_window.container.delete([self]) def findMainWindow(self): ''' diff --git a/src/main/python/utils/UnitOperations.py b/src/main/python/utils/UnitOperations.py index 6c5c9c8..7a1d367 100644 --- a/src/main/python/utils/UnitOperations.py +++ b/src/main/python/utils/UnitOperations.py @@ -86,6 +86,14 @@ def add_connection(self,flag,sourceId, UnitOpr): else : self.output_stms[sourceId] = UnitOpr + def remove_connection(self, flag, sourceId): + if flag == 1: + if sourceId in self.input_stms: + del self.input_stms[sourceId] + else: + if sourceId in self.output_stms: + del self.output_stms[sourceId] + def set_pos(self,pos): self.pos = pos diff --git a/undo_redo/Undo.pkl b/undo_redo/Undo.pkl index 84c2f3767dca13fe55be0a903a18fac1f67b071d..7f95ae549464c40f09062fff0ac8ae78fa668306 100644 GIT binary patch literal 1567 zcmb7?ze)o^5XJ*0K^tv+h*UyiA)>`Ka#+MBNNiFZWMvK#4Y@n?gn-pSaAP(0l0*wD zpTkG7@EM%jTrS-1By6_YV*dQT@0;iH+hMh&UI%L#sjLXs7j2yz;2W?OVl)X`PpLBUTe_v!_W?FcUioF%^c{= z|JLEz3_NIhgy(y`RyY)1-^RfGVv9~o32CV`wn&AV;Y1o#a7y<7WXRUxNdra{ zV%-4ad|WN(y=U|E6G>q%7#Np@x!eXuQ)pw6AvMKoWtZPTxON#y(9REjQLZhnt3Jls zHrzz(z0{`U_|81cOr3pcrn30m75*)Y4+Z=#69)A$_yE2={;|b@viOnCLoqzKj0q6K zQ0=UHCUZ=1ZvjNlhm@C06!;PFZ@6*7V+ S1;NGw5=?!J;B Date: Fri, 29 May 2026 18:45:41 +0530 Subject: [PATCH 2/2] added option to drag and drop components --- src/main/python/mainApp.py | 202 ++++++++++++++++++++++++------------- undo_redo/Undo.pkl | Bin 1567 -> 1306 bytes 2 files changed, 133 insertions(+), 69 deletions(-) diff --git a/src/main/python/mainApp.py b/src/main/python/mainApp.py index 3575676..b3c0d7c 100644 --- a/src/main/python/mainApp.py +++ b/src/main/python/mainApp.py @@ -33,6 +33,57 @@ ui,_ = loadUiType(parentPath+'/ui/utils/main.ui') +class DragButtonFilter(QObject): + def __init__(self, parent, component_type): + super().__init__(parent) + self.component_type = component_type + self.startPos = None + + def eventFilter(self, obj, event): + if event.type() == QEvent.MouseButtonPress: + if event.button() == Qt.LeftButton: + self.startPos = event.pos() + elif event.type() == QEvent.MouseMove: + if event.buttons() & Qt.LeftButton and self.startPos: + if (event.pos() - self.startPos).manhattanLength() >= QApplication.startDragDistance(): + drag = QDrag(obj) + mimeData = QMimeData() + mimeData.setText(self.component_type) + drag.setMimeData(mimeData) + + # Optional: Grab button appearance as drag pixmap + pixmap = obj.grab() + drag.setPixmap(pixmap.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + drag.setHotSpot(QPoint(32, 32)) + + drag.exec_(Qt.CopyAction) + self.startPos = None + return True + return False + +class DropFilter(QObject): + def __init__(self, parent, main_app): + super().__init__(parent) + self.main_app = main_app + + def eventFilter(self, obj, event): + if event.type() == QEvent.DragEnter: + if event.mimeData().hasText(): + event.acceptProposedAction() + return True + elif event.type() == QEvent.DragMove: + if event.mimeData().hasText(): + event.acceptProposedAction() + return True + elif event.type() == QEvent.Drop: + component_type = event.mimeData().text() + # map from viewport coordinates to scene coordinates + pos = self.main_app.graphicsView.mapToScene(event.pos()) + self.main_app.component(component_type, pos=pos) + event.acceptProposedAction() + return True + return False + ''' MainApp class is responsible for all the main App Ui operations ''' @@ -79,6 +130,12 @@ def __init__(self): self.graphicsView.setMouseTracking(True) self.graphicsView.keyPressEvent=self.delete_call + # ✅ Enable Drag-and-Drop on the canvas + self.graphicsView.setAcceptDrops(True) + self.graphicsView.viewport().setAcceptDrops(True) + self.drop_filter = DropFilter(self, self) + self.graphicsView.viewport().installEventFilter(self.drop_filter) + # box for selected compounds self.selectedElementsDock = QDockWidget("Selected Compounds", self) self.selectedElementsDock.setFeatures( @@ -222,50 +279,47 @@ def menu_bar(self): Handles all the buttons of different components. ''' def button_handler(self): - # --- Streams --- - self.pushButton.clicked.connect(partial(self.component, 'MaterialStream')) + # --- Mapping of buttons to component types --- + button_to_type = { + self.pushButton: 'MaterialStream', + self.pushButton_7: 'Mixer', + self.pushButton_10: 'Splitter', + self.pushButton_11: 'Heater', + self.pushButton_12: 'Cooler', + self.pushButton_9: 'Flash', + self.pushButton_13: 'CompoundSeparator', + self.pushButton_25: 'Valve', + self.pushButton_14: 'CentrifugalPump', + self.pushButton_15: 'AdiabaticCompressor', + self.pushButton_16: 'AdiabaticExpander', + self.pushButton_26: 'DistillationColumn', + self.pushButton_18: 'ShortcutColumn' + } + + # --- Tooltips --- self.pushButton.setToolTip("Represents a flow of material (mixture of compounds) between unit operations, carrying properties like temperature, pressure, and composition.") - - # --- Mixer/Splitter --- - self.pushButton_7.clicked.connect(partial(self.component, 'Mixer')) self.pushButton_7.setToolTip("Combines two or more input streams into a single output stream by mixing their compositions and energy.") - - self.pushButton_10.clicked.connect(partial(self.component, 'Splitter')) self.pushButton_10.setToolTip("Divides one input stream into multiple output streams based on specified split ratios.") - - # --- Exchangers --- - self.pushButton_11.clicked.connect(partial(self.component, 'Heater')) self.pushButton_11.setToolTip("Increases the temperature of a process stream by adding heat energy.") - - self.pushButton_12.clicked.connect(partial(self.component, 'Cooler')) self.pushButton_12.setToolTip("Decreases the temperature of a process stream by removing heat energy.") - - # --- Separator --- - self.pushButton_9.clicked.connect(partial(self.component, 'Flash')) self.pushButton_9.setToolTip("Separates a vapor–liquid mixture into vapor and liquid phases at a given temperature and pressure.") - - self.pushButton_13.clicked.connect(partial(self.component, 'CompoundSeparator')) self.pushButton_13.setToolTip("Splits a mixture into components based on composition, typically an ideal separation.") - - # --- Pressure Change --- - self.pushButton_25.clicked.connect(partial(self.component, 'Valve')) self.pushButton_25.setToolTip("Reduces the pressure of a fluid stream (throttling process) without performing work or heat exchange.") - - self.pushButton_14.clicked.connect(partial(self.component, 'CentrifugalPump')) self.pushButton_14.setToolTip("Increases the pressure of a liquid stream using mechanical work (energy input).") - - self.pushButton_15.clicked.connect(partial(self.component, 'AdiabaticCompressor')) self.pushButton_15.setToolTip("Compresses a gas stream without heat exchange; increases pressure and temperature.") - - self.pushButton_16.clicked.connect(partial(self.component, 'AdiabaticExpander')) self.pushButton_16.setToolTip("Expands a gas stream to produce work output, lowering pressure and temperature.") - - # --- Columns --- - self.pushButton_26.clicked.connect(partial(self.component, 'DistillationColumn')) self.pushButton_26.setToolTip("Separates mixtures into products based on volatility differences using vapor–liquid equilibrium.") - - self.pushButton_18.clicked.connect(partial(self.component, 'ShortcutColumn')) self.pushButton_18.setToolTip("Performs approximate distillation using shortcut (simplified) column calculations.") + + # --- Install Filters and Connect Clicks --- + for btn, comp_type in button_to_type.items(): + # Support clicking + btn.clicked.connect(partial(self.component, comp_type)) + + # Support dragging + drag_filter = DragButtonFilter(self, comp_type) + btn.installEventFilter(drag_filter) + setattr(btn, "_drag_filter", drag_filter) # Keep reference to prevent GC ''' Displays help box @@ -388,10 +442,10 @@ def terminate(self): def zoom_reset(self): if(self.zoom_count>0): for i in range(self.zoom_count): - self.zoomout() + self.zoom_out() elif(self.zoom_count<0): for i in range(abs(self.zoom_count)): - self.zoomin() + self.zoom_in() ''' ZoomOut the canvas @@ -415,8 +469,12 @@ def zoom_in(self): from PyQt5.QtCore import QPointF, QTimer from PyQt5.QtWidgets import QMessageBox - def component(self, unit_operation_type): - print("[DEBUG] component() called with:", unit_operation_type) + def component(self, unit_operation_type, pos=None): + print("[DEBUG] component() called with:", unit_operation_type, "at pos:", pos) + + # Fix: ignore the bool argument from clicked signal + if not isinstance(pos, QPointF): + pos = None # --- Step 1: Check compound selection --- if not self.comp.is_compound_selected(): @@ -441,34 +499,38 @@ def component(self, unit_operation_type): return # --- Step 3: Manage component placement offsets (grid + center) --- - horizontal_gap = 180 # horizontal space between components - vertical_gap = 150 # vertical space between rows - items_per_row = 5 # how many components before wrapping to next row - - # Initialize offset if missing or None - if not hasattr(self, "component_offset") or self.component_offset is None: - view_center = self.graphicsView.mapToScene(self.graphicsView.viewport().rect().center()) - self.component_offset = QPointF(view_center.x(), view_center.y()) - self._grid_count = 0 - print("[DEBUG] Offset initialized to:", self.component_offset) - else: - # Safety check for grid counter - if not hasattr(self, "_grid_count"): + if pos is None: + horizontal_gap = 180 # horizontal space between components + vertical_gap = 150 # vertical space between rows + items_per_row = 5 # how many components before wrapping to next row + + # Initialize offset if missing or None + if not hasattr(self, "component_offset") or self.component_offset is None: + view_center = self.graphicsView.mapToScene(self.graphicsView.viewport().rect().center()) + self.component_offset = QPointF(view_center.x(), view_center.y()) self._grid_count = 0 - - # Move right or wrap to next row - x, y = self.component_offset.x(), self.component_offset.y() - self._grid_count += 1 - if self._grid_count >= items_per_row: - self.component_offset = QPointF( - self.component_offset.x() - horizontal_gap * (items_per_row - 1), - y + vertical_gap - ) - self._grid_count = 0 - print("[DEBUG] Wrapped to next row:", self.component_offset) + print("[DEBUG] Offset initialized to:", self.component_offset) else: - self.component_offset = QPointF(x + horizontal_gap, y) - print("[DEBUG] Moved right to:", self.component_offset) + # Safety check for grid counter + if not hasattr(self, "_grid_count"): + self._grid_count = 0 + + # Move right or wrap to next row + x, y = self.component_offset.x(), self.component_offset.y() + self._grid_count += 1 + if self._grid_count >= items_per_row: + self.component_offset = QPointF( + self.component_offset.x() - horizontal_gap * (items_per_row - 1), + y + vertical_gap + ) + self._grid_count = 0 + print("[DEBUG] Wrapped to next row:", self.component_offset) + else: + self.component_offset = QPointF(x + horizontal_gap, y) + print("[DEBUG] Moved right to:", self.component_offset) + target_pos = self.component_offset + else: + target_pos = pos # --- Step 4: Add the new unit operation --- before_ids = {id(it) for it in self.scene.items()} # Snapshot before adding @@ -477,11 +539,12 @@ def component(self, unit_operation_type): # --- Step 5: Direct placement if returned item --- if node_item is not None and hasattr(node_item, "setPos"): - node_item.setPos(self.component_offset) - print("[DEBUG] Set position on returned item:", self.component_offset) + node_item.setPos(target_pos) + print("[DEBUG] Set position on returned item:", target_pos) - self.graphicsView.centerOn(node_item) - print("[DEBUG] Centered view on new component.") + if pos is None: # Only center view for click-to-add + self.graphicsView.centerOn(node_item) + print("[DEBUG] Centered view on new component.") return # --- Step 6: Fallback — detect newly added graphics item --- @@ -494,10 +557,11 @@ def find_and_position_new(): for it in new_items: try: if hasattr(it, "setPos"): - it.setPos(self.component_offset) - print("[DEBUG] Positioned new item:", it, "at", self.component_offset) - self.graphicsView.centerOn(it) - print("[DEBUG] Centered fallback component in viewport.") + it.setPos(target_pos) + print("[DEBUG] Positioned new item:", it, "at", target_pos) + if pos is None: + self.graphicsView.centerOn(it) + print("[DEBUG] Centered fallback component in viewport.") positioned = True break except Exception as e: diff --git a/undo_redo/Undo.pkl b/undo_redo/Undo.pkl index 7f95ae549464c40f09062fff0ac8ae78fa668306..112bdfaa10aa0543f22c7c0170965f68c7234af1 100644 GIT binary patch literal 1306 zcmbV~KTE?v7{-ex>1GhYtw_b8Q?OZF13}NFAh@=Y3@2AeY-tkEq2Sc%#@F3H_$B%Q z{9L}5^U|buEy0^^c%OTo=XdX`_2$~0@=Rc`1a41rnr1M8B@DRr6sHupJBo9FWDf)VwzNj`Ka#+MBNNiFZWMvK#4Y@n?gn-pSaAP(0l0*wD zpTkG7@EM%jTrS-1By6_YV*dQT@0;iH+hMh&UI%L#sjLXs7j2yz;2W?OVl)X`PpLBUTe_v!_W?FcUioF%^c{= z|JLEz3_NIhgy(y`RyY)1-^RfGVv9~o32CV`wn&AV;Y1o#a7y<7WXRUxNdra{ zV%-4ad|WN(y=U|E6G>q%7#Np@x!eXuQ)pw6AvMKoWtZPTxON#y(9REjQLZhnt3Jls zHrzz(z0{`U_|81cOr3pcrn30m75*)Y4+Z=#69)A$_yE2={;|b@viOnCLoqzKj0q6K zQ0=UHCUZ=1ZvjNlhm@C06!;PFZ@6*7V+ S1;NGw5=?!J;B