digsim-logic-simulator 0.22.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.
- digsim/__init__.py +6 -0
- digsim/app/__main__.py +12 -0
- digsim/app/cli.py +68 -0
- digsim/app/gui/__init__.py +6 -0
- digsim/app/gui/_circuit_area.py +468 -0
- digsim/app/gui/_component_selection.py +154 -0
- digsim/app/gui/_main_window.py +163 -0
- digsim/app/gui/_top_bar.py +339 -0
- digsim/app/gui/_utils.py +26 -0
- digsim/app/gui/_warning_dialog.py +46 -0
- digsim/app/gui_objects/__init__.py +7 -0
- digsim/app/gui_objects/_bus_bit_object.py +94 -0
- digsim/app/gui_objects/_buzzer_object.py +97 -0
- digsim/app/gui_objects/_component_context_menu.py +79 -0
- digsim/app/gui_objects/_component_object.py +374 -0
- digsim/app/gui_objects/_component_port_item.py +63 -0
- digsim/app/gui_objects/_dip_switch_object.py +104 -0
- digsim/app/gui_objects/_gui_note_object.py +80 -0
- digsim/app/gui_objects/_gui_object_factory.py +80 -0
- digsim/app/gui_objects/_hexdigit_object.py +53 -0
- digsim/app/gui_objects/_image_objects.py +239 -0
- digsim/app/gui_objects/_label_object.py +97 -0
- digsim/app/gui_objects/_logic_analyzer_object.py +86 -0
- digsim/app/gui_objects/_seven_segment_object.py +131 -0
- digsim/app/gui_objects/_shortcut_objects.py +82 -0
- digsim/app/gui_objects/_yosys_object.py +32 -0
- digsim/app/gui_objects/images/AND.png +0 -0
- digsim/app/gui_objects/images/Analyzer.png +0 -0
- digsim/app/gui_objects/images/BUF.png +0 -0
- digsim/app/gui_objects/images/Buzzer.png +0 -0
- digsim/app/gui_objects/images/Clock.png +0 -0
- digsim/app/gui_objects/images/DFF.png +0 -0
- digsim/app/gui_objects/images/DIP_SWITCH.png +0 -0
- digsim/app/gui_objects/images/FlipFlop.png +0 -0
- digsim/app/gui_objects/images/IC.png +0 -0
- digsim/app/gui_objects/images/LED_OFF.png +0 -0
- digsim/app/gui_objects/images/LED_ON.png +0 -0
- digsim/app/gui_objects/images/MUX.png +0 -0
- digsim/app/gui_objects/images/NAND.png +0 -0
- digsim/app/gui_objects/images/NOR.png +0 -0
- digsim/app/gui_objects/images/NOT.png +0 -0
- digsim/app/gui_objects/images/ONE.png +0 -0
- digsim/app/gui_objects/images/OR.png +0 -0
- digsim/app/gui_objects/images/PB.png +0 -0
- digsim/app/gui_objects/images/Switch_OFF.png +0 -0
- digsim/app/gui_objects/images/Switch_ON.png +0 -0
- digsim/app/gui_objects/images/XNOR.png +0 -0
- digsim/app/gui_objects/images/XOR.png +0 -0
- digsim/app/gui_objects/images/YOSYS.png +0 -0
- digsim/app/gui_objects/images/ZERO.png +0 -0
- digsim/app/images/app_icon.png +0 -0
- digsim/app/model/__init__.py +6 -0
- digsim/app/model/_model.py +210 -0
- digsim/app/model/_model_components.py +162 -0
- digsim/app/model/_model_new_wire.py +57 -0
- digsim/app/model/_model_objects.py +155 -0
- digsim/app/model/_model_settings.py +35 -0
- digsim/app/model/_model_shortcuts.py +72 -0
- digsim/app/settings/__init__.py +8 -0
- digsim/app/settings/_component_settings.py +415 -0
- digsim/app/settings/_gui_settings.py +71 -0
- digsim/app/settings/_shortcut_dialog.py +39 -0
- digsim/circuit/__init__.py +7 -0
- digsim/circuit/_circuit.py +329 -0
- digsim/circuit/_waves_writer.py +61 -0
- digsim/circuit/components/__init__.py +26 -0
- digsim/circuit/components/_bus_bits.py +68 -0
- digsim/circuit/components/_button.py +44 -0
- digsim/circuit/components/_buzzer.py +45 -0
- digsim/circuit/components/_clock.py +54 -0
- digsim/circuit/components/_dip_switch.py +73 -0
- digsim/circuit/components/_flip_flops.py +99 -0
- digsim/circuit/components/_gates.py +246 -0
- digsim/circuit/components/_hexdigit.py +82 -0
- digsim/circuit/components/_ic.py +36 -0
- digsim/circuit/components/_label_wire.py +167 -0
- digsim/circuit/components/_led.py +18 -0
- digsim/circuit/components/_logic_analyzer.py +60 -0
- digsim/circuit/components/_mem64kbyte.py +42 -0
- digsim/circuit/components/_memstdout.py +37 -0
- digsim/circuit/components/_note.py +25 -0
- digsim/circuit/components/_on_off_switch.py +54 -0
- digsim/circuit/components/_seven_segment.py +28 -0
- digsim/circuit/components/_static_level.py +28 -0
- digsim/circuit/components/_static_value.py +44 -0
- digsim/circuit/components/_yosys_atoms.py +1353 -0
- digsim/circuit/components/_yosys_component.py +232 -0
- digsim/circuit/components/atoms/__init__.py +23 -0
- digsim/circuit/components/atoms/_component.py +280 -0
- digsim/circuit/components/atoms/_digsim_exception.py +8 -0
- digsim/circuit/components/atoms/_port.py +398 -0
- digsim/circuit/components/ic/74162.json +1331 -0
- digsim/circuit/components/ic/7448.json +834 -0
- digsim/storage_model/__init__.py +7 -0
- digsim/storage_model/_app.py +58 -0
- digsim/storage_model/_circuit.py +126 -0
- digsim/synth/__init__.py +6 -0
- digsim/synth/__main__.py +67 -0
- digsim/synth/_synthesis.py +156 -0
- digsim/utils/__init__.py +6 -0
- digsim/utils/_yosys_netlist.py +134 -0
- digsim_logic_simulator-0.22.0.dist-info/METADATA +140 -0
- digsim_logic_simulator-0.22.0.dist-info/RECORD +107 -0
- digsim_logic_simulator-0.22.0.dist-info/WHEEL +5 -0
- digsim_logic_simulator-0.22.0.dist-info/entry_points.txt +2 -0
- digsim_logic_simulator-0.22.0.dist-info/licenses/LICENSE.md +32 -0
- digsim_logic_simulator-0.22.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""An application model for a GUI simulated circuit"""
|
|
5
|
+
|
|
6
|
+
import queue
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QThread, Signal
|
|
11
|
+
|
|
12
|
+
from digsim.app.gui_objects import ComponentObject
|
|
13
|
+
from digsim.circuit.components.atoms import Component
|
|
14
|
+
from digsim.storage_model import AppFileDataClass
|
|
15
|
+
|
|
16
|
+
from ._model_objects import ModelObjects
|
|
17
|
+
from ._model_settings import ModelSettings
|
|
18
|
+
from ._model_shortcuts import ModelShortcuts
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AppModel(QThread):
|
|
22
|
+
"""The application model class for a GUI simulated circuit"""
|
|
23
|
+
|
|
24
|
+
sig_audio_start = Signal(bool)
|
|
25
|
+
sig_audio_notify = Signal(Component)
|
|
26
|
+
sig_control_notify = Signal()
|
|
27
|
+
sig_sim_time_notify = Signal(float)
|
|
28
|
+
sig_synchronize_gui = Signal()
|
|
29
|
+
sig_repaint = Signal()
|
|
30
|
+
sig_update_wires = Signal()
|
|
31
|
+
sig_delete_component = Signal(ComponentObject)
|
|
32
|
+
sig_delete_wires = Signal()
|
|
33
|
+
sig_error = Signal(str)
|
|
34
|
+
sig_warning_log = Signal(str, str)
|
|
35
|
+
sig_zoom_in_gui = Signal()
|
|
36
|
+
sig_zoom_out_gui = Signal()
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__()
|
|
40
|
+
self._setup_model_components()
|
|
41
|
+
self._started = False
|
|
42
|
+
self._single_step = False
|
|
43
|
+
self._changed = False
|
|
44
|
+
self._gui_event_queue = queue.Queue()
|
|
45
|
+
self._multi_select = False
|
|
46
|
+
|
|
47
|
+
def _setup_model_components(self):
|
|
48
|
+
self._model_objects = ModelObjects(self)
|
|
49
|
+
self._model_shortcuts = ModelShortcuts(self)
|
|
50
|
+
self._model_settings = ModelSettings(self)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def objects(self):
|
|
54
|
+
"""return the model objects"""
|
|
55
|
+
return self._model_objects
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def shortcuts(self):
|
|
59
|
+
"""return the model shortcuts"""
|
|
60
|
+
return self._model_shortcuts
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def settings(self):
|
|
64
|
+
"""return the model settings"""
|
|
65
|
+
return self._model_settings
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_running(self):
|
|
69
|
+
"""Return True if the simulation thread is running"""
|
|
70
|
+
return self._started
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_changed(self):
|
|
74
|
+
"""Return True if there are changes in the model since last save"""
|
|
75
|
+
return self._changed
|
|
76
|
+
|
|
77
|
+
def _model_clear(self):
|
|
78
|
+
"""Clear model"""
|
|
79
|
+
self.objects.clear()
|
|
80
|
+
self.shortcuts.clear()
|
|
81
|
+
self._changed = False
|
|
82
|
+
|
|
83
|
+
def model_init(self):
|
|
84
|
+
"""(Re)initialize the model/circuit"""
|
|
85
|
+
self.objects.circuit.init()
|
|
86
|
+
self.objects.init()
|
|
87
|
+
self.sig_sim_time_notify.emit(0)
|
|
88
|
+
|
|
89
|
+
def model_start(self):
|
|
90
|
+
"""Start model simulation thread"""
|
|
91
|
+
self.model_abort_wire()
|
|
92
|
+
self._started = True
|
|
93
|
+
self._single_step = False
|
|
94
|
+
self.start()
|
|
95
|
+
self.sig_control_notify.emit()
|
|
96
|
+
|
|
97
|
+
def model_single_step(self):
|
|
98
|
+
"""Start model simulation thread"""
|
|
99
|
+
self._started = True
|
|
100
|
+
self._single_step = True
|
|
101
|
+
self.start()
|
|
102
|
+
self.sig_control_notify.emit()
|
|
103
|
+
|
|
104
|
+
def model_stop(self):
|
|
105
|
+
"""Stop model simulation thread"""
|
|
106
|
+
self._started = False
|
|
107
|
+
self.wait()
|
|
108
|
+
|
|
109
|
+
def model_reset(self):
|
|
110
|
+
"""Reset model simulation"""
|
|
111
|
+
if not self._started:
|
|
112
|
+
self.model_init()
|
|
113
|
+
|
|
114
|
+
def model_changed(self):
|
|
115
|
+
"""Set changed to True, for example when gui has moved component"""
|
|
116
|
+
self._changed = True
|
|
117
|
+
self.sig_control_notify.emit()
|
|
118
|
+
|
|
119
|
+
def model_add_event(self, func):
|
|
120
|
+
"""Add medel events (functions) from the GUI"""
|
|
121
|
+
self._gui_event_queue.put(func)
|
|
122
|
+
|
|
123
|
+
def model_abort_wire(self):
|
|
124
|
+
if self._model_objects.new_wire.ongoing():
|
|
125
|
+
self._model_objects.new_wire.abort()
|
|
126
|
+
self.sig_repaint.emit()
|
|
127
|
+
|
|
128
|
+
def run(self):
|
|
129
|
+
"""Simulation thread run function"""
|
|
130
|
+
start_time = time.perf_counter()
|
|
131
|
+
next_tick = start_time
|
|
132
|
+
sim_tick_ms = 1000 / self._model_settings.get("update_frequency")
|
|
133
|
+
real_time = self._model_settings.get("real_time")
|
|
134
|
+
self.sig_audio_start.emit(True)
|
|
135
|
+
while self._started:
|
|
136
|
+
next_tick += sim_tick_ms / 1000
|
|
137
|
+
|
|
138
|
+
# Execute one GUI event at a time
|
|
139
|
+
if not self._gui_event_queue.empty():
|
|
140
|
+
gui_event_func = self._gui_event_queue.get()
|
|
141
|
+
gui_event_func()
|
|
142
|
+
|
|
143
|
+
single_step_stop = self.objects.circuit.run(
|
|
144
|
+
ms=sim_tick_ms, single_step=self._single_step
|
|
145
|
+
)
|
|
146
|
+
if single_step_stop:
|
|
147
|
+
self._started = False
|
|
148
|
+
|
|
149
|
+
self.objects.components.update_callback_objects()
|
|
150
|
+
|
|
151
|
+
if not self._single_step and real_time:
|
|
152
|
+
now = time.perf_counter()
|
|
153
|
+
sleep_time = next_tick - now
|
|
154
|
+
sleep_time = max(0.01, sleep_time)
|
|
155
|
+
else:
|
|
156
|
+
sleep_time = 0.01 # Sleep a little, to be able to handle event
|
|
157
|
+
time.sleep(sleep_time)
|
|
158
|
+
|
|
159
|
+
self.sig_sim_time_notify.emit(self.objects.circuit.time_ns / 1000000000)
|
|
160
|
+
|
|
161
|
+
self._single_step = False
|
|
162
|
+
self.sig_control_notify.emit()
|
|
163
|
+
self.sig_audio_start.emit(False)
|
|
164
|
+
|
|
165
|
+
def save_circuit(self, path):
|
|
166
|
+
"""Save the circuit with GUI information"""
|
|
167
|
+
circuit_folder = str(Path(path).parent)
|
|
168
|
+
model_dataclass = self.objects.circuit_to_model(circuit_folder)
|
|
169
|
+
appfile_dataclass = AppFileDataClass(
|
|
170
|
+
circuit=model_dataclass.circuit,
|
|
171
|
+
gui=model_dataclass.gui,
|
|
172
|
+
shortcuts=self.shortcuts.to_dict(),
|
|
173
|
+
settings=self.settings.get_all(),
|
|
174
|
+
)
|
|
175
|
+
appfile_dataclass.save(path)
|
|
176
|
+
self._changed = False
|
|
177
|
+
self.sig_control_notify.emit()
|
|
178
|
+
|
|
179
|
+
def load_circuit(self, path):
|
|
180
|
+
"""Load a circuit with GUI information"""
|
|
181
|
+
self._model_clear()
|
|
182
|
+
app_file_dc = AppFileDataClass.load(path)
|
|
183
|
+
circuit_folder = str(Path(path).parent)
|
|
184
|
+
if len(circuit_folder) == 0:
|
|
185
|
+
circuit_folder = "."
|
|
186
|
+
exception_str_list = self.objects.model_to_circuit(app_file_dc, circuit_folder)
|
|
187
|
+
self.shortcuts.from_dict(app_file_dc.shortcuts)
|
|
188
|
+
self.settings.from_dict(app_file_dc.settings)
|
|
189
|
+
self.model_init()
|
|
190
|
+
self._changed = False
|
|
191
|
+
self.objects.reset_undo_stack()
|
|
192
|
+
self.sig_synchronize_gui.emit()
|
|
193
|
+
self.sig_control_notify.emit()
|
|
194
|
+
if len(exception_str_list) > 0:
|
|
195
|
+
self.sig_warning_log.emit("Load Circuit Warning", "\n".join(exception_str_list))
|
|
196
|
+
|
|
197
|
+
def clear_circuit(self):
|
|
198
|
+
"""Clear the circuit"""
|
|
199
|
+
self.objects.push_undo_state()
|
|
200
|
+
self._model_clear()
|
|
201
|
+
self.sig_synchronize_gui.emit()
|
|
202
|
+
self.sig_control_notify.emit()
|
|
203
|
+
|
|
204
|
+
def zoom_in(self):
|
|
205
|
+
"""Send zoom in signal to GUI"""
|
|
206
|
+
self.sig_zoom_in_gui.emit()
|
|
207
|
+
|
|
208
|
+
def zoom_out(self):
|
|
209
|
+
"""Send zoom out signal to GUI"""
|
|
210
|
+
self.sig_zoom_out_gui.emit()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Handle component objects in the model"""
|
|
5
|
+
|
|
6
|
+
import digsim.circuit.components
|
|
7
|
+
from digsim.app.gui_objects import ComponentObject
|
|
8
|
+
from digsim.circuit.components import Buzzer
|
|
9
|
+
from digsim.circuit.components.atoms import CallbackComponent
|
|
10
|
+
from digsim.storage_model import GuiPositionDataClass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelComponents:
|
|
14
|
+
"""Class to handle the component objects in the model"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def is_component_object(obj):
|
|
18
|
+
"""Test if this object is a component object"""
|
|
19
|
+
return isinstance(obj, ComponentObject)
|
|
20
|
+
|
|
21
|
+
def __init__(self, app_model, circuit):
|
|
22
|
+
self._app_model = app_model
|
|
23
|
+
self._circuit = circuit
|
|
24
|
+
self._component_objects = {}
|
|
25
|
+
self._component_callback_list = []
|
|
26
|
+
|
|
27
|
+
def clear(self):
|
|
28
|
+
"""Clear components objects"""
|
|
29
|
+
self._component_objects = {}
|
|
30
|
+
|
|
31
|
+
def init(self):
|
|
32
|
+
"""Initialize components objects"""
|
|
33
|
+
self._app_model.sig_repaint.emit()
|
|
34
|
+
|
|
35
|
+
def get_dict(self):
|
|
36
|
+
"""Get component objects dict"""
|
|
37
|
+
return self._component_objects
|
|
38
|
+
|
|
39
|
+
def is_empty(self):
|
|
40
|
+
"""Return True if there are component objects in the model"""
|
|
41
|
+
return len(self._component_objects) == 0
|
|
42
|
+
|
|
43
|
+
def get_top_zlevel(self):
|
|
44
|
+
"""Get thehighest z level in the model"""
|
|
45
|
+
max_zlevel = None
|
|
46
|
+
for _, comp_object in self._component_objects.items():
|
|
47
|
+
max_zlevel = (
|
|
48
|
+
comp_object.zlevel if max_zlevel is None else (max(max_zlevel, comp_object.zlevel))
|
|
49
|
+
)
|
|
50
|
+
return max_zlevel
|
|
51
|
+
|
|
52
|
+
def component_moved(self):
|
|
53
|
+
"""Call when component has moved to update state"""
|
|
54
|
+
self._app_model.objects.push_undo_state()
|
|
55
|
+
self._app_model.model_changed()
|
|
56
|
+
|
|
57
|
+
def bring_to_front(self, component_object):
|
|
58
|
+
"""Make the component object the highest in the stack"""
|
|
59
|
+
max_zlevel = self.get_top_zlevel()
|
|
60
|
+
component_object.zlevel = max_zlevel + 1
|
|
61
|
+
self._app_model.model_changed()
|
|
62
|
+
|
|
63
|
+
def send_to_back(self, component_object):
|
|
64
|
+
"""Make the component object the lowest in the stack"""
|
|
65
|
+
min_zlevel = None
|
|
66
|
+
for _, comp_object in self._component_objects.items():
|
|
67
|
+
min_zlevel = (
|
|
68
|
+
comp_object.zlevel if min_zlevel is None else (min(min_zlevel, comp_object.zlevel))
|
|
69
|
+
)
|
|
70
|
+
if min_zlevel == 0:
|
|
71
|
+
for _, comp_object in self._component_objects.items():
|
|
72
|
+
comp_object.zlevel = comp_object.zlevel + 1
|
|
73
|
+
component_object.zlevel = 0
|
|
74
|
+
else:
|
|
75
|
+
component_object.zlevel = min_zlevel - 1
|
|
76
|
+
self._app_model.model_changed()
|
|
77
|
+
|
|
78
|
+
def update_callback_objects(self):
|
|
79
|
+
"""
|
|
80
|
+
Update the GUI for the components that have changed since the last call
|
|
81
|
+
"""
|
|
82
|
+
if len(self._component_callback_list) == 0:
|
|
83
|
+
return
|
|
84
|
+
for comp in self._component_callback_list:
|
|
85
|
+
if isinstance(comp, Buzzer):
|
|
86
|
+
self._app_model.sig_audio_notify.emit(comp)
|
|
87
|
+
self._app_model.sig_repaint.emit()
|
|
88
|
+
self._component_callback_list = []
|
|
89
|
+
|
|
90
|
+
def _component_callback(self, component):
|
|
91
|
+
"""Add component to callback list (if not available)"""
|
|
92
|
+
if component not in self._component_callback_list:
|
|
93
|
+
self._component_callback_list.append(component)
|
|
94
|
+
|
|
95
|
+
def _get_component_class(self, name):
|
|
96
|
+
return getattr(digsim.circuit.components, name)
|
|
97
|
+
|
|
98
|
+
def get_object_parameters(self, name):
|
|
99
|
+
"""Get parameters for a component"""
|
|
100
|
+
return self._get_component_class(name).get_parameters()
|
|
101
|
+
|
|
102
|
+
def _add_object(self, component, xpos, ypos):
|
|
103
|
+
"""Add component object in position"""
|
|
104
|
+
component_object_class = digsim.app.gui_objects.class_factory(type(component).__name__)
|
|
105
|
+
self._component_objects[component] = component_object_class(
|
|
106
|
+
self._app_model, component, xpos, ypos
|
|
107
|
+
)
|
|
108
|
+
if isinstance(component, CallbackComponent):
|
|
109
|
+
component.set_callback(self._component_callback)
|
|
110
|
+
return self._component_objects[component]
|
|
111
|
+
|
|
112
|
+
def add_object_by_name(self, name, pos, settings):
|
|
113
|
+
"""Add component object from class name"""
|
|
114
|
+
self._app_model.objects.push_undo_state()
|
|
115
|
+
component_class = self._get_component_class(name)
|
|
116
|
+
component = component_class(self._circuit, **settings)
|
|
117
|
+
self._app_model.model_init()
|
|
118
|
+
component_object = self._add_object(component, pos.x(), pos.y())
|
|
119
|
+
self._app_model.model_changed()
|
|
120
|
+
return component_object
|
|
121
|
+
|
|
122
|
+
def get_object(self, component):
|
|
123
|
+
"""Get component object (from component)"""
|
|
124
|
+
return self._component_objects[component]
|
|
125
|
+
|
|
126
|
+
def get_object_list(self):
|
|
127
|
+
"""Get list of component objects"""
|
|
128
|
+
return list(self._component_objects.values())
|
|
129
|
+
|
|
130
|
+
def update_settings(self, component_object, settings):
|
|
131
|
+
"""Update settings for a component"""
|
|
132
|
+
self._app_model.objects.push_undo_state()
|
|
133
|
+
component_object.component.update_settings(settings)
|
|
134
|
+
self._app_model.model_changed()
|
|
135
|
+
# Settings can change the component size
|
|
136
|
+
component_object.update_size()
|
|
137
|
+
self._app_model.sig_repaint.emit()
|
|
138
|
+
|
|
139
|
+
def update_sizes(self):
|
|
140
|
+
"""Update all component sizes"""
|
|
141
|
+
for comp_object in self.get_object_list():
|
|
142
|
+
comp_object.update_size()
|
|
143
|
+
|
|
144
|
+
def delete(self, component_object):
|
|
145
|
+
"""Delete a component object in the model"""
|
|
146
|
+
del self._component_objects[component_object.component]
|
|
147
|
+
self._circuit.delete_component(component_object.component)
|
|
148
|
+
self._app_model.sig_delete_component.emit(component_object)
|
|
149
|
+
|
|
150
|
+
def add_gui_positions(self, gui_dc_dict):
|
|
151
|
+
"""Create model components from circuit_dict"""
|
|
152
|
+
for comp in self._circuit.get_toplevel_components():
|
|
153
|
+
gui_dc = gui_dc_dict.get(comp.name(), GuiPositionDataClass())
|
|
154
|
+
component_object = self._add_object(comp, gui_dc.x, gui_dc.y)
|
|
155
|
+
component_object.zlevel = gui_dc.z
|
|
156
|
+
|
|
157
|
+
def get_gui_dict(self):
|
|
158
|
+
"""Return gui dict from component objects"""
|
|
159
|
+
gui_dict = {}
|
|
160
|
+
for comp, comp_object in self.get_dict().items():
|
|
161
|
+
gui_dict[comp.name()] = comp_object.to_gui_dataclass()
|
|
162
|
+
return gui_dict
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Handle new wire object in the model"""
|
|
5
|
+
|
|
6
|
+
from digsim.circuit.components.atoms import PortConnectionError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NewWire:
|
|
10
|
+
"""Class to handle functionality for making a new wire"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, app_model):
|
|
13
|
+
self._app_model = app_model
|
|
14
|
+
self._start_port = None
|
|
15
|
+
self._end_pos = None
|
|
16
|
+
|
|
17
|
+
def start_port(self):
|
|
18
|
+
"""Get start port for unfinished new wire object"""
|
|
19
|
+
return self._start_port
|
|
20
|
+
|
|
21
|
+
def end_pos(self):
|
|
22
|
+
"""Get end point for unfinished new wire object"""
|
|
23
|
+
return self._end_pos
|
|
24
|
+
|
|
25
|
+
def start(self, component, portname):
|
|
26
|
+
"""Start new wire object"""
|
|
27
|
+
if component.port(portname).can_add_wire():
|
|
28
|
+
self._start_port = component.port(portname)
|
|
29
|
+
|
|
30
|
+
def end(self, component, portname):
|
|
31
|
+
"""End new wire object"""
|
|
32
|
+
end_port = component.port(portname)
|
|
33
|
+
if self._start_port.is_output() and end_port.is_input():
|
|
34
|
+
self._app_model.objects.push_undo_state()
|
|
35
|
+
self._start_port.wire = end_port
|
|
36
|
+
elif self._start_port.is_input() and end_port.is_output():
|
|
37
|
+
self._app_model.objects.push_undo_state()
|
|
38
|
+
end_port.wire = self._start_port
|
|
39
|
+
else:
|
|
40
|
+
raise PortConnectionError("Cannot connect to port of same type")
|
|
41
|
+
self._app_model.sig_update_wires.emit()
|
|
42
|
+
self._app_model.model_changed()
|
|
43
|
+
self._start_port = None
|
|
44
|
+
self._end_pos = None
|
|
45
|
+
|
|
46
|
+
def abort(self):
|
|
47
|
+
"""Abort new wire object"""
|
|
48
|
+
self._start_port = None
|
|
49
|
+
self._end_pos = None
|
|
50
|
+
|
|
51
|
+
def set_end_pos(self, pos):
|
|
52
|
+
"""Update end point for unfinished new wire object"""
|
|
53
|
+
self._end_pos = pos
|
|
54
|
+
|
|
55
|
+
def ongoing(self):
|
|
56
|
+
"""Return True if an unfinished new wire object is active"""
|
|
57
|
+
return self._start_port is not None
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Handle objects in the model"""
|
|
5
|
+
|
|
6
|
+
from digsim.circuit import Circuit
|
|
7
|
+
from digsim.circuit.components.atoms import DigsimException
|
|
8
|
+
from digsim.storage_model import AppFileDataClass, ModelDataClass
|
|
9
|
+
|
|
10
|
+
from ._model_components import ModelComponents
|
|
11
|
+
from ._model_new_wire import NewWire
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelObjects:
|
|
15
|
+
"""Class to handle objects in the model"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, app_model):
|
|
18
|
+
self._app_model = app_model
|
|
19
|
+
self._circuit = Circuit(name="DigSimCircuit")
|
|
20
|
+
self._model_components = ModelComponents(app_model, self._circuit)
|
|
21
|
+
self._undo_stack = []
|
|
22
|
+
self._redo_stack = []
|
|
23
|
+
self._new_wire = NewWire(self._app_model)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def circuit(self):
|
|
27
|
+
"""return the model circuit"""
|
|
28
|
+
return self._circuit
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def components(self):
|
|
32
|
+
"""return the model components"""
|
|
33
|
+
return self._model_components
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def new_wire(self):
|
|
37
|
+
"""return the model components"""
|
|
38
|
+
return self._new_wire
|
|
39
|
+
|
|
40
|
+
def init(self):
|
|
41
|
+
"""Initialize objects"""
|
|
42
|
+
self._model_components.init()
|
|
43
|
+
|
|
44
|
+
def clear(self):
|
|
45
|
+
"""Clear components and wires"""
|
|
46
|
+
self._model_components.clear()
|
|
47
|
+
self._circuit.clear()
|
|
48
|
+
|
|
49
|
+
def get_list(self):
|
|
50
|
+
"""Get list of all model objects"""
|
|
51
|
+
model_objects = self._model_components.get_object_list()
|
|
52
|
+
return model_objects
|
|
53
|
+
|
|
54
|
+
def get_selected(self):
|
|
55
|
+
"""Get selected objects"""
|
|
56
|
+
return [obj for obj in self.get_list() if obj.selected]
|
|
57
|
+
|
|
58
|
+
def _delete(self, selected_objects):
|
|
59
|
+
for obj in selected_objects:
|
|
60
|
+
if ModelComponents.is_component_object(obj):
|
|
61
|
+
self._model_components.delete(obj)
|
|
62
|
+
|
|
63
|
+
def delete_selected(self):
|
|
64
|
+
"""Delete selected object(s)"""
|
|
65
|
+
self.push_undo_state()
|
|
66
|
+
selected_objects = self._app_model.objects.get_selected()
|
|
67
|
+
if len(selected_objects) > 0:
|
|
68
|
+
self._delete(selected_objects)
|
|
69
|
+
self._app_model.model_changed()
|
|
70
|
+
self._app_model.sig_delete_wires.emit()
|
|
71
|
+
|
|
72
|
+
def model_to_circuit(self, model_dc, circuit_folder):
|
|
73
|
+
if isinstance(model_dc, AppFileDataClass):
|
|
74
|
+
# Loaded model
|
|
75
|
+
dc = ModelDataClass.from_app_file_dc(model_dc)
|
|
76
|
+
else:
|
|
77
|
+
dc = model_dc
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Create circuit
|
|
81
|
+
exception_str_list = self.circuit.from_dataclass(
|
|
82
|
+
dc.circuit,
|
|
83
|
+
circuit_folder,
|
|
84
|
+
component_exceptions=False,
|
|
85
|
+
connect_exceptions=False,
|
|
86
|
+
)
|
|
87
|
+
# Add component positions
|
|
88
|
+
self.components.add_gui_positions(dc.gui)
|
|
89
|
+
except DigsimException as exc:
|
|
90
|
+
self.sig_error.emit(f"Circuit error: {str(exc)}")
|
|
91
|
+
return exception_str_list
|
|
92
|
+
return exception_str_list
|
|
93
|
+
|
|
94
|
+
def circuit_to_model(self, circuit_folder):
|
|
95
|
+
model_dc = ModelDataClass(
|
|
96
|
+
circuit=self.circuit.to_dataclass(circuit_folder), gui=self.components.get_gui_dict()
|
|
97
|
+
)
|
|
98
|
+
return model_dc
|
|
99
|
+
|
|
100
|
+
def _restore_state(self, model_dc):
|
|
101
|
+
self.clear()
|
|
102
|
+
exception_str_list = self.model_to_circuit(model_dc, None)
|
|
103
|
+
self._app_model.model_init()
|
|
104
|
+
self._app_model.model_changed()
|
|
105
|
+
if len(exception_str_list) > 0:
|
|
106
|
+
self.sig_warning_log.emit("Load Circuit Warning", "\n".join(exception_str_list))
|
|
107
|
+
|
|
108
|
+
def reset_undo_stack(self):
|
|
109
|
+
"""Clear undo/redo stacks"""
|
|
110
|
+
self._undo_stack = []
|
|
111
|
+
self._redo_stack = []
|
|
112
|
+
self._app_model.sig_control_notify.emit()
|
|
113
|
+
|
|
114
|
+
def push_undo_state(self, clear_redo_stack=True):
|
|
115
|
+
"""Push undo state to stack"""
|
|
116
|
+
self._undo_stack.append(self.circuit_to_model("/"))
|
|
117
|
+
if clear_redo_stack:
|
|
118
|
+
self._redo_stack = []
|
|
119
|
+
self._app_model.sig_control_notify.emit()
|
|
120
|
+
|
|
121
|
+
def drop_undo_state(self):
|
|
122
|
+
"""Drop last undo state"""
|
|
123
|
+
if len(self._undo_stack) > 0:
|
|
124
|
+
self._undo_stack.pop()
|
|
125
|
+
self._app_model.sig_control_notify.emit()
|
|
126
|
+
|
|
127
|
+
def push_redo_state(self):
|
|
128
|
+
"""Push redo state to stack"""
|
|
129
|
+
self._redo_stack.append(self.circuit_to_model("/"))
|
|
130
|
+
|
|
131
|
+
def undo(self):
|
|
132
|
+
"""Undo to last saved state"""
|
|
133
|
+
if len(self._undo_stack) > 0:
|
|
134
|
+
self.push_redo_state()
|
|
135
|
+
model_dc = self._undo_stack.pop()
|
|
136
|
+
self._restore_state(model_dc)
|
|
137
|
+
self._app_model.sig_control_notify.emit()
|
|
138
|
+
self._app_model.sig_synchronize_gui.emit()
|
|
139
|
+
|
|
140
|
+
def redo(self):
|
|
141
|
+
"""Undo to last saved state"""
|
|
142
|
+
if len(self._redo_stack) > 0:
|
|
143
|
+
self.push_undo_state(clear_redo_stack=False)
|
|
144
|
+
model_dc = self._redo_stack.pop()
|
|
145
|
+
self._restore_state(model_dc)
|
|
146
|
+
self._app_model.sig_control_notify.emit()
|
|
147
|
+
self._app_model.sig_synchronize_gui.emit()
|
|
148
|
+
|
|
149
|
+
def can_undo(self):
|
|
150
|
+
"""Return true if the undo stack is not empty"""
|
|
151
|
+
return len(self._undo_stack) > 0
|
|
152
|
+
|
|
153
|
+
def can_redo(self):
|
|
154
|
+
"""Return true if the undo stack is not empty"""
|
|
155
|
+
return len(self._redo_stack) > 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Handle settings in the model"""
|
|
5
|
+
|
|
6
|
+
from digsim.app.settings import GuiSettingsDialog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelSettings:
|
|
10
|
+
"""Class to handle settings in the model"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, app_model):
|
|
13
|
+
self._app_model = app_model
|
|
14
|
+
self._settings = GuiSettingsDialog.default_settings()
|
|
15
|
+
|
|
16
|
+
def update(self, settings):
|
|
17
|
+
"""Update model settings"""
|
|
18
|
+
self._settings.update(settings)
|
|
19
|
+
self._app_model.model_changed()
|
|
20
|
+
# Settings can change the component sizes
|
|
21
|
+
self._app_model.objects.components.update_sizes()
|
|
22
|
+
self._app_model.sig_repaint.emit()
|
|
23
|
+
|
|
24
|
+
def get_all(self):
|
|
25
|
+
"""Return settings dict"""
|
|
26
|
+
return self._settings.copy()
|
|
27
|
+
|
|
28
|
+
def from_dict(self, circuit_dict):
|
|
29
|
+
"""Get settings from circuit dict"""
|
|
30
|
+
for key, data in circuit_dict.items():
|
|
31
|
+
self._settings[key] = data
|
|
32
|
+
|
|
33
|
+
def get(self, key):
|
|
34
|
+
"""Get model setting"""
|
|
35
|
+
return self._settings.get(key)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Handle shortcuts in the model"""
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelShortcuts:
|
|
10
|
+
"""class to handle key shortcuts in the model"""
|
|
11
|
+
|
|
12
|
+
QT_KEY_TO_KEY = {
|
|
13
|
+
Qt.Key_0: "0",
|
|
14
|
+
Qt.Key_1: "1",
|
|
15
|
+
Qt.Key_2: "2",
|
|
16
|
+
Qt.Key_3: "3",
|
|
17
|
+
Qt.Key_4: "4",
|
|
18
|
+
Qt.Key_5: "5",
|
|
19
|
+
Qt.Key_6: "6",
|
|
20
|
+
Qt.Key_7: "7",
|
|
21
|
+
Qt.Key_8: "8",
|
|
22
|
+
Qt.Key_9: "9",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __init__(self, app_model):
|
|
26
|
+
self._app_model = app_model
|
|
27
|
+
self._shortcut_component = {}
|
|
28
|
+
|
|
29
|
+
def clear(self):
|
|
30
|
+
"""Clear shortcuts"""
|
|
31
|
+
self._shortcut_component = {}
|
|
32
|
+
|
|
33
|
+
def set_component(self, key, component):
|
|
34
|
+
"""Set shortcut"""
|
|
35
|
+
self._shortcut_component[key] = component
|
|
36
|
+
|
|
37
|
+
def get_component(self, key):
|
|
38
|
+
"""Get shortcut"""
|
|
39
|
+
return self._shortcut_component.get(key)
|
|
40
|
+
|
|
41
|
+
def press(self, qtkey):
|
|
42
|
+
"""Handle shortcut keypress"""
|
|
43
|
+
key = self.QT_KEY_TO_KEY.get(qtkey)
|
|
44
|
+
if key is None:
|
|
45
|
+
return
|
|
46
|
+
component = self.get_component(key)
|
|
47
|
+
if component is not None:
|
|
48
|
+
self._app_model.model_add_event(component.onpress)
|
|
49
|
+
|
|
50
|
+
def release(self, qtkey):
|
|
51
|
+
"""Handle shortcut keyrelease"""
|
|
52
|
+
key = self.QT_KEY_TO_KEY.get(qtkey)
|
|
53
|
+
if key is None:
|
|
54
|
+
return
|
|
55
|
+
component = self.get_component(key)
|
|
56
|
+
if component is not None:
|
|
57
|
+
self._app_model.model_add_event(component.onrelease)
|
|
58
|
+
|
|
59
|
+
def to_dict(self):
|
|
60
|
+
"""Generate dict from shortcuts"""
|
|
61
|
+
shortcuts_dict = {}
|
|
62
|
+
for key, component in self._shortcut_component.items():
|
|
63
|
+
shortcuts_dict[key] = component.name()
|
|
64
|
+
return shortcuts_dict
|
|
65
|
+
|
|
66
|
+
def from_dict(self, shortcuts_dict):
|
|
67
|
+
"""Generate shortcuts from dict"""
|
|
68
|
+
self.clear()
|
|
69
|
+
if shortcuts_dict is not None:
|
|
70
|
+
for key, component_name in shortcuts_dict.items():
|
|
71
|
+
component = self._app_model.objects.circuit.get_component(component_name)
|
|
72
|
+
self.set_component(key, component)
|