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,364 @@
1
+ # RCAIDE_GUI/tabs/visualize_geometry/features/camera.py
2
+
3
+ # RCAIDE-GUI Imports
4
+ from tabs.visualize_geometry.visualize_geometry import VisualizeGeometryWidget
5
+
6
+ # PyQt imports
7
+ from PyQt6.QtWidgets import QPushButton
8
+ from PyQt6.QtCore import QTimer
9
+
10
+ # Python imports
11
+ import functools
12
+ import types
13
+ import math
14
+
15
+ _DEFAULT_BOUNDS = (-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
16
+ # --- helpers: unwrap / window / interactor ---
17
+ def _unwrap(func):
18
+ # Walk through __wrapped__ to get the original function (prevents wrapper stacking confusion)
19
+ f = func
20
+ seen = set()
21
+ while hasattr(f, "__wrapped__") and f not in seen:
22
+ seen.add(f)
23
+ f = f.__wrapped__
24
+ return f
25
+
26
+ def _rw(self):
27
+ # Get the VTK RenderWindow safely (can be missing during tab rebuilds)
28
+ try:
29
+ return self.vtkWidget.GetRenderWindow()
30
+ except Exception:
31
+ return None
32
+
33
+ def _iren(self):
34
+ # Get the RenderWindowInteractor safely
35
+ rw = _rw(self)
36
+ try:
37
+ return rw.GetInteractor() if rw is not None else None
38
+ except Exception:
39
+ return None
40
+
41
+ # --- helpers: bounds + camera focusing ---
42
+ def _safe_bounds(self):
43
+ # Return scene bounds if there are visible actors; otherwise return a stable default bounds box
44
+ r = getattr(self, "renderer", None)
45
+ if r is None:
46
+ return _DEFAULT_BOUNDS
47
+ try:
48
+ if r.VisibleActorCount() > 0:
49
+ b = r.ComputeVisiblePropBounds()
50
+ if b and len(b) == 6:
51
+ return b
52
+ except Exception:
53
+ pass
54
+ return _DEFAULT_BOUNDS
55
+
56
+ def _reset_camera_focus_to_center_safe(self):
57
+ # Move camera focal point to the center of visible bounds (prevents “orbiting off into space”)
58
+ r = getattr(self, "renderer", None)
59
+ if r is None:
60
+ return
61
+ b = self._safe_bounds()
62
+ cx = (b[0] + b[1]) * 0.5
63
+ cy = (b[2] + b[3]) * 0.5
64
+ cz = (b[4] + b[5]) * 0.5
65
+ cam = r.GetActiveCamera()
66
+ if cam is not None:
67
+ cam.SetFocalPoint(cx, cy, cz)
68
+
69
+ def _set_view_function_safe(self, Position, Viewup):
70
+ # Set camera position + up-vector safely, then reset camera and re-center focus
71
+ r = getattr(self, "renderer", None)
72
+ if r is None:
73
+ return
74
+
75
+ cam = getattr(self, "get_camera", None) or r.GetActiveCamera()
76
+ self.get_camera = cam
77
+ if cam is None:
78
+ return
79
+
80
+ cam.SetFocalPoint(0, 0, 0)
81
+ cam.SetPosition(*Position)
82
+ cam.SetViewUp(*Viewup)
83
+
84
+ try:
85
+ r.ResetCamera()
86
+ cam.Zoom(1.5)
87
+ except Exception:
88
+ pass
89
+
90
+ try:
91
+ self.reset_camera_focus_to_center()
92
+ except Exception:
93
+ pass
94
+
95
+ rw = _rw(self)
96
+ if rw is not None:
97
+ try:
98
+ rw.Render()
99
+ except Exception:
100
+ pass
101
+
102
+ # --- safe camera view commands (use bounds-based offsets) ---
103
+ def _front_function_safe(self):
104
+ # Front view: offset along -X by half the model width
105
+ if getattr(self, "renderer", None) is None:
106
+ return
107
+ b = self._safe_bounds()
108
+ self.set_view_function((-(b[1] - b[0]) * 0.5, 0, 0), (0, 0, 1))
109
+
110
+ def _side_function_safe(self):
111
+ # Side view: offset along -Y by full model height
112
+ if getattr(self, "renderer", None) is None:
113
+ return
114
+ b = self._safe_bounds()
115
+ self.set_view_function((0, -(b[3] - b[2]), 0), (0, 0, 1))
116
+
117
+ def _top_function_safe(self):
118
+ # Top view: offset along +Z by full model height; set Y-up
119
+ if getattr(self, "renderer", None) is None:
120
+ return
121
+ b = self._safe_bounds()
122
+ self.set_view_function((0, 0, (b[3] - b[2])), (0, 1, 0))
123
+
124
+ def _isometric_function_safe(self):
125
+ # Isometric view: offset equally in -X/-Y/+Z based on depth bounds
126
+ if getattr(self, "renderer", None) is None:
127
+ return
128
+ b = self._safe_bounds()
129
+ self.set_view_function((-b[5], -b[5], b[5]), (0, 0, 1))
130
+
131
+ # --- attach safe methods onto the widget (overrides are intentional) ---
132
+ VisualizeGeometryWidget._safe_bounds = _safe_bounds
133
+ VisualizeGeometryWidget.reset_camera_focus_to_center = _reset_camera_focus_to_center_safe
134
+ VisualizeGeometryWidget.set_view_function = _set_view_function_safe
135
+ VisualizeGeometryWidget.front_function = _front_function_safe
136
+ VisualizeGeometryWidget.side_function = _side_function_safe
137
+ VisualizeGeometryWidget.top_function = _top_function_safe
138
+ VisualizeGeometryWidget.isometric_function = _isometric_function_safe
139
+
140
+ # --- RUN_SOLVE NON-BLOCKING (PREVENT GUI LOCK)---
141
+ if not getattr(VisualizeGeometryWidget, "_run_solve_nonblocking_patched", False):
142
+ # Grab the original run_solve (even if it has already been wrapped elsewhere)
143
+ _base_run_solve = _unwrap(VisualizeGeometryWidget.run_solve)
144
+
145
+ @functools.wraps(_base_run_solve)
146
+ def _run_solve_nonblocking(self, *args, **kwargs):
147
+ iren = _iren(self)
148
+ old_start = None
149
+
150
+ # Temporarily disable iren.Start() so VTK does not take over the event loop
151
+ if iren is not None:
152
+ try:
153
+ old_start = iren.Start
154
+
155
+ def _noop_start(_self_iren):
156
+ return None
157
+
158
+ iren.Start = types.MethodType(_noop_start, iren)
159
+ except Exception:
160
+ old_start = None
161
+
162
+ try:
163
+ _base_run_solve(self, *args, **kwargs)
164
+ finally:
165
+ # Restore iren.Start() no matter what happens inside run_solve
166
+ if iren is not None and old_start is not None:
167
+ try:
168
+ iren.Start = old_start
169
+ except Exception:
170
+ pass
171
+
172
+ # Re-apply expected background styling and force a redraw
173
+ try:
174
+ r = getattr(self, "renderer", None)
175
+ if r is not None:
176
+ r.SetBackground(0.05, 0.09, 0.15)
177
+ r.SetBackground2(0.12, 0.18, 0.28)
178
+ r.GradientBackgroundOn()
179
+ rw = _rw(self)
180
+ if rw is not None:
181
+ rw.Render()
182
+ except Exception:
183
+ pass
184
+
185
+ # Keep toolbar visible after solve
186
+ try:
187
+ self.toolbar.raise_()
188
+ except Exception:
189
+ pass
190
+
191
+ # Preserve reference to the original function for future unwrap() calls
192
+ _run_solve_nonblocking.__wrapped__ = _base_run_solve
193
+ VisualizeGeometryWidget.run_solve = _run_solve_nonblocking
194
+ VisualizeGeometryWidget._run_solve_nonblocking_patched = True
195
+
196
+ # =======================================
197
+ # AUTO-ROTATE CAMERA (360° ORBIT)
198
+ # =======================================
199
+ # Toolbar toggle that orbits the camera around its current focal point.
200
+
201
+ def add_360_view(self):
202
+ # Requires toolbar + renderer + VTK widget
203
+ if not getattr(self, "toolbar", None) or getattr(self, "renderer", None) is None:
204
+ return
205
+ if not getattr(self, "vtkWidget", None):
206
+ return
207
+
208
+ # Don’t add twice (update_toolbar can be called multiple times)
209
+ if getattr(self, "view360_button", None) is not None:
210
+ return
211
+
212
+ btn = QPushButton("360° View")
213
+ btn.setCheckable(True)
214
+ self.toolbar.addSeparator()
215
+ self.toolbar.addWidget(btn)
216
+ self.view360_button = btn
217
+
218
+ # Timer drives the orbit animation
219
+ timer = QTimer(self)
220
+ timer.setInterval(25)
221
+ self._view360_timer = timer
222
+
223
+ # Orbit state (center, distance, up vector, angle, basis directions)
224
+ state = {"c": None, "d": None, "up": None, "theta": 0.0, "dir0": None, "t": None}
225
+
226
+ def _norm(x, y, z):
227
+ # Normalize a vector and return (unit_vector, magnitude)
228
+ n = (x*x + y*y + z*z) ** 0.5
229
+ if n <= 1e-12:
230
+ return (0.0, 0.0, 0.0), 0.0
231
+ return (x/n, y/n, z/n), n
232
+
233
+ def _cross(ax, ay, az, bx, by, bz):
234
+ # Cross product a × b
235
+ return (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx)
236
+
237
+ def init_orbit():
238
+ # Capture focal point / camera position and build orbit basis
239
+ r = getattr(self, "renderer", None)
240
+ if r is None:
241
+ return
242
+ cam = r.GetActiveCamera()
243
+ if cam is None:
244
+ return
245
+
246
+ c = cam.GetFocalPoint()
247
+ p = cam.GetPosition()
248
+
249
+ # dir0 is the direction from focal point to camera, dist is the radius
250
+ (dir0, dist) = _norm(p[0] - c[0], p[1] - c[1], p[2] - c[2])
251
+
252
+ # If the camera is sitting on the focal point, estimate a usable distance
253
+ if dist <= 1e-6:
254
+ b = r.ComputeVisiblePropBounds()
255
+ if b and len(b) == 6 and b[1] != b[0]:
256
+ size = max(b[1] - b[0], b[3] - b[2], b[5] - b[4])
257
+ dist = max(1e-3, size * 3.5)
258
+ dir0 = (1.0, 0.0, 0.0)
259
+ else:
260
+ return
261
+
262
+ # Normalize up vector (fallback to Z-up if invalid)
263
+ up_raw = cam.GetViewUp()
264
+ (up, upn) = _norm(up_raw[0], up_raw[1], up_raw[2])
265
+ if upn <= 1e-12:
266
+ up = (0.0, 0.0, 1.0)
267
+
268
+ # Build a tangent direction for rotation: t = up × dir0
269
+ tx, ty, tz = _cross(up[0], up[1], up[2], dir0[0], dir0[1], dir0[2])
270
+ (t, tn) = _norm(tx, ty, tz)
271
+
272
+ # Fallback if up is parallel to dir0
273
+ if tn <= 1e-12:
274
+ tx, ty, tz = _cross(up[0], up[1], up[2], 1.0, 0.0, 0.0)
275
+ (t, tn) = _norm(tx, ty, tz)
276
+ if tn <= 1e-12:
277
+ tx, ty, tz = _cross(up[0], up[1], up[2], 0.0, 1.0, 0.0)
278
+ (t, _) = _norm(tx, ty, tz)
279
+
280
+ # Save orbit basis
281
+ state["c"] = (c[0], c[1], c[2])
282
+ state["d"] = dist
283
+ state["up"] = up
284
+ state["dir0"] = dir0
285
+ state["t"] = t
286
+ state["theta"] = 0.0
287
+
288
+ # Render once so the first orbit frame looks correct
289
+ try:
290
+ r.ResetCameraClippingRange()
291
+ self.vtkWidget.GetRenderWindow().Render()
292
+ except Exception:
293
+ pass
294
+
295
+ def tick():
296
+ # Move camera along the orbit and redraw
297
+ r = getattr(self, "renderer", None)
298
+ if r is None:
299
+ return
300
+ cam = r.GetActiveCamera()
301
+ if cam is None or state["c"] is None:
302
+ return
303
+
304
+ # Step the angle (1.5° per tick)
305
+ state["theta"] += math.radians(1.5)
306
+ if state["theta"] >= 2.0 * math.pi:
307
+ state["theta"] -= 2.0 * math.pi
308
+
309
+ ct = math.cos(state["theta"])
310
+ st = math.sin(state["theta"])
311
+
312
+ # dir_new = dir0*cos(theta) + t*sin(theta)
313
+ dir0 = state["dir0"]
314
+ t = state["t"]
315
+ dir_new = (
316
+ dir0[0] * ct + t[0] * st,
317
+ dir0[1] * ct + t[1] * st,
318
+ dir0[2] * ct + t[2] * st,
319
+ )
320
+
321
+ # Position = center + dir_new * radius
322
+ c = state["c"]
323
+ d = state["d"]
324
+ pos = (c[0] + dir_new[0] * d, c[1] + dir_new[1] * d, c[2] + dir_new[2] * d)
325
+
326
+ cam.SetPosition(pos[0], pos[1], pos[2])
327
+ cam.SetFocalPoint(c[0], c[1], c[2])
328
+ cam.SetViewUp(state["up"][0], state["up"][1], state["up"][2])
329
+
330
+ try:
331
+ r.ResetCameraClippingRange()
332
+ self.vtkWidget.GetRenderWindow().Render()
333
+ except Exception:
334
+ pass
335
+
336
+ # Timer callback for orbit animation
337
+ timer.timeout.connect(tick)
338
+
339
+ def on_toggle(checked):
340
+ # Start/stop orbit
341
+ if checked:
342
+ init_orbit()
343
+ timer.start()
344
+ else:
345
+ timer.stop()
346
+
347
+ btn.toggled.connect(on_toggle)
348
+
349
+ # Activate Patch
350
+ def _patch_update_toolbar_for_360_view():
351
+ # Patch update_toolbar once to add the 360° button
352
+ if getattr(VisualizeGeometryWidget, "_view360_update_toolbar_patched", False):
353
+ return
354
+
355
+ old_update_toolbar = VisualizeGeometryWidget.update_toolbar
356
+
357
+ def wrapped_update_toolbar(self, *args, **kwargs):
358
+ old_update_toolbar(self, *args, **kwargs)
359
+ add_360_view(self)
360
+
361
+ VisualizeGeometryWidget.update_toolbar = wrapped_update_toolbar
362
+ VisualizeGeometryWidget._view360_update_toolbar_patched = True
363
+
364
+ _patch_update_toolbar_for_360_view()
@@ -0,0 +1,219 @@
1
+ # RCAIDE_GUI/tabs/visualize_geometry/features/drag_aircraft.py
2
+
3
+ # RCAIDE-GUI Imports
4
+ from tabs.visualize_geometry.visualize_geometry import VisualizeGeometryWidget
5
+
6
+ # PyQt imports
7
+ from PyQt6.QtWidgets import QPushButton
8
+
9
+ # Python imports
10
+ import vtkmodules.all as vtk
11
+
12
+ class DragAircraftInteractor(vtk.vtkInteractorStyleTrackballCamera):
13
+ def __init__(self, widget, actors):
14
+ super().__init__()
15
+
16
+ # widget gives us access to renderer + vtkWidget
17
+ self.widget = widget
18
+
19
+ # actors is a dict of lists (group name -> [vtkActor, ...])
20
+ self.actors = actors
21
+
22
+ # drag state
23
+ self.is_dragging = False
24
+ self.last_pos = None
25
+
26
+ # depth lock (keeps drag from jumping in/out of screen)
27
+ self._drag_dz = None
28
+
29
+ # picker lets us lock drag depth at the clicked point
30
+ self._picker = vtk.vtkCellPicker()
31
+ self._picker.SetTolerance(0.0005)
32
+
33
+ # hook mouse events
34
+ self.AddObserver("LeftButtonPressEvent", self.left_down)
35
+ self.AddObserver("LeftButtonReleaseEvent", self.left_up)
36
+ self.AddObserver("MouseMoveEvent", self.mouse_move)
37
+
38
+ def left_down(self, obj, event):
39
+ # start dragging
40
+ self.is_dragging = True
41
+
42
+ # record starting mouse position
43
+ iren = self.GetInteractor()
44
+ self.last_pos = iren.GetEventPosition()
45
+
46
+ # choose a fixed screen-depth for the drag (dz)
47
+ r = self.widget.renderer
48
+ x, y = self.last_pos
49
+
50
+ # if we clicked the model, use that picked point for depth
51
+ if self._picker.Pick(x, y, 0, r):
52
+ p = self._picker.GetPickPosition()
53
+
54
+ r.SetWorldPoint(p[0], p[1], p[2], 1.0)
55
+ r.WorldToDisplay()
56
+ self._drag_dz = r.GetDisplayPoint()[2]
57
+
58
+ # otherwise use camera focal point for depth
59
+ else:
60
+ cam = r.GetActiveCamera()
61
+ fp = cam.GetFocalPoint()
62
+
63
+ r.SetWorldPoint(fp[0], fp[1], fp[2], 1.0)
64
+ r.WorldToDisplay()
65
+ self._drag_dz = r.GetDisplayPoint()[2]
66
+
67
+ def left_up(self, obj, event):
68
+ # stop dragging
69
+ self.is_dragging = False
70
+ self.last_pos = None
71
+ self._drag_dz = None
72
+
73
+ def mouse_move(self, obj, event):
74
+ # only drag when active
75
+ if not self.is_dragging or self.last_pos is None or self._drag_dz is None:
76
+ return
77
+
78
+ # current mouse position
79
+ iren = self.GetInteractor()
80
+ x, y = iren.GetEventPosition()
81
+
82
+ # pixel delta since last frame
83
+ dx = x - self.last_pos[0]
84
+ dy = y - self.last_pos[1]
85
+ self.last_pos = (x, y)
86
+
87
+ r = self.widget.renderer
88
+ dz = self._drag_dz
89
+
90
+ # screen -> world for current mouse position
91
+ r.SetDisplayPoint(x, y, dz)
92
+ r.DisplayToWorld()
93
+ wx, wy, wz, _ = r.GetWorldPoint()
94
+
95
+ # screen -> world for previous mouse position
96
+ r.SetDisplayPoint(x - dx, y - dy, dz)
97
+ r.DisplayToWorld()
98
+ wx2, wy2, wz2, _ = r.GetWorldPoint()
99
+
100
+ # world delta to apply to every actor
101
+ ddx, ddy, ddz = (wx - wx2, wy - wy2, wz - wz2)
102
+
103
+ # move every actor by the same world delta
104
+ for group in self.actors.values():
105
+ for actor in group:
106
+ ax, ay, az = actor.GetPosition()
107
+ actor.SetPosition(ax + ddx, ay + ddy, az + ddz)
108
+
109
+ # redraw
110
+ iren.GetRenderWindow().Render()
111
+
112
+
113
+ def add_drag_button(self):
114
+ # need toolbar + renderer
115
+ if not getattr(self, "toolbar", None) or getattr(self, "renderer", None) is None:
116
+ return
117
+
118
+ # don’t add the button twice
119
+ if getattr(self, "_drag_button_loaded", False):
120
+ return
121
+ self._drag_button_loaded = True
122
+
123
+ # toolbar toggle button
124
+ btn = QPushButton("✈️ Drag Aircraft")
125
+ btn.setCheckable(True)
126
+
127
+ self.toolbar.addSeparator()
128
+ self.toolbar.addWidget(btn)
129
+
130
+ # interactor (where we swap interaction styles)
131
+ iren = self.render_window_interactor
132
+
133
+ # keep references so they don’t get garbage collected
134
+ self._drag_style = None
135
+ self._drag_prev_style = None
136
+
137
+ def _flatten(container, out):
138
+ # accept dicts, lists, and single actor objects
139
+ if container is None:
140
+ return
141
+
142
+ if isinstance(container, dict):
143
+ for v in container.values():
144
+ _flatten(v, out)
145
+ return
146
+
147
+ if isinstance(container, (list, tuple, set)):
148
+ for v in container:
149
+ _flatten(v, out)
150
+ return
151
+
152
+ # actor-like object
153
+ if hasattr(container, "GetPosition"):
154
+ out.append(container)
155
+
156
+ def _collect_aircraft_actors():
157
+ # grab actor containers from the widget (whatever exists)
158
+ groups = {
159
+ "Fuselages": getattr(self, "fuselage_actors", None),
160
+ "Nacelles": getattr(self, "nacelle_actors", None),
161
+ "Wings": getattr(self, "wing_actors", None),
162
+ "Rotors": getattr(self, "rotor_actors", None),
163
+ "Booms": getattr(self, "boom_actors", None),
164
+ "Fuel Tanks": getattr(self, "fuel_tank_actors", None),
165
+ }
166
+
167
+ # flatten each container into a simple list
168
+ flat = {}
169
+ for name, container in groups.items():
170
+ lst = []
171
+ _flatten(container, lst)
172
+ flat[name] = lst
173
+
174
+ return flat
175
+
176
+ def toggle_drag(active):
177
+ if active:
178
+ # save current interaction style
179
+ self._drag_prev_style = iren.GetInteractorStyle()
180
+
181
+ # create style once, then reuse it
182
+ if self._drag_style is None:
183
+ self._drag_style = DragAircraftInteractor(self, _collect_aircraft_actors())
184
+ else:
185
+ self._drag_style.actors = _collect_aircraft_actors()
186
+
187
+ # make sure the style knows which renderer to use
188
+ self._drag_style.SetDefaultRenderer(self.renderer)
189
+
190
+ # enable drag style
191
+ iren.SetInteractorStyle(self._drag_style)
192
+
193
+ else:
194
+ # restore previous style
195
+ if self._drag_prev_style is not None:
196
+ iren.SetInteractorStyle(self._drag_prev_style)
197
+ # redraw
198
+ self.vtkWidget.GetRenderWindow().Render()
199
+
200
+ # toggle on/off from toolbar
201
+ btn.toggled.connect(toggle_drag)
202
+
203
+ # patch run_solve to add drag button after solve
204
+ def _patch_drag():
205
+ # patch once
206
+ if getattr(VisualizeGeometryWidget, "_drag_patched", False):
207
+ return
208
+
209
+ old = VisualizeGeometryWidget.run_solve
210
+
211
+ def wrapped(self, *args, **kwargs):
212
+ old(self, *args, **kwargs)
213
+ add_drag_button(self)
214
+
215
+ VisualizeGeometryWidget.run_solve = wrapped
216
+ VisualizeGeometryWidget._drag_patched = True
217
+
218
+ # Activate Patch
219
+ _patch_drag()