pySimBlocks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- tests/regression/test_simulink_dc_motor.py +217 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from PySide6.QtWidgets import (
|
|
22
|
+
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from pySimBlocks.gui.dialogs.settings.project import ProjectSettingsWidget
|
|
26
|
+
from pySimBlocks.gui.dialogs.settings.simulation import SimulationSettingsWidget
|
|
27
|
+
from pySimBlocks.gui.dialogs.settings.plots import PlotSettingsWidget
|
|
28
|
+
from pySimBlocks.gui.models.project_state import ProjectState
|
|
29
|
+
from pySimBlocks.gui.project_controller import ProjectController
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SettingsDialog(QDialog):
|
|
33
|
+
def __init__(self, project_state: ProjectState, project_controller: ProjectController, parent=None):
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
self.setWindowTitle("Settings")
|
|
36
|
+
self.setMinimumWidth(500)
|
|
37
|
+
|
|
38
|
+
layout = QVBoxLayout(self)
|
|
39
|
+
|
|
40
|
+
# ---------------- Tabs ----------------
|
|
41
|
+
self.tabs = QTabWidget()
|
|
42
|
+
self.project_tab = ProjectSettingsWidget(project_state, project_controller, self)
|
|
43
|
+
self.simulation_tab = SimulationSettingsWidget(project_state, project_controller)
|
|
44
|
+
self.plots_tab = PlotSettingsWidget(project_state, project_controller)
|
|
45
|
+
|
|
46
|
+
self.tabs.addTab(self.project_tab, "Project")
|
|
47
|
+
self.tabs.addTab(self.simulation_tab, "Simulation")
|
|
48
|
+
self.tabs.addTab(self.plots_tab, "Plots")
|
|
49
|
+
|
|
50
|
+
self.tabs.currentChanged.connect(self._on_tab_changed)
|
|
51
|
+
|
|
52
|
+
layout.addWidget(self.tabs)
|
|
53
|
+
|
|
54
|
+
# ---------------- Buttons ----------------
|
|
55
|
+
buttons = QHBoxLayout()
|
|
56
|
+
buttons.addStretch()
|
|
57
|
+
|
|
58
|
+
ok_btn = QPushButton("Ok")
|
|
59
|
+
ok_btn.setDefault(True)
|
|
60
|
+
ok_btn.clicked.connect(self.ok)
|
|
61
|
+
|
|
62
|
+
apply_btn = QPushButton("Apply")
|
|
63
|
+
apply_btn.clicked.connect(self.apply)
|
|
64
|
+
|
|
65
|
+
buttons.addWidget(ok_btn)
|
|
66
|
+
buttons.addWidget(apply_btn)
|
|
67
|
+
|
|
68
|
+
layout.addLayout(buttons)
|
|
69
|
+
|
|
70
|
+
def ok(self):
|
|
71
|
+
self.apply()
|
|
72
|
+
self.accept()
|
|
73
|
+
|
|
74
|
+
def apply(self):
|
|
75
|
+
if not self.project_tab.apply():
|
|
76
|
+
return
|
|
77
|
+
self.simulation_tab.apply()
|
|
78
|
+
|
|
79
|
+
def refresh_tabs_from_project(self):
|
|
80
|
+
self.simulation_tab.refresh_from_project()
|
|
81
|
+
self.plots_tab.refresh_from_project()
|
|
82
|
+
|
|
83
|
+
def _on_tab_changed(self, index):
|
|
84
|
+
#save curr widget
|
|
85
|
+
widget = self.tabs.widget(index)
|
|
86
|
+
|
|
87
|
+
if hasattr(widget, "refresh_from_project"):
|
|
88
|
+
widget.refresh_from_project()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from PySide6.QtWidgets import (
|
|
22
|
+
QDialog, QLabel, QPushButton,
|
|
23
|
+
QHBoxLayout, QVBoxLayout, QStyle
|
|
24
|
+
)
|
|
25
|
+
from PySide6.QtCore import Qt
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UnsavedChangesDialog(QDialog):
|
|
29
|
+
SAVE = 1
|
|
30
|
+
DISCARD = 2
|
|
31
|
+
CANCEL = 3
|
|
32
|
+
|
|
33
|
+
def __init__(self, action_name: str, parent=None):
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
|
|
36
|
+
self.setWindowTitle("Unsaved changes")
|
|
37
|
+
self.setModal(True)
|
|
38
|
+
|
|
39
|
+
self.setWindowFlags(
|
|
40
|
+
Qt.Dialog |
|
|
41
|
+
Qt.WindowTitleHint |
|
|
42
|
+
Qt.WindowCloseButtonHint
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# --- Icon (same as QMessageBox warning) ---
|
|
46
|
+
icon_label = QLabel()
|
|
47
|
+
icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning)
|
|
48
|
+
icon_label.setPixmap(icon.pixmap(64, 64))
|
|
49
|
+
icon_label.setAlignment(Qt.AlignTop)
|
|
50
|
+
|
|
51
|
+
# --- Text ---
|
|
52
|
+
text_label = QLabel(
|
|
53
|
+
"<b>The project has unsaved changes. "
|
|
54
|
+
f"Do you want to save your changes before {action_name}?"
|
|
55
|
+
)
|
|
56
|
+
text_label.setWordWrap(True)
|
|
57
|
+
|
|
58
|
+
text_layout = QVBoxLayout()
|
|
59
|
+
text_layout.addWidget(text_label)
|
|
60
|
+
|
|
61
|
+
content_layout = QHBoxLayout()
|
|
62
|
+
content_layout.addWidget(icon_label)
|
|
63
|
+
content_layout.addLayout(text_layout)
|
|
64
|
+
|
|
65
|
+
# --- Buttons (order EXACT) ---
|
|
66
|
+
yes_btn = QPushButton("Yes")
|
|
67
|
+
no_btn = QPushButton("No")
|
|
68
|
+
cancel_btn = QPushButton("Cancel")
|
|
69
|
+
|
|
70
|
+
yes_btn.clicked.connect(lambda: self.done(self.SAVE))
|
|
71
|
+
no_btn.clicked.connect(lambda: self.done(self.DISCARD))
|
|
72
|
+
cancel_btn.clicked.connect(lambda: self.done(self.CANCEL))
|
|
73
|
+
|
|
74
|
+
buttons_layout = QHBoxLayout()
|
|
75
|
+
buttons_layout.addStretch()
|
|
76
|
+
buttons_layout.addWidget(yes_btn)
|
|
77
|
+
buttons_layout.addWidget(no_btn)
|
|
78
|
+
buttons_layout.addWidget(cancel_btn)
|
|
79
|
+
|
|
80
|
+
# --- Main layout ---
|
|
81
|
+
layout = QVBoxLayout(self)
|
|
82
|
+
layout.addLayout(content_layout)
|
|
83
|
+
layout.addSpacing(12)
|
|
84
|
+
layout.addLayout(buttons_layout)
|
|
85
|
+
|
|
86
|
+
# ❌ Esc does nothing
|
|
87
|
+
def reject(self):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# ❌ Close button (X) does nothing
|
|
91
|
+
def closeEvent(self, event):
|
|
92
|
+
event.ignore()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from PySide6.QtWidgets import QApplication
|
|
25
|
+
from pySimBlocks.gui.main_window import MainWindow
|
|
26
|
+
|
|
27
|
+
# ============================================================
|
|
28
|
+
# Entry point
|
|
29
|
+
# ============================================================
|
|
30
|
+
def main():
|
|
31
|
+
if len(sys.argv) > 1:
|
|
32
|
+
project_dir = os.path.abspath(sys.argv[1])
|
|
33
|
+
else:
|
|
34
|
+
project_dir = os.getcwd()
|
|
35
|
+
project_path = Path(project_dir).resolve()
|
|
36
|
+
run_app(project_path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_app(project_path: Path):
|
|
40
|
+
app = QApplication(sys.argv)
|
|
41
|
+
window = MainWindow(project_path)
|
|
42
|
+
app.aboutToQuit.connect(window.cleanup)
|
|
43
|
+
window.resize(1100, 600)
|
|
44
|
+
window.show()
|
|
45
|
+
sys.exit(app.exec())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from pySimBlocks.gui.graphics.block_item import BlockItem
|
|
22
|
+
from pySimBlocks.gui.graphics.connection_item import ConnectionItem
|
|
23
|
+
from pySimBlocks.gui.graphics.port_item import PortItem
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BlockItem",
|
|
27
|
+
"ConnectionItem",
|
|
28
|
+
"PortItem"
|
|
29
|
+
]
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from PySide6.QtCore import QPoint, QPointF, QRectF, Qt
|
|
24
|
+
from PySide6.QtGui import QPainterPath, QPen
|
|
25
|
+
from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem, QStyle
|
|
26
|
+
|
|
27
|
+
from pySimBlocks.gui.dialogs.block_dialog import BlockDialog
|
|
28
|
+
from pySimBlocks.gui.graphics.port_item import PortItem
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pySimBlocks.gui.models.block_instance import BlockInstance
|
|
32
|
+
from pySimBlocks.gui.widgets.diagram_view import DiagramView
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BlockItem(QGraphicsRectItem):
|
|
36
|
+
WIDTH = 120
|
|
37
|
+
HEIGHT = 60
|
|
38
|
+
MIN_WIDTH = 40
|
|
39
|
+
MIN_HEIGHT = 30
|
|
40
|
+
GRID_DX = 5
|
|
41
|
+
GRID_DY = 5
|
|
42
|
+
SELECTION_HANDLE_SIZE = 8
|
|
43
|
+
SELECTION_HANDLE_HIT_SIZE = 16
|
|
44
|
+
|
|
45
|
+
def __init__(self,
|
|
46
|
+
instance: "BlockInstance",
|
|
47
|
+
pos: QPointF | QPoint,
|
|
48
|
+
view: "DiagramView",
|
|
49
|
+
layout: dict | None = None,
|
|
50
|
+
):
|
|
51
|
+
layout = layout or {}
|
|
52
|
+
width = layout.get("width", self.WIDTH)
|
|
53
|
+
height = layout.get("height", self.HEIGHT)
|
|
54
|
+
width = float(width) if isinstance(width, (int, float)) else float(self.WIDTH)
|
|
55
|
+
height = float(height) if isinstance(height, (int, float)) else float(self.HEIGHT)
|
|
56
|
+
width = max(float(self.MIN_WIDTH), width)
|
|
57
|
+
height = max(float(self.MIN_HEIGHT), height)
|
|
58
|
+
super().__init__(0, 0, width, height)
|
|
59
|
+
self.view = view
|
|
60
|
+
self.instance = instance
|
|
61
|
+
self.orientation = layout.get("orientation", "normal")
|
|
62
|
+
self._resize_handle: str | None = None
|
|
63
|
+
self._resize_start_mouse: QPointF | None = None
|
|
64
|
+
self._resize_start_pos: QPointF | None = None
|
|
65
|
+
self._resize_start_width = self.WIDTH
|
|
66
|
+
self._resize_start_height = self.HEIGHT
|
|
67
|
+
|
|
68
|
+
self.setPos(pos)
|
|
69
|
+
self.setFlag(QGraphicsRectItem.ItemIsMovable)
|
|
70
|
+
self.setFlag(QGraphicsRectItem.ItemIsSelectable)
|
|
71
|
+
self.setFlag(QGraphicsRectItem.ItemSendsScenePositionChanges)
|
|
72
|
+
|
|
73
|
+
# Ports
|
|
74
|
+
self.port_items: list[PortItem] = []
|
|
75
|
+
for port in self.instance.ports:
|
|
76
|
+
item = PortItem(port, self)
|
|
77
|
+
self.port_items.append(item)
|
|
78
|
+
self._layout_ports()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------
|
|
82
|
+
# Public Methods
|
|
83
|
+
# --------------------------------------------------------------------------
|
|
84
|
+
def get_port_item(self, name:str) -> PortItem | None:
|
|
85
|
+
for port in self.port_items:
|
|
86
|
+
if port.instance.name == name:
|
|
87
|
+
return port
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------
|
|
90
|
+
def refresh_ports(self):
|
|
91
|
+
|
|
92
|
+
for item in list(self.port_items):
|
|
93
|
+
if item.instance not in self.instance.ports:
|
|
94
|
+
self.scene().removeItem(item)
|
|
95
|
+
self.port_items.remove(item)
|
|
96
|
+
|
|
97
|
+
displayed_ports = {item.instance for item in self.port_items}
|
|
98
|
+
|
|
99
|
+
for port in self.instance.ports:
|
|
100
|
+
if port not in displayed_ports:
|
|
101
|
+
item = PortItem(port, self)
|
|
102
|
+
self.port_items.append(item)
|
|
103
|
+
|
|
104
|
+
self._layout_ports()
|
|
105
|
+
for item in self.port_items:
|
|
106
|
+
item.update_display_as()
|
|
107
|
+
self.view.on_block_ports_refreshed(self)
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------------------
|
|
110
|
+
def toggle_orientation(self):
|
|
111
|
+
self.orientation = "flipped" if self.orientation == "normal" else "normal"
|
|
112
|
+
|
|
113
|
+
self._layout_ports()
|
|
114
|
+
self.view.on_block_moved(self)
|
|
115
|
+
self.update()
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
# Visual Methods
|
|
119
|
+
# --------------------------------------------------------------------------
|
|
120
|
+
def boundingRect(self) -> QRectF:
|
|
121
|
+
half = self.SELECTION_HANDLE_HIT_SIZE / 2
|
|
122
|
+
return self.rect().adjusted(-half, -half, half, half)
|
|
123
|
+
|
|
124
|
+
# --------------------------------------------------------------
|
|
125
|
+
def shape(self) -> QPainterPath:
|
|
126
|
+
path = QPainterPath()
|
|
127
|
+
path.addRect(self.rect())
|
|
128
|
+
for rect in self._handle_hit_rects().values():
|
|
129
|
+
path.addRect(rect)
|
|
130
|
+
return path
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------
|
|
133
|
+
def paint(self, painter, option, widget=None):
|
|
134
|
+
t = self.view.theme
|
|
135
|
+
selected = bool(option.state & QStyle.State_Selected)
|
|
136
|
+
|
|
137
|
+
if selected:
|
|
138
|
+
painter.setBrush(t.block_bg_selected)
|
|
139
|
+
painter.setPen(QPen(t.block_border_selected, 3))
|
|
140
|
+
else:
|
|
141
|
+
painter.setBrush(t.block_bg)
|
|
142
|
+
painter.setPen(QPen(t.block_border, 3))
|
|
143
|
+
|
|
144
|
+
painter.drawRect(self.rect())
|
|
145
|
+
if selected:
|
|
146
|
+
painter.setPen(t.text_selected)
|
|
147
|
+
else:
|
|
148
|
+
painter.setPen(t.text)
|
|
149
|
+
painter.drawText(self.rect(), Qt.AlignCenter, self.instance.name)
|
|
150
|
+
|
|
151
|
+
if selected:
|
|
152
|
+
half = self.SELECTION_HANDLE_SIZE / 2
|
|
153
|
+
r = self.rect()
|
|
154
|
+
corners = [
|
|
155
|
+
(r.left(), r.top()),
|
|
156
|
+
(r.right(), r.top()),
|
|
157
|
+
(r.left(), r.bottom()),
|
|
158
|
+
(r.right(), r.bottom()),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
painter.setPen(QPen(t.block_border_selected, 1))
|
|
162
|
+
painter.setBrush(t.text_selected)
|
|
163
|
+
for x, y in corners:
|
|
164
|
+
painter.drawRect(x - half, y - half, self.SELECTION_HANDLE_SIZE, self.SELECTION_HANDLE_SIZE)
|
|
165
|
+
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
# Event Methods
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
def mousePressEvent(self, event):
|
|
170
|
+
if self.isSelected():
|
|
171
|
+
handle = self._handle_at(event.pos())
|
|
172
|
+
if handle is not None:
|
|
173
|
+
self._resize_handle = handle
|
|
174
|
+
self._resize_start_mouse = event.scenePos()
|
|
175
|
+
self._resize_start_pos = self.pos()
|
|
176
|
+
self._resize_start_width = self.rect().width()
|
|
177
|
+
self._resize_start_height = self.rect().height()
|
|
178
|
+
event.accept()
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
super().mousePressEvent(event)
|
|
182
|
+
|
|
183
|
+
# --------------------------------------------------------------
|
|
184
|
+
def mouseMoveEvent(self, event):
|
|
185
|
+
if self._resize_handle and self._resize_start_mouse and self._resize_start_pos:
|
|
186
|
+
delta = event.scenePos() - self._resize_start_mouse
|
|
187
|
+
dx = round(delta.x() / self.GRID_DX) * self.GRID_DX
|
|
188
|
+
dy = round(delta.y() / self.GRID_DY) * self.GRID_DY
|
|
189
|
+
|
|
190
|
+
start_x = self._resize_start_pos.x()
|
|
191
|
+
start_y = self._resize_start_pos.y()
|
|
192
|
+
start_w = self._resize_start_width
|
|
193
|
+
start_h = self._resize_start_height
|
|
194
|
+
|
|
195
|
+
if self._resize_handle in ("tl", "bl"): # drag left edge
|
|
196
|
+
new_x = min(start_x + dx, start_x + start_w - self.MIN_WIDTH)
|
|
197
|
+
new_w = max(self.MIN_WIDTH, (start_x + start_w) - new_x)
|
|
198
|
+
else: # drag right edge
|
|
199
|
+
new_x = start_x
|
|
200
|
+
new_w = max(self.MIN_WIDTH, start_w + dx)
|
|
201
|
+
|
|
202
|
+
if self._resize_handle in ("tl", "tr"): # drag top edge
|
|
203
|
+
new_y = min(start_y + dy, start_y + start_h - self.MIN_HEIGHT)
|
|
204
|
+
new_h = max(self.MIN_HEIGHT, (start_y + start_h) - new_y)
|
|
205
|
+
else: # drag bottom edge
|
|
206
|
+
new_y = start_y
|
|
207
|
+
new_h = max(self.MIN_HEIGHT, start_h + dy)
|
|
208
|
+
|
|
209
|
+
self.setPos(QPointF(new_x, new_y))
|
|
210
|
+
self.setRect(0, 0, new_w, new_h)
|
|
211
|
+
self._layout_ports()
|
|
212
|
+
self.view.on_block_moved(self)
|
|
213
|
+
self.update()
|
|
214
|
+
event.accept()
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
super().mouseMoveEvent(event)
|
|
218
|
+
|
|
219
|
+
# --------------------------------------------------------------
|
|
220
|
+
def mouseReleaseEvent(self, event):
|
|
221
|
+
self._resize_handle = None
|
|
222
|
+
self._resize_start_mouse = None
|
|
223
|
+
self._resize_start_pos = None
|
|
224
|
+
super().mouseReleaseEvent(event)
|
|
225
|
+
|
|
226
|
+
# --------------------------------------------------------------
|
|
227
|
+
def mouseDoubleClickEvent(self, event):
|
|
228
|
+
dialog = BlockDialog(self, readonly=False)
|
|
229
|
+
dialog.exec()
|
|
230
|
+
self.update()
|
|
231
|
+
event.accept()
|
|
232
|
+
|
|
233
|
+
# --------------------------------------------------------------
|
|
234
|
+
def itemChange(self, change, value):
|
|
235
|
+
if change == QGraphicsItem.ItemPositionChange and self.scene():
|
|
236
|
+
x = round(value.x() / self.GRID_DX) * self.GRID_DX
|
|
237
|
+
y = round(value.y() / self.GRID_DY) * self.GRID_DY
|
|
238
|
+
return QPointF(x, y)
|
|
239
|
+
|
|
240
|
+
if change == QGraphicsItem.ItemPositionHasChanged:
|
|
241
|
+
self.view.on_block_moved(self)
|
|
242
|
+
|
|
243
|
+
return super().itemChange(change, value)
|
|
244
|
+
|
|
245
|
+
# --------------------------------------------------------------------------
|
|
246
|
+
# Private Methods
|
|
247
|
+
# --------------------------------------------------------------------------
|
|
248
|
+
def _handle_hit_rects(self) -> dict[str, QRectF]:
|
|
249
|
+
half = self.SELECTION_HANDLE_HIT_SIZE / 2
|
|
250
|
+
r = self.rect()
|
|
251
|
+
return {
|
|
252
|
+
"tl": QRectF(r.left() - half, r.top() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
|
|
253
|
+
"tr": QRectF(r.right() - half, r.top() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
|
|
254
|
+
"bl": QRectF(r.left() - half, r.bottom() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
|
|
255
|
+
"br": QRectF(r.right() - half, r.bottom() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# --------------------------------------------------------------
|
|
259
|
+
def _handle_at(self, local_pos: QPointF) -> str | None:
|
|
260
|
+
for name, rect in self._handle_hit_rects().items():
|
|
261
|
+
if rect.contains(local_pos):
|
|
262
|
+
return name
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# --------------------------------------------------------------
|
|
266
|
+
def _layout_ports(self):
|
|
267
|
+
inputs = [p for p in self.port_items if p.is_input]
|
|
268
|
+
outputs = [p for p in self.port_items if not p.is_input]
|
|
269
|
+
|
|
270
|
+
flipped = self.orientation == "flipped"
|
|
271
|
+
width = self.rect().width()
|
|
272
|
+
|
|
273
|
+
if not flipped:
|
|
274
|
+
self._layout_side(inputs, x=0)
|
|
275
|
+
self._layout_side(outputs, x=width)
|
|
276
|
+
else:
|
|
277
|
+
self._layout_side(inputs, x=width)
|
|
278
|
+
self._layout_side(outputs, x=0)
|
|
279
|
+
|
|
280
|
+
# --------------------------------------------------------------
|
|
281
|
+
def _layout_side(self, ports, x):
|
|
282
|
+
if not ports:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
step = self.rect().height() / (len(ports) + 1)
|
|
286
|
+
|
|
287
|
+
for i, port in enumerate(ports, start=1):
|
|
288
|
+
port.setPos(x, i * step)
|
|
289
|
+
port.update_label_position()
|