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,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()
|