RCAIDE-GUI 1.0.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.
- common_widgets/__init__.py +6 -0
- common_widgets/animated_toggle.py +141 -0
- common_widgets/color.py +17 -0
- common_widgets/data_entry_widget.py +337 -0
- common_widgets/image_widget.py +21 -0
- common_widgets/unit_picker_widget.py +48 -0
- main.py +168 -0
- rcaide_gui-1.0.0.dist-info/METADATA +96 -0
- rcaide_gui-1.0.0.dist-info/RECORD +170 -0
- rcaide_gui-1.0.0.dist-info/WHEEL +5 -0
- rcaide_gui-1.0.0.dist-info/entry_points.txt +2 -0
- rcaide_gui-1.0.0.dist-info/top_level.txt +5 -0
- rcaide_io.py +818 -0
- tabs/__init__.py +10 -0
- tabs/aircraft_configs/__init__.py +3 -0
- tabs/aircraft_configs/aircraft_configs.py +301 -0
- tabs/analysis/__init__.py +4 -0
- tabs/analysis/analysis.py +92 -0
- tabs/analysis/widgets/__init__.py +13 -0
- tabs/analysis/widgets/aeroacoustics_widget.py +158 -0
- tabs/analysis/widgets/aerodynamics_widget.py +108 -0
- tabs/analysis/widgets/analysis_data_widget.py +29 -0
- tabs/analysis/widgets/atmosphere_widget.py +49 -0
- tabs/analysis/widgets/costs_widget.py +35 -0
- tabs/analysis/widgets/energy_widget.py +32 -0
- tabs/analysis/widgets/geometry_widget.py +55 -0
- tabs/analysis/widgets/planets_widget.py +34 -0
- tabs/analysis/widgets/propulsion_widget.py +53 -0
- tabs/analysis/widgets/stability_widget.py +34 -0
- tabs/analysis/widgets/weights_widget.py +94 -0
- tabs/geometry/__init__.py +6 -0
- tabs/geometry/aircraft_configs.py +232 -0
- tabs/geometry/frames/__init__.py +15 -0
- tabs/geometry/frames/booms/__init__.py +5 -0
- tabs/geometry/frames/booms/boom_frame.py +238 -0
- tabs/geometry/frames/cargo_bays/__init__.py +5 -0
- tabs/geometry/frames/cargo_bays/cargo_bay_frame.py +159 -0
- tabs/geometry/frames/default_frame.py +23 -0
- tabs/geometry/frames/energy_network/__init__.py +2 -0
- tabs/geometry/frames/energy_network/turbofan_network/__init__.py +3 -0
- tabs/geometry/frames/energy_network/turbofan_network/frames/__init__.py +1 -0
- tabs/geometry/frames/energy_network/turbofan_network/widgets/__init__.py +1 -0
- tabs/geometry/frames/fuselages/__init__.py +5 -0
- tabs/geometry/frames/fuselages/fuselage_frame.py +294 -0
- tabs/geometry/frames/geometry_frame.py +91 -0
- tabs/geometry/frames/landing_gears/__init__.py +5 -0
- tabs/geometry/frames/landing_gears/landing_gear_frame.py +189 -0
- tabs/geometry/frames/nacelles/__init__.py +1 -0
- tabs/geometry/frames/powertrain/__init__.py +6 -0
- tabs/geometry/frames/powertrain/converters/__init__.py +5 -0
- tabs/geometry/frames/powertrain/converters/converter_frame.py +137 -0
- tabs/geometry/frames/powertrain/distributors/__init__.py +5 -0
- tabs/geometry/frames/powertrain/distributors/distributor_frame.py +143 -0
- tabs/geometry/frames/powertrain/nacelles/__init__.py +5 -0
- tabs/geometry/frames/powertrain/nacelles/nacelle_frame.py +283 -0
- tabs/geometry/frames/powertrain/powertrain_frame.py +259 -0
- tabs/geometry/frames/powertrain/propulsors/__init__.py +5 -0
- tabs/geometry/frames/powertrain/propulsors/propulsor_frame.py +146 -0
- tabs/geometry/frames/powertrain/sources/__init__.py +6 -0
- tabs/geometry/frames/powertrain/sources/energy_source_frame.py +147 -0
- tabs/geometry/frames/vehicle_frame.py +171 -0
- tabs/geometry/frames/wings/__init__.py +5 -0
- tabs/geometry/frames/wings/wings_frame.py +457 -0
- tabs/geometry/geometry.py +513 -0
- tabs/geometry/widgets/__init__.py +12 -0
- tabs/geometry/widgets/booms/__init__.py +6 -0
- tabs/geometry/widgets/booms/boom_section_widget.py +109 -0
- tabs/geometry/widgets/cargo_bays/__init__.py +1 -0
- tabs/geometry/widgets/fuselages/__init__.py +8 -0
- tabs/geometry/widgets/fuselages/cabin_class_widget.py +188 -0
- tabs/geometry/widgets/fuselages/cabin_widget.py +217 -0
- tabs/geometry/widgets/fuselages/fuselage_section_widget.py +190 -0
- tabs/geometry/widgets/geometry_data_widget.py +25 -0
- tabs/geometry/widgets/landing_gears/__init__.py +6 -0
- tabs/geometry/widgets/nacelles/__init__.py +1 -0
- tabs/geometry/widgets/powertrain/__init__.py +9 -0
- tabs/geometry/widgets/powertrain/converters/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/converters/turboelectric_generator_widget.py +74 -0
- tabs/geometry/widgets/powertrain/distributors/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/distributors/fuel_line_widget.py +81 -0
- tabs/geometry/widgets/powertrain/modulators/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/modulators/esc_widget.py +58 -0
- tabs/geometry/widgets/powertrain/nacelles/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/nacelles/nacelle_section_widget.py +115 -0
- tabs/geometry/widgets/powertrain/powertrain_connector_widget.py +136 -0
- tabs/geometry/widgets/powertrain/powertrain_widget.py +206 -0
- tabs/geometry/widgets/powertrain/propulsors/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/propulsors/turbofan_widget.py +462 -0
- tabs/geometry/widgets/powertrain/sources/__init__.py +6 -0
- tabs/geometry/widgets/powertrain/sources/fuel_tank_widget.py +103 -0
- tabs/geometry/widgets/powertrain/sources/source_selector_widget.py +35 -0
- tabs/geometry/widgets/wings/__init__.py +7 -0
- tabs/geometry/widgets/wings/wing_cs_widget.py +198 -0
- tabs/geometry/widgets/wings/wing_section_widget.py +298 -0
- tabs/home/__init__.py +3 -0
- tabs/home/home.py +585 -0
- tabs/mission/__init__.py +4 -0
- tabs/mission/mission.py +1124 -0
- tabs/mission/widgets/__init__.py +3 -0
- tabs/mission/widgets/flight_controls_widget.py +186 -0
- tabs/mission/widgets/mission_analysis_widget.py +166 -0
- tabs/mission/widgets/mission_segment_helper.py +358 -0
- tabs/mission/widgets/mission_segment_widget.py +358 -0
- tabs/solve/__init__.py +4 -0
- tabs/solve/plots/__init__.py +12 -0
- tabs/solve/plots/aeroacoustics/__init__.py +6 -0
- tabs/solve/plots/aeroacoustics/plot_2D_noise_contour.py +184 -0
- tabs/solve/plots/aeroacoustics/plot_3D_noise_contour.py +219 -0
- tabs/solve/plots/aeroacoustics/plot_noise_certification_contour.py +115 -0
- tabs/solve/plots/aeroacoustics/plot_noise_level.py +118 -0
- tabs/solve/plots/aerodynamics/__init__.py +10 -0
- tabs/solve/plots/aerodynamics/plot_aerodynamic_coefficients.py +156 -0
- tabs/solve/plots/aerodynamics/plot_aerodynamic_forces.py +155 -0
- tabs/solve/plots/aerodynamics/plot_aircraft_aerodynamics.py +136 -0
- tabs/solve/plots/aerodynamics/plot_drag_components.py +157 -0
- tabs/solve/plots/aerodynamics/plot_lift_distribution.py +107 -0
- tabs/solve/plots/aerodynamics/plot_rotor_conditions.py +255 -0
- tabs/solve/plots/aerodynamics/plot_rotor_disc_performance.py +144 -0
- tabs/solve/plots/aerodynamics/plot_rotor_performance.py +124 -0
- tabs/solve/plots/common/__init__.py +4 -0
- tabs/solve/plots/common/plot_style.py +71 -0
- tabs/solve/plots/common/set_axes.py +55 -0
- tabs/solve/plots/create_plot_widgets.py +34 -0
- tabs/solve/plots/emissions/__init__.py +3 -0
- tabs/solve/plots/emissions/plot_emissions.py +167 -0
- tabs/solve/plots/energy/__init__.py +12 -0
- tabs/solve/plots/energy/plot_altitude_sfc_weight.py +134 -0
- tabs/solve/plots/energy/plot_battery_cell_conditions.py +154 -0
- tabs/solve/plots/energy/plot_battery_degradation.py +157 -0
- tabs/solve/plots/energy/plot_battery_module_C_rates.py +139 -0
- tabs/solve/plots/energy/plot_battery_module_conditions.py +157 -0
- tabs/solve/plots/energy/plot_battery_pack_conditions.py +164 -0
- tabs/solve/plots/energy/plot_battery_ragone_diagram.py +97 -0
- tabs/solve/plots/energy/plot_battery_temperature.py +136 -0
- tabs/solve/plots/energy/plot_electric_propulsor_efficiencies.py +136 -0
- tabs/solve/plots/energy/plot_propulsor_throttles.py +119 -0
- tabs/solve/plots/mission/__init__.py +5 -0
- tabs/solve/plots/mission/plot_aircraft_velocities.py +145 -0
- tabs/solve/plots/mission/plot_flight_conditions.py +236 -0
- tabs/solve/plots/mission/plot_flight_trajectory.py +172 -0
- tabs/solve/plots/stability/__init__.py +5 -0
- tabs/solve/plots/stability/plot_flight_forces_and_moments.py +163 -0
- tabs/solve/plots/stability/plot_lateral_stability.py +151 -0
- tabs/solve/plots/stability/plot_longitudinal_stability.py +173 -0
- tabs/solve/plots/thermal_management/__init__.py +7 -0
- tabs/solve/plots/thermal_management/plot_air_cooled_conditions.py +152 -0
- tabs/solve/plots/thermal_management/plot_cross_flow_heat_exchanger_conditions.py +185 -0
- tabs/solve/plots/thermal_management/plot_reservoir_conditions.py +141 -0
- tabs/solve/plots/thermal_management/plot_thermal_management_performance.py +105 -0
- tabs/solve/plots/thermal_management/plot_wavy_channel_conditions.py +161 -0
- tabs/solve/plots/weights/__init__.py +4 -0
- tabs/solve/plots/weights/plot_load_diagram.py +163 -0
- tabs/solve/plots/weights/plot_weight_breakdown.py +164 -0
- tabs/solve/solve.py +1498 -0
- tabs/style_sheet.py +114 -0
- tabs/tab_widget.py +14 -0
- tabs/visualize_geometry/__init__.py +4 -0
- tabs/visualize_geometry/core_3d_viewer.py +253 -0
- tabs/visualize_geometry/features/__init__.py +10 -0
- tabs/visualize_geometry/features/axes_gizmo.py +173 -0
- tabs/visualize_geometry/features/background.py +161 -0
- tabs/visualize_geometry/features/blueprint.py +685 -0
- tabs/visualize_geometry/features/camera.py +364 -0
- tabs/visualize_geometry/features/drag_aircraft.py +219 -0
- tabs/visualize_geometry/features/grid.py +194 -0
- tabs/visualize_geometry/features/measurement.py +367 -0
- tabs/visualize_geometry/features/screenshot.py +91 -0
- tabs/visualize_geometry/geometry_helper_functions.py +3 -0
- tabs/visualize_geometry/visualize_geometry.py +731 -0
- utilities.py +257 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# RCAIDE_GUI/common_widgets/animated_toggle.py
|
|
2
|
+
|
|
3
|
+
# PyQt imports
|
|
4
|
+
from PyQt6.QtCore import (
|
|
5
|
+
Qt, QSize, QPoint, QPointF, QRectF,
|
|
6
|
+
QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
|
|
7
|
+
pyqtSlot, pyqtProperty)
|
|
8
|
+
from PyQt6.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter
|
|
9
|
+
from PyQt6.QtWidgets import QCheckBox
|
|
10
|
+
|
|
11
|
+
# ------------------------------------------------------------------------------
|
|
12
|
+
# Animated Toggle Widget
|
|
13
|
+
# ------------------------------------------------------------------------------
|
|
14
|
+
class AnimatedToggle(QCheckBox):
|
|
15
|
+
_transparent_pen = QPen(Qt.GlobalColor.transparent)
|
|
16
|
+
_light_grey_pen = QPen(Qt.GlobalColor.lightGray)
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
parent=None,
|
|
20
|
+
bar_color=Qt.GlobalColor.gray,
|
|
21
|
+
checked_color="#00B0FF",
|
|
22
|
+
handle_color=Qt.GlobalColor.white,
|
|
23
|
+
pulse_unchecked_color="#44999999",
|
|
24
|
+
pulse_checked_color="#4400B0EE"
|
|
25
|
+
):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
|
|
28
|
+
# Save our properties on the object via self, so we can access them later
|
|
29
|
+
# in the paintEvent.
|
|
30
|
+
self._bar_brush = QBrush(bar_color)
|
|
31
|
+
self._bar_checked_brush = QBrush(QColor(checked_color).lighter())
|
|
32
|
+
|
|
33
|
+
self._handle_brush = QBrush(handle_color)
|
|
34
|
+
self._handle_checked_brush = QBrush(QColor(checked_color))
|
|
35
|
+
|
|
36
|
+
self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
|
|
37
|
+
self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))
|
|
38
|
+
|
|
39
|
+
# Setup the rest of the widget.
|
|
40
|
+
self.setContentsMargins(8, 0, 8, 0)
|
|
41
|
+
self._handle_position = 0
|
|
42
|
+
|
|
43
|
+
self._pulse_radius = 0
|
|
44
|
+
|
|
45
|
+
self.animation = QPropertyAnimation(self, b"handle_position", self)
|
|
46
|
+
self.animation.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
|
47
|
+
self.animation.setDuration(200) # time in ms
|
|
48
|
+
|
|
49
|
+
self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
|
|
50
|
+
self.pulse_anim.setDuration(350) # time in ms
|
|
51
|
+
self.pulse_anim.setStartValue(10)
|
|
52
|
+
self.pulse_anim.setEndValue(20)
|
|
53
|
+
|
|
54
|
+
self.animations_group = QSequentialAnimationGroup()
|
|
55
|
+
self.animations_group.addAnimation(self.animation)
|
|
56
|
+
self.animations_group.addAnimation(self.pulse_anim)
|
|
57
|
+
|
|
58
|
+
self.stateChanged.connect(self.setup_animation)
|
|
59
|
+
|
|
60
|
+
def sizeHint(self):
|
|
61
|
+
return QSize(58, 45)
|
|
62
|
+
|
|
63
|
+
def hitButton(self, pos: QPoint):
|
|
64
|
+
return self.contentsRect().contains(pos)
|
|
65
|
+
|
|
66
|
+
@pyqtSlot(int)
|
|
67
|
+
def setup_animation(self, value):
|
|
68
|
+
self.animations_group.stop()
|
|
69
|
+
if value:
|
|
70
|
+
self.animation.setEndValue(1)
|
|
71
|
+
else:
|
|
72
|
+
self.animation.setEndValue(0)
|
|
73
|
+
self.animations_group.start()
|
|
74
|
+
|
|
75
|
+
def paintEvent(self, e: QPaintEvent):
|
|
76
|
+
|
|
77
|
+
contRect = self.contentsRect()
|
|
78
|
+
handleRadius = round(0.24 * contRect.height())
|
|
79
|
+
|
|
80
|
+
p = QPainter(self)
|
|
81
|
+
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
82
|
+
|
|
83
|
+
p.setPen(self._transparent_pen)
|
|
84
|
+
barRect = QRectF(
|
|
85
|
+
0, 0,
|
|
86
|
+
contRect.width() - handleRadius, 0.40 * contRect.height()
|
|
87
|
+
)
|
|
88
|
+
barRect.moveCenter(QPointF(contRect.center()))
|
|
89
|
+
rounding = barRect.height() / 2
|
|
90
|
+
|
|
91
|
+
# the handle will move along this line
|
|
92
|
+
trailLength = contRect.width() - 2 * handleRadius
|
|
93
|
+
|
|
94
|
+
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
|
|
95
|
+
|
|
96
|
+
if self.pulse_anim.state() == QPropertyAnimation.State.Running:
|
|
97
|
+
p.setBrush(
|
|
98
|
+
self._pulse_checked_animation if
|
|
99
|
+
self.isChecked() else self._pulse_unchecked_animation)
|
|
100
|
+
p.drawEllipse(QPointF(xPos, barRect.center().y()),
|
|
101
|
+
self._pulse_radius, self._pulse_radius)
|
|
102
|
+
|
|
103
|
+
if self.isChecked():
|
|
104
|
+
p.setBrush(self._bar_checked_brush)
|
|
105
|
+
p.drawRoundedRect(barRect, rounding, rounding)
|
|
106
|
+
p.setBrush(self._handle_checked_brush)
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
p.setBrush(self._bar_brush)
|
|
110
|
+
p.drawRoundedRect(barRect, rounding, rounding)
|
|
111
|
+
p.setPen(self._light_grey_pen)
|
|
112
|
+
p.setBrush(self._handle_brush)
|
|
113
|
+
|
|
114
|
+
p.drawEllipse(
|
|
115
|
+
QPointF(xPos, barRect.center().y()),
|
|
116
|
+
handleRadius, handleRadius)
|
|
117
|
+
|
|
118
|
+
p.end()
|
|
119
|
+
|
|
120
|
+
@pyqtProperty(float)
|
|
121
|
+
def handle_position(self):
|
|
122
|
+
return self._handle_position
|
|
123
|
+
|
|
124
|
+
@handle_position.setter
|
|
125
|
+
def handle_position(self, pos):
|
|
126
|
+
"""change the property
|
|
127
|
+
we need to trigger QWidget.update() method, either by:
|
|
128
|
+
1- calling it here [ what we doing ].
|
|
129
|
+
2- connecting the QPropertyAnimation.valueChanged() signal to it.
|
|
130
|
+
"""
|
|
131
|
+
self._handle_position = pos
|
|
132
|
+
self.update()
|
|
133
|
+
|
|
134
|
+
@pyqtProperty(float)
|
|
135
|
+
def pulse_radius(self):
|
|
136
|
+
return self._pulse_radius
|
|
137
|
+
|
|
138
|
+
@pulse_radius.setter
|
|
139
|
+
def pulse_radius(self, pos):
|
|
140
|
+
self._pulse_radius = pos
|
|
141
|
+
self.update()
|
common_widgets/color.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/visualize_geometry/features/axes_gizmo.py
|
|
2
|
+
|
|
3
|
+
# Python imports
|
|
4
|
+
from PyQt6.QtGui import QPalette, QColor
|
|
5
|
+
from PyQt6.QtWidgets import QWidget
|
|
6
|
+
|
|
7
|
+
# ------------------------------------------------------------------------------
|
|
8
|
+
# Color Widget
|
|
9
|
+
# ------------------------------------------------------------------------------
|
|
10
|
+
class Color(QWidget):
|
|
11
|
+
def __init__(self, color):
|
|
12
|
+
super(Color, self).__init__()
|
|
13
|
+
self.setAutoFillBackground(True)
|
|
14
|
+
|
|
15
|
+
palette = self.palette()
|
|
16
|
+
palette.setColor(QPalette.ColorRole.Window, QColor(color))
|
|
17
|
+
self.setPalette(palette)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/visualize_geometry/features/axes_gizmo.py
|
|
2
|
+
|
|
3
|
+
# Created: M Clarke, LEADS, 2024
|
|
4
|
+
# Python imports
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
8
|
+
from PyQt6.QtGui import QDoubleValidator
|
|
9
|
+
from PyQt6.QtWidgets import QLineEdit, QLabel, QGridLayout, QWidget, QSizePolicy, QSpacerItem, QCheckBox, QHBoxLayout, \
|
|
10
|
+
QVBoxLayout
|
|
11
|
+
|
|
12
|
+
from utilities import Units
|
|
13
|
+
from common_widgets import UnitPickerWidget
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# Data Entry Widget
|
|
17
|
+
# ------------------------------------------------------------------------------
|
|
18
|
+
class DataEntryWidget(QWidget):
|
|
19
|
+
# Emitted when the user finishes editing any field or changes a unit.
|
|
20
|
+
# Safe to connect to save_data — does NOT fire during programmatic load_data calls.
|
|
21
|
+
data_changed = pyqtSignal()
|
|
22
|
+
|
|
23
|
+
def __init__(self, data_units_labels, num_cols=2):
|
|
24
|
+
super(DataEntryWidget, self).__init__()
|
|
25
|
+
self.data_units_labels = data_units_labels
|
|
26
|
+
self.data_fields = {}
|
|
27
|
+
|
|
28
|
+
self.init_ui(num_cols)
|
|
29
|
+
|
|
30
|
+
def init_ui(self, num_cols):
|
|
31
|
+
grid_layout = QGridLayout()
|
|
32
|
+
row, col = 0, 0
|
|
33
|
+
for label in self.data_units_labels:
|
|
34
|
+
grid_layout.setColumnStretch(col * 4 + 1, 1)
|
|
35
|
+
|
|
36
|
+
if label[1] != Units.Heading:
|
|
37
|
+
grid_layout.addWidget(QLabel(label[0] + ":"), row, col * 4)
|
|
38
|
+
if label[1] == Units.Boolean:
|
|
39
|
+
check_box = QCheckBox(self)
|
|
40
|
+
check_box.setChecked(False)
|
|
41
|
+
check_box.stateChanged.connect(lambda _: self.data_changed.emit())
|
|
42
|
+
grid_layout.addWidget(check_box, row, col * 4 + 1, 1, 2)
|
|
43
|
+
self.data_fields[label[0]] = check_box
|
|
44
|
+
elif label[1] == Units.Position:
|
|
45
|
+
x_line_edit = QLineEdit(self)
|
|
46
|
+
y_line_edit = QLineEdit(self)
|
|
47
|
+
z_line_edit = QLineEdit(self)
|
|
48
|
+
|
|
49
|
+
x_line_edit.setValidator(QDoubleValidator())
|
|
50
|
+
y_line_edit.setValidator(QDoubleValidator())
|
|
51
|
+
z_line_edit.setValidator(QDoubleValidator())
|
|
52
|
+
|
|
53
|
+
unit_picker = UnitPickerWidget(Units.Length)
|
|
54
|
+
unit_picker.on_change_callback = self._make_unit_change_handler(
|
|
55
|
+
unit_picker, x_line_edit, y_line_edit, z_line_edit
|
|
56
|
+
)
|
|
57
|
+
# Also emit data_changed when unit changes (fires only on user action,
|
|
58
|
+
# not during load_data, because UnitPickerWidget._suppress_callback guards it).
|
|
59
|
+
_conv_cb = unit_picker.on_change_callback
|
|
60
|
+
unit_picker.on_change_callback = lambda p, n, _cb=_conv_cb: (_cb(p, n), self.data_changed.emit())
|
|
61
|
+
unit_picker.setFixedWidth(80)
|
|
62
|
+
|
|
63
|
+
layout = QHBoxLayout()
|
|
64
|
+
layout.addWidget(x_line_edit)
|
|
65
|
+
layout.addWidget(y_line_edit)
|
|
66
|
+
layout.addWidget(z_line_edit)
|
|
67
|
+
|
|
68
|
+
x_line_edit.setMinimumSize(50, 0)
|
|
69
|
+
y_line_edit.setMinimumSize(50, 0)
|
|
70
|
+
z_line_edit.setMinimumSize(50, 0)
|
|
71
|
+
|
|
72
|
+
# editingFinished fires on Enter/Tab — not on programmatic setText.
|
|
73
|
+
x_line_edit.editingFinished.connect(self.data_changed)
|
|
74
|
+
y_line_edit.editingFinished.connect(self.data_changed)
|
|
75
|
+
z_line_edit.editingFinished.connect(self.data_changed)
|
|
76
|
+
|
|
77
|
+
grid_layout.addLayout(layout, row, col * 4 + 1, 1, 2)
|
|
78
|
+
grid_layout.addWidget(
|
|
79
|
+
unit_picker, row, col * 4 + 3, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
80
|
+
|
|
81
|
+
self.data_fields[label[0]] = (
|
|
82
|
+
x_line_edit, y_line_edit, z_line_edit, unit_picker)
|
|
83
|
+
elif label[1] == Units.Heading:
|
|
84
|
+
row += 1 if col != 0 else 0
|
|
85
|
+
layout = QHBoxLayout()
|
|
86
|
+
heading_layout = QVBoxLayout()
|
|
87
|
+
heading_label = QLabel(label[0])
|
|
88
|
+
font = heading_label.font()
|
|
89
|
+
font.setBold(True)
|
|
90
|
+
font.setUnderline(True)
|
|
91
|
+
heading_label.setFont(font)
|
|
92
|
+
heading_layout.addWidget(heading_label)
|
|
93
|
+
layout.addLayout(heading_layout)
|
|
94
|
+
grid_layout.addLayout(layout, row, 0, 1, 4)
|
|
95
|
+
col = num_cols - 1
|
|
96
|
+
self.data_fields[label[0]] = ()
|
|
97
|
+
else:
|
|
98
|
+
line_edit = QLineEdit(self)
|
|
99
|
+
# Inertia tensors are entered as list/matrix text, not scalar floats.
|
|
100
|
+
if label[1] != Units.Intertia:
|
|
101
|
+
line_edit.setValidator(QDoubleValidator())
|
|
102
|
+
line_edit.setMinimumWidth(150)
|
|
103
|
+
|
|
104
|
+
unit_picker = UnitPickerWidget(label[1])
|
|
105
|
+
unit_picker.on_change_callback = self._make_unit_change_handler(
|
|
106
|
+
unit_picker, line_edit
|
|
107
|
+
)
|
|
108
|
+
_conv_cb = unit_picker.on_change_callback
|
|
109
|
+
unit_picker.on_change_callback = lambda p, n, _cb=_conv_cb: (_cb(p, n), self.data_changed.emit())
|
|
110
|
+
unit_picker.setFixedWidth(80)
|
|
111
|
+
|
|
112
|
+
line_edit.editingFinished.connect(self.data_changed)
|
|
113
|
+
|
|
114
|
+
grid_layout.addWidget(line_edit, row, col * 4 + 1, 1, 2)
|
|
115
|
+
grid_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum), row,
|
|
116
|
+
col * 4 + 2)
|
|
117
|
+
grid_layout.addWidget(
|
|
118
|
+
unit_picker, row, col * 4 + 3, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
119
|
+
|
|
120
|
+
self.data_fields[label[0]] = (line_edit, unit_picker)
|
|
121
|
+
|
|
122
|
+
col = col + 1 if col < num_cols - 1 else 0
|
|
123
|
+
if col == 0:
|
|
124
|
+
row += 1
|
|
125
|
+
grid_layout.setRowStretch(row, 1)
|
|
126
|
+
|
|
127
|
+
grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
128
|
+
self.setLayout(grid_layout)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _apply_unit_at(unit_picker, index, value):
|
|
132
|
+
return unit_picker.unit_list[index][1](value)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _display_value_from_si(unit_picker, index, si_value):
|
|
136
|
+
# Unit converters map display values into RCAIDE/base units; invert
|
|
137
|
+
# that linear conversion to show the same value in the selected unit.
|
|
138
|
+
converter = unit_picker.unit_list[index][1]
|
|
139
|
+
zero = converter(0.0)
|
|
140
|
+
one = converter(1.0)
|
|
141
|
+
scale = one - zero
|
|
142
|
+
if scale == 0:
|
|
143
|
+
return si_value
|
|
144
|
+
return (si_value - zero) / scale
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _format_converted_value(value):
|
|
148
|
+
return f"{value:.15g}"
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _parse_list_value(text):
|
|
152
|
+
# Use literal_eval so list-like fields become Python lists without eval.
|
|
153
|
+
value = ast.literal_eval(text)
|
|
154
|
+
if not isinstance(value, list):
|
|
155
|
+
raise ValueError(f"Expected a list value, got {type(value).__name__}")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
def _make_unit_change_handler(self, unit_picker, *line_edits):
|
|
159
|
+
def convert_display_values(previous_index, new_index):
|
|
160
|
+
for line_edit in line_edits:
|
|
161
|
+
text = line_edit.text()
|
|
162
|
+
if not text:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
old_display_value = float(text)
|
|
167
|
+
except ValueError:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Preserve the stored physical value while changing only how
|
|
171
|
+
# the current text is displayed, e.g. radians <-> degrees.
|
|
172
|
+
si_value = self._apply_unit_at(
|
|
173
|
+
unit_picker, previous_index, old_display_value
|
|
174
|
+
)
|
|
175
|
+
new_display_value = self._display_value_from_si(
|
|
176
|
+
unit_picker, new_index, si_value
|
|
177
|
+
)
|
|
178
|
+
line_edit.setText(self._format_converted_value(new_display_value))
|
|
179
|
+
|
|
180
|
+
return convert_display_values
|
|
181
|
+
|
|
182
|
+
def clear_values(self):
|
|
183
|
+
for i, key in enumerate(self.data_fields.keys()):
|
|
184
|
+
if self.data_units_labels[i][1] == Units.Boolean:
|
|
185
|
+
self.data_fields[key].setChecked(False)
|
|
186
|
+
elif self.data_units_labels[i][1] == Units.Position:
|
|
187
|
+
self.data_fields[key][0].setText("")
|
|
188
|
+
self.data_fields[key][1].setText("")
|
|
189
|
+
self.data_fields[key][2].setText("")
|
|
190
|
+
self.data_fields[key][3].set_index(0)
|
|
191
|
+
elif self.data_units_labels[i][1] == Units.Heading:
|
|
192
|
+
continue
|
|
193
|
+
else:
|
|
194
|
+
self.data_fields[key][0].setText("")
|
|
195
|
+
self.data_fields[key][1].set_index(0)
|
|
196
|
+
|
|
197
|
+
def get_values(self):
|
|
198
|
+
data = {}
|
|
199
|
+
for i, label in enumerate(self.data_fields.keys()):
|
|
200
|
+
# Find corresponding unit
|
|
201
|
+
if self.data_units_labels[i][1] == Units.Boolean:
|
|
202
|
+
data_field = self.data_fields[label]
|
|
203
|
+
data[label] = (data_field.isChecked(), 0)
|
|
204
|
+
elif self.data_units_labels[i][1] == Units.Position:
|
|
205
|
+
data_field = self.data_fields[label]
|
|
206
|
+
x_line_edit, y_line_edit, z_line_edit, unit_picker = data_field
|
|
207
|
+
|
|
208
|
+
x_value = float(x_line_edit.text()
|
|
209
|
+
) if x_line_edit.text() else 0.0
|
|
210
|
+
y_value = float(y_line_edit.text()
|
|
211
|
+
) if y_line_edit.text() else 0.0
|
|
212
|
+
z_value = float(z_line_edit.text()
|
|
213
|
+
) if z_line_edit.text() else 0.0
|
|
214
|
+
|
|
215
|
+
data[label] = [[x_value, y_value,
|
|
216
|
+
z_value]], unit_picker.current_index
|
|
217
|
+
elif self.data_units_labels[i][1] == Units.Heading:
|
|
218
|
+
continue
|
|
219
|
+
elif self.data_units_labels[i][1] == Units.Count:
|
|
220
|
+
data_field = self.data_fields[label]
|
|
221
|
+
line_edit, unit_picker = data_field
|
|
222
|
+
text = line_edit.text()
|
|
223
|
+
# Count fields should remain integers for RCAIDE.
|
|
224
|
+
value = int(text) if text else 0
|
|
225
|
+
data[label] = value, unit_picker.current_index
|
|
226
|
+
elif self.data_units_labels[i][1] == Units.Intertia:
|
|
227
|
+
data_field = self.data_fields[label]
|
|
228
|
+
line_edit, unit_picker = data_field
|
|
229
|
+
text = line_edit.text()
|
|
230
|
+
# Inertia is stored as a tensor/list, not a single scalar.
|
|
231
|
+
value = self._parse_list_value(text) if text else None
|
|
232
|
+
data[label] = value, unit_picker.current_index
|
|
233
|
+
else:
|
|
234
|
+
data_field = self.data_fields[label]
|
|
235
|
+
line_edit, unit_picker = data_field
|
|
236
|
+
text = line_edit.text()
|
|
237
|
+
# Most fields are numeric, but some Unitless fields are text labels.
|
|
238
|
+
try:
|
|
239
|
+
value = float(text) if text else None
|
|
240
|
+
except ValueError:
|
|
241
|
+
value = text
|
|
242
|
+
data[label] = value, unit_picker.current_index
|
|
243
|
+
return data
|
|
244
|
+
|
|
245
|
+
def get_values_si(self):
|
|
246
|
+
data = {}
|
|
247
|
+
for i, label in enumerate(self.data_fields.keys()):
|
|
248
|
+
if self.data_units_labels[i][1] == Units.Boolean:
|
|
249
|
+
data_field = self.data_fields[label]
|
|
250
|
+
data[label] = (data_field.isChecked(), 0)
|
|
251
|
+
elif self.data_units_labels[i][1] == Units.Position:
|
|
252
|
+
data_field = self.data_fields[label]
|
|
253
|
+
x_line_edit, y_line_edit, z_line_edit, unit_picker = data_field
|
|
254
|
+
|
|
255
|
+
x_value = float(x_line_edit.text()
|
|
256
|
+
) if x_line_edit.text() else 0.0
|
|
257
|
+
y_value = float(y_line_edit.text()
|
|
258
|
+
) if y_line_edit.text() else 0.0
|
|
259
|
+
z_value = float(z_line_edit.text()
|
|
260
|
+
) if z_line_edit.text() else 0.0
|
|
261
|
+
|
|
262
|
+
x_value, y_value, z_value = unit_picker.apply_unit(
|
|
263
|
+
x_value), unit_picker.apply_unit(y_value), unit_picker.apply_unit(z_value)
|
|
264
|
+
|
|
265
|
+
data[label] = [[x_value, y_value,
|
|
266
|
+
z_value]], unit_picker.current_index
|
|
267
|
+
elif self.data_units_labels[i][1] == Units.Heading:
|
|
268
|
+
continue
|
|
269
|
+
elif self.data_units_labels[i][1] == Units.Count:
|
|
270
|
+
data_field = self.data_fields[label]
|
|
271
|
+
line_edit, unit_picker = data_field
|
|
272
|
+
text = line_edit.text()
|
|
273
|
+
# Count fields should remain integers for RCAIDE.
|
|
274
|
+
value = int(text) if text else 0
|
|
275
|
+
data[label] = value, unit_picker.current_index
|
|
276
|
+
elif self.data_units_labels[i][1] == Units.Intertia:
|
|
277
|
+
data_field = self.data_fields[label]
|
|
278
|
+
line_edit, unit_picker = data_field
|
|
279
|
+
text = line_edit.text()
|
|
280
|
+
# Inertia is stored as a tensor/list, not a single scalar.
|
|
281
|
+
value = self._parse_list_value(text) if text else None
|
|
282
|
+
data[label] = value, unit_picker.current_index
|
|
283
|
+
else:
|
|
284
|
+
data_field = self.data_fields[label]
|
|
285
|
+
line_edit, unit_picker = data_field
|
|
286
|
+
text = line_edit.text()
|
|
287
|
+
# Numeric fields are converted to SI; text fields are preserved.
|
|
288
|
+
try:
|
|
289
|
+
value = unit_picker.apply_unit(float(text)) if text else None
|
|
290
|
+
except ValueError:
|
|
291
|
+
value = text
|
|
292
|
+
data[label] = value, unit_picker.current_index
|
|
293
|
+
return data
|
|
294
|
+
|
|
295
|
+
def load_data(self, data):
|
|
296
|
+
# Loading saved data should not trigger auto-save.
|
|
297
|
+
old_block_state = self.blockSignals(True)
|
|
298
|
+
try:
|
|
299
|
+
# Load each field based on its input type.
|
|
300
|
+
for i, label in enumerate(self.data_fields.keys()):
|
|
301
|
+
if self.data_units_labels[i][1] == Units.Boolean:
|
|
302
|
+
# Checkbox field.
|
|
303
|
+
self.data_fields[label].setChecked(data[label][0])
|
|
304
|
+
elif self.data_units_labels[i][1] == Units.Position:
|
|
305
|
+
# Three coordinate boxes and one unit picker.
|
|
306
|
+
x_line_edit, y_line_edit, z_line_edit, unit_picker = self.data_fields[label]
|
|
307
|
+
value, index = data[label]
|
|
308
|
+
# Unwrap [[x, y, z]] if needed.
|
|
309
|
+
if isinstance(value, list) and value and isinstance(value[0], list):
|
|
310
|
+
value = value[0]
|
|
311
|
+
# Use origin if the saved value is invalid.
|
|
312
|
+
elif not isinstance(value, list):
|
|
313
|
+
value = [0, 0, 0]
|
|
314
|
+
x_line_edit.setText(str(value[0]))
|
|
315
|
+
y_line_edit.setText(str(value[1]))
|
|
316
|
+
z_line_edit.setText(str(value[2]))
|
|
317
|
+
unit_picker.set_index(index)
|
|
318
|
+
elif self.data_units_labels[i][1] == Units.Heading:
|
|
319
|
+
# Heading label only.
|
|
320
|
+
pass
|
|
321
|
+
else:
|
|
322
|
+
# Normal value field and unit picker.
|
|
323
|
+
line_edit, unit_picker = self.data_fields[label]
|
|
324
|
+
value, index = data[label]
|
|
325
|
+
# Show None as an empty field.
|
|
326
|
+
line_edit.setText("" if value is None else str(value))
|
|
327
|
+
unit_picker.set_index(index)
|
|
328
|
+
finally:
|
|
329
|
+
# Restore signal behavior.
|
|
330
|
+
self.blockSignals(old_block_state)
|
|
331
|
+
|
|
332
|
+
# TODO implement mark_save and changed_since_save
|
|
333
|
+
def mark_save(self):
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
def changed_since_save(self):
|
|
337
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt
|
|
2
|
+
from PyQt6.QtGui import QPixmap
|
|
3
|
+
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
|
|
4
|
+
|
|
5
|
+
class ImageWidget(QWidget):
|
|
6
|
+
def __init__(self, image_file, w=256, h=256):
|
|
7
|
+
super().__init__()
|
|
8
|
+
|
|
9
|
+
layout = QVBoxLayout()
|
|
10
|
+
|
|
11
|
+
pixmap = QPixmap(image_file)
|
|
12
|
+
pixmap_scaled = pixmap.scaled(w, h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation)
|
|
13
|
+
label = QLabel()
|
|
14
|
+
label.setPixmap(pixmap_scaled)
|
|
15
|
+
# label.setScaledContents(True)
|
|
16
|
+
|
|
17
|
+
layout.addWidget(label)
|
|
18
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
19
|
+
|
|
20
|
+
self.setLayout(layout)
|
|
21
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox
|
|
2
|
+
|
|
3
|
+
class UnitPickerWidget(QWidget):
|
|
4
|
+
def __init__(self, unit_class):
|
|
5
|
+
super(UnitPickerWidget, self).__init__()
|
|
6
|
+
|
|
7
|
+
self.unit_class = unit_class()
|
|
8
|
+
self.unit_list = self.unit_class.unit_list
|
|
9
|
+
self.current_index = 0
|
|
10
|
+
self.on_change_callback = None
|
|
11
|
+
self._suppress_callback = False
|
|
12
|
+
|
|
13
|
+
self.unit_picker = QComboBox()
|
|
14
|
+
|
|
15
|
+
self.init_ui()
|
|
16
|
+
|
|
17
|
+
def init_ui(self):
|
|
18
|
+
main_layout = QVBoxLayout()
|
|
19
|
+
self.setLayout(main_layout)
|
|
20
|
+
|
|
21
|
+
unit_name_list = [unit[0] for unit in self.unit_list]
|
|
22
|
+
self.unit_picker.addItems(unit_name_list)
|
|
23
|
+
self.unit_picker.setCurrentIndex(self.current_index)
|
|
24
|
+
self.unit_picker.currentIndexChanged.connect(self.on_unit_change)
|
|
25
|
+
|
|
26
|
+
main_layout.addWidget(self.unit_picker)
|
|
27
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
28
|
+
|
|
29
|
+
def on_unit_change(self, index):
|
|
30
|
+
previous_index = self.current_index
|
|
31
|
+
# Programmatic loads set the combo box without rewriting field values;
|
|
32
|
+
# user-initiated changes ask the owning widget to convert the display.
|
|
33
|
+
if (
|
|
34
|
+
not self._suppress_callback
|
|
35
|
+
and self.on_change_callback is not None
|
|
36
|
+
and previous_index != index
|
|
37
|
+
):
|
|
38
|
+
self.on_change_callback(previous_index, index)
|
|
39
|
+
self.current_index = index
|
|
40
|
+
|
|
41
|
+
def apply_unit(self, value):
|
|
42
|
+
return self.unit_list[self.current_index][1](value)
|
|
43
|
+
|
|
44
|
+
def set_index(self, index):
|
|
45
|
+
self.current_index = index
|
|
46
|
+
self._suppress_callback = True
|
|
47
|
+
self.unit_picker.setCurrentIndex(index)
|
|
48
|
+
self._suppress_callback = False
|