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.
Files changed (170) hide show
  1. common_widgets/__init__.py +6 -0
  2. common_widgets/animated_toggle.py +141 -0
  3. common_widgets/color.py +17 -0
  4. common_widgets/data_entry_widget.py +337 -0
  5. common_widgets/image_widget.py +21 -0
  6. common_widgets/unit_picker_widget.py +48 -0
  7. main.py +168 -0
  8. rcaide_gui-1.0.0.dist-info/METADATA +96 -0
  9. rcaide_gui-1.0.0.dist-info/RECORD +170 -0
  10. rcaide_gui-1.0.0.dist-info/WHEEL +5 -0
  11. rcaide_gui-1.0.0.dist-info/entry_points.txt +2 -0
  12. rcaide_gui-1.0.0.dist-info/top_level.txt +5 -0
  13. rcaide_io.py +818 -0
  14. tabs/__init__.py +10 -0
  15. tabs/aircraft_configs/__init__.py +3 -0
  16. tabs/aircraft_configs/aircraft_configs.py +301 -0
  17. tabs/analysis/__init__.py +4 -0
  18. tabs/analysis/analysis.py +92 -0
  19. tabs/analysis/widgets/__init__.py +13 -0
  20. tabs/analysis/widgets/aeroacoustics_widget.py +158 -0
  21. tabs/analysis/widgets/aerodynamics_widget.py +108 -0
  22. tabs/analysis/widgets/analysis_data_widget.py +29 -0
  23. tabs/analysis/widgets/atmosphere_widget.py +49 -0
  24. tabs/analysis/widgets/costs_widget.py +35 -0
  25. tabs/analysis/widgets/energy_widget.py +32 -0
  26. tabs/analysis/widgets/geometry_widget.py +55 -0
  27. tabs/analysis/widgets/planets_widget.py +34 -0
  28. tabs/analysis/widgets/propulsion_widget.py +53 -0
  29. tabs/analysis/widgets/stability_widget.py +34 -0
  30. tabs/analysis/widgets/weights_widget.py +94 -0
  31. tabs/geometry/__init__.py +6 -0
  32. tabs/geometry/aircraft_configs.py +232 -0
  33. tabs/geometry/frames/__init__.py +15 -0
  34. tabs/geometry/frames/booms/__init__.py +5 -0
  35. tabs/geometry/frames/booms/boom_frame.py +238 -0
  36. tabs/geometry/frames/cargo_bays/__init__.py +5 -0
  37. tabs/geometry/frames/cargo_bays/cargo_bay_frame.py +159 -0
  38. tabs/geometry/frames/default_frame.py +23 -0
  39. tabs/geometry/frames/energy_network/__init__.py +2 -0
  40. tabs/geometry/frames/energy_network/turbofan_network/__init__.py +3 -0
  41. tabs/geometry/frames/energy_network/turbofan_network/frames/__init__.py +1 -0
  42. tabs/geometry/frames/energy_network/turbofan_network/widgets/__init__.py +1 -0
  43. tabs/geometry/frames/fuselages/__init__.py +5 -0
  44. tabs/geometry/frames/fuselages/fuselage_frame.py +294 -0
  45. tabs/geometry/frames/geometry_frame.py +91 -0
  46. tabs/geometry/frames/landing_gears/__init__.py +5 -0
  47. tabs/geometry/frames/landing_gears/landing_gear_frame.py +189 -0
  48. tabs/geometry/frames/nacelles/__init__.py +1 -0
  49. tabs/geometry/frames/powertrain/__init__.py +6 -0
  50. tabs/geometry/frames/powertrain/converters/__init__.py +5 -0
  51. tabs/geometry/frames/powertrain/converters/converter_frame.py +137 -0
  52. tabs/geometry/frames/powertrain/distributors/__init__.py +5 -0
  53. tabs/geometry/frames/powertrain/distributors/distributor_frame.py +143 -0
  54. tabs/geometry/frames/powertrain/nacelles/__init__.py +5 -0
  55. tabs/geometry/frames/powertrain/nacelles/nacelle_frame.py +283 -0
  56. tabs/geometry/frames/powertrain/powertrain_frame.py +259 -0
  57. tabs/geometry/frames/powertrain/propulsors/__init__.py +5 -0
  58. tabs/geometry/frames/powertrain/propulsors/propulsor_frame.py +146 -0
  59. tabs/geometry/frames/powertrain/sources/__init__.py +6 -0
  60. tabs/geometry/frames/powertrain/sources/energy_source_frame.py +147 -0
  61. tabs/geometry/frames/vehicle_frame.py +171 -0
  62. tabs/geometry/frames/wings/__init__.py +5 -0
  63. tabs/geometry/frames/wings/wings_frame.py +457 -0
  64. tabs/geometry/geometry.py +513 -0
  65. tabs/geometry/widgets/__init__.py +12 -0
  66. tabs/geometry/widgets/booms/__init__.py +6 -0
  67. tabs/geometry/widgets/booms/boom_section_widget.py +109 -0
  68. tabs/geometry/widgets/cargo_bays/__init__.py +1 -0
  69. tabs/geometry/widgets/fuselages/__init__.py +8 -0
  70. tabs/geometry/widgets/fuselages/cabin_class_widget.py +188 -0
  71. tabs/geometry/widgets/fuselages/cabin_widget.py +217 -0
  72. tabs/geometry/widgets/fuselages/fuselage_section_widget.py +190 -0
  73. tabs/geometry/widgets/geometry_data_widget.py +25 -0
  74. tabs/geometry/widgets/landing_gears/__init__.py +6 -0
  75. tabs/geometry/widgets/nacelles/__init__.py +1 -0
  76. tabs/geometry/widgets/powertrain/__init__.py +9 -0
  77. tabs/geometry/widgets/powertrain/converters/__init__.py +5 -0
  78. tabs/geometry/widgets/powertrain/converters/turboelectric_generator_widget.py +74 -0
  79. tabs/geometry/widgets/powertrain/distributors/__init__.py +5 -0
  80. tabs/geometry/widgets/powertrain/distributors/fuel_line_widget.py +81 -0
  81. tabs/geometry/widgets/powertrain/modulators/__init__.py +5 -0
  82. tabs/geometry/widgets/powertrain/modulators/esc_widget.py +58 -0
  83. tabs/geometry/widgets/powertrain/nacelles/__init__.py +5 -0
  84. tabs/geometry/widgets/powertrain/nacelles/nacelle_section_widget.py +115 -0
  85. tabs/geometry/widgets/powertrain/powertrain_connector_widget.py +136 -0
  86. tabs/geometry/widgets/powertrain/powertrain_widget.py +206 -0
  87. tabs/geometry/widgets/powertrain/propulsors/__init__.py +5 -0
  88. tabs/geometry/widgets/powertrain/propulsors/turbofan_widget.py +462 -0
  89. tabs/geometry/widgets/powertrain/sources/__init__.py +6 -0
  90. tabs/geometry/widgets/powertrain/sources/fuel_tank_widget.py +103 -0
  91. tabs/geometry/widgets/powertrain/sources/source_selector_widget.py +35 -0
  92. tabs/geometry/widgets/wings/__init__.py +7 -0
  93. tabs/geometry/widgets/wings/wing_cs_widget.py +198 -0
  94. tabs/geometry/widgets/wings/wing_section_widget.py +298 -0
  95. tabs/home/__init__.py +3 -0
  96. tabs/home/home.py +585 -0
  97. tabs/mission/__init__.py +4 -0
  98. tabs/mission/mission.py +1124 -0
  99. tabs/mission/widgets/__init__.py +3 -0
  100. tabs/mission/widgets/flight_controls_widget.py +186 -0
  101. tabs/mission/widgets/mission_analysis_widget.py +166 -0
  102. tabs/mission/widgets/mission_segment_helper.py +358 -0
  103. tabs/mission/widgets/mission_segment_widget.py +358 -0
  104. tabs/solve/__init__.py +4 -0
  105. tabs/solve/plots/__init__.py +12 -0
  106. tabs/solve/plots/aeroacoustics/__init__.py +6 -0
  107. tabs/solve/plots/aeroacoustics/plot_2D_noise_contour.py +184 -0
  108. tabs/solve/plots/aeroacoustics/plot_3D_noise_contour.py +219 -0
  109. tabs/solve/plots/aeroacoustics/plot_noise_certification_contour.py +115 -0
  110. tabs/solve/plots/aeroacoustics/plot_noise_level.py +118 -0
  111. tabs/solve/plots/aerodynamics/__init__.py +10 -0
  112. tabs/solve/plots/aerodynamics/plot_aerodynamic_coefficients.py +156 -0
  113. tabs/solve/plots/aerodynamics/plot_aerodynamic_forces.py +155 -0
  114. tabs/solve/plots/aerodynamics/plot_aircraft_aerodynamics.py +136 -0
  115. tabs/solve/plots/aerodynamics/plot_drag_components.py +157 -0
  116. tabs/solve/plots/aerodynamics/plot_lift_distribution.py +107 -0
  117. tabs/solve/plots/aerodynamics/plot_rotor_conditions.py +255 -0
  118. tabs/solve/plots/aerodynamics/plot_rotor_disc_performance.py +144 -0
  119. tabs/solve/plots/aerodynamics/plot_rotor_performance.py +124 -0
  120. tabs/solve/plots/common/__init__.py +4 -0
  121. tabs/solve/plots/common/plot_style.py +71 -0
  122. tabs/solve/plots/common/set_axes.py +55 -0
  123. tabs/solve/plots/create_plot_widgets.py +34 -0
  124. tabs/solve/plots/emissions/__init__.py +3 -0
  125. tabs/solve/plots/emissions/plot_emissions.py +167 -0
  126. tabs/solve/plots/energy/__init__.py +12 -0
  127. tabs/solve/plots/energy/plot_altitude_sfc_weight.py +134 -0
  128. tabs/solve/plots/energy/plot_battery_cell_conditions.py +154 -0
  129. tabs/solve/plots/energy/plot_battery_degradation.py +157 -0
  130. tabs/solve/plots/energy/plot_battery_module_C_rates.py +139 -0
  131. tabs/solve/plots/energy/plot_battery_module_conditions.py +157 -0
  132. tabs/solve/plots/energy/plot_battery_pack_conditions.py +164 -0
  133. tabs/solve/plots/energy/plot_battery_ragone_diagram.py +97 -0
  134. tabs/solve/plots/energy/plot_battery_temperature.py +136 -0
  135. tabs/solve/plots/energy/plot_electric_propulsor_efficiencies.py +136 -0
  136. tabs/solve/plots/energy/plot_propulsor_throttles.py +119 -0
  137. tabs/solve/plots/mission/__init__.py +5 -0
  138. tabs/solve/plots/mission/plot_aircraft_velocities.py +145 -0
  139. tabs/solve/plots/mission/plot_flight_conditions.py +236 -0
  140. tabs/solve/plots/mission/plot_flight_trajectory.py +172 -0
  141. tabs/solve/plots/stability/__init__.py +5 -0
  142. tabs/solve/plots/stability/plot_flight_forces_and_moments.py +163 -0
  143. tabs/solve/plots/stability/plot_lateral_stability.py +151 -0
  144. tabs/solve/plots/stability/plot_longitudinal_stability.py +173 -0
  145. tabs/solve/plots/thermal_management/__init__.py +7 -0
  146. tabs/solve/plots/thermal_management/plot_air_cooled_conditions.py +152 -0
  147. tabs/solve/plots/thermal_management/plot_cross_flow_heat_exchanger_conditions.py +185 -0
  148. tabs/solve/plots/thermal_management/plot_reservoir_conditions.py +141 -0
  149. tabs/solve/plots/thermal_management/plot_thermal_management_performance.py +105 -0
  150. tabs/solve/plots/thermal_management/plot_wavy_channel_conditions.py +161 -0
  151. tabs/solve/plots/weights/__init__.py +4 -0
  152. tabs/solve/plots/weights/plot_load_diagram.py +163 -0
  153. tabs/solve/plots/weights/plot_weight_breakdown.py +164 -0
  154. tabs/solve/solve.py +1498 -0
  155. tabs/style_sheet.py +114 -0
  156. tabs/tab_widget.py +14 -0
  157. tabs/visualize_geometry/__init__.py +4 -0
  158. tabs/visualize_geometry/core_3d_viewer.py +253 -0
  159. tabs/visualize_geometry/features/__init__.py +10 -0
  160. tabs/visualize_geometry/features/axes_gizmo.py +173 -0
  161. tabs/visualize_geometry/features/background.py +161 -0
  162. tabs/visualize_geometry/features/blueprint.py +685 -0
  163. tabs/visualize_geometry/features/camera.py +364 -0
  164. tabs/visualize_geometry/features/drag_aircraft.py +219 -0
  165. tabs/visualize_geometry/features/grid.py +194 -0
  166. tabs/visualize_geometry/features/measurement.py +367 -0
  167. tabs/visualize_geometry/features/screenshot.py +91 -0
  168. tabs/visualize_geometry/geometry_helper_functions.py +3 -0
  169. tabs/visualize_geometry/visualize_geometry.py +731 -0
  170. utilities.py +257 -0
@@ -0,0 +1,6 @@
1
+ from .color import Color
2
+ from .unit_picker_widget import UnitPickerWidget
3
+ from .data_entry_widget import DataEntryWidget
4
+ from .image_widget import ImageWidget
5
+ from .animated_toggle import AnimatedToggle
6
+
@@ -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()
@@ -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