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,194 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/visualize_geometry/features/grid.py
|
|
2
|
+
|
|
3
|
+
# RCAIDE-GUI Imports
|
|
4
|
+
from tabs.visualize_geometry.visualize_geometry import VisualizeGeometryWidget
|
|
5
|
+
|
|
6
|
+
# PyQt imports
|
|
7
|
+
from PyQt6.QtWidgets import QCheckBox
|
|
8
|
+
|
|
9
|
+
# Python imports
|
|
10
|
+
import vtkmodules.all as vtk
|
|
11
|
+
|
|
12
|
+
def add_fullscreen_grid(renderer, divisions=12):
|
|
13
|
+
"""Create a layer-1 overlay renderer that draws a 2D grid across the window.
|
|
14
|
+
|
|
15
|
+
Returns the overlay renderer or None.
|
|
16
|
+
"""
|
|
17
|
+
# If renderer or window isn't available, do nothing
|
|
18
|
+
if renderer is None or not hasattr(renderer, "GetRenderWindow"):
|
|
19
|
+
return None
|
|
20
|
+
win = renderer.GetRenderWindow()
|
|
21
|
+
if win is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
# Get window size; if zero, don't draw
|
|
25
|
+
w, h = win.GetActualSize()
|
|
26
|
+
if not w or not h:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
# Simple grid color and opacity
|
|
30
|
+
color, opacity = (0.6, 0.7, 0.9), 0.25
|
|
31
|
+
|
|
32
|
+
# Determine spacing between grid lines
|
|
33
|
+
step = max(1.0, min(w, h) / float(max(1, divisions)))
|
|
34
|
+
|
|
35
|
+
# Build points and line cells for the grid
|
|
36
|
+
pts = vtk.vtkPoints()
|
|
37
|
+
lines = vtk.vtkCellArray()
|
|
38
|
+
pid = 0
|
|
39
|
+
|
|
40
|
+
# Add vertical grid lines
|
|
41
|
+
for i in range(int(w / step) + 1):
|
|
42
|
+
x = i * step
|
|
43
|
+
pts.InsertNextPoint(x, 0, 0)
|
|
44
|
+
pts.InsertNextPoint(x, h, 0)
|
|
45
|
+
lines.InsertNextCell(2)
|
|
46
|
+
lines.InsertCellPoint(pid)
|
|
47
|
+
lines.InsertCellPoint(pid + 1)
|
|
48
|
+
pid += 2
|
|
49
|
+
|
|
50
|
+
# Add horizontal grid lines
|
|
51
|
+
for j in range(int(h / step) + 1):
|
|
52
|
+
y = j * step
|
|
53
|
+
pts.InsertNextPoint(0, y, 0)
|
|
54
|
+
pts.InsertNextPoint(w, y, 0)
|
|
55
|
+
lines.InsertNextCell(2)
|
|
56
|
+
lines.InsertCellPoint(pid)
|
|
57
|
+
lines.InsertCellPoint(pid + 1)
|
|
58
|
+
pid += 2
|
|
59
|
+
|
|
60
|
+
# Make polydata and a 2D mapper/actor
|
|
61
|
+
grid = vtk.vtkPolyData()
|
|
62
|
+
grid.SetPoints(pts)
|
|
63
|
+
grid.SetLines(lines)
|
|
64
|
+
|
|
65
|
+
mapper = vtk.vtkPolyDataMapper2D()
|
|
66
|
+
mapper.SetInputData(grid)
|
|
67
|
+
|
|
68
|
+
actor = vtk.vtkActor2D()
|
|
69
|
+
actor.SetMapper(mapper)
|
|
70
|
+
actor.GetProperty().SetColor(color)
|
|
71
|
+
actor.GetProperty().SetOpacity(opacity)
|
|
72
|
+
|
|
73
|
+
# Overlay renderer (non-interactive)
|
|
74
|
+
overlay = vtk.vtkRenderer()
|
|
75
|
+
overlay.SetLayer(1)
|
|
76
|
+
overlay.InteractiveOff()
|
|
77
|
+
overlay.AddActor(actor)
|
|
78
|
+
overlay.SetBackgroundAlpha(0.0)
|
|
79
|
+
|
|
80
|
+
# Ensure base layer + overlay layer
|
|
81
|
+
try:
|
|
82
|
+
win.SetNumberOfLayers(2)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
win.AddRenderer(overlay)
|
|
87
|
+
return overlay
|
|
88
|
+
|
|
89
|
+
def _remove_grid_overlay(self):
|
|
90
|
+
"""Remove overlay renderer if present."""
|
|
91
|
+
r = getattr(self, "renderer", None)
|
|
92
|
+
if r is None or not hasattr(r, "GetRenderWindow"):
|
|
93
|
+
self.overlay_grid = None
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
win = r.GetRenderWindow()
|
|
97
|
+
overlay = getattr(self, "overlay_grid", None)
|
|
98
|
+
if overlay is None or win is None:
|
|
99
|
+
self.overlay_grid = None
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
win.RemoveRenderer(overlay)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
self.overlay_grid = None
|
|
108
|
+
|
|
109
|
+
def _rebuild_grid_overlay_if_enabled(self):
|
|
110
|
+
"""Toggle/rebuild grid overlay based on checkbox state."""
|
|
111
|
+
r = getattr(self, "renderer", None)
|
|
112
|
+
if r is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
cb = getattr(self, "grid_checkbox", None)
|
|
116
|
+
if cb is None:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Remove if disabled
|
|
120
|
+
if not cb.isChecked():
|
|
121
|
+
_remove_grid_overlay(self)
|
|
122
|
+
try:
|
|
123
|
+
r.GetRenderWindow().Render()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# Rebuild (remake to match current window size)
|
|
129
|
+
_remove_grid_overlay(self)
|
|
130
|
+
self.overlay_grid = add_fullscreen_grid(r, divisions=12)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
r.GetRenderWindow().Render()
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def _add_gridline_toggle(self):
|
|
138
|
+
"""Add the gridlines checkbox once; safe to call repeatedly."""
|
|
139
|
+
if not hasattr(self, "toolbar") or self.toolbar is None:
|
|
140
|
+
return
|
|
141
|
+
if getattr(self, "_gridline_toggle_added", False):
|
|
142
|
+
return
|
|
143
|
+
if getattr(self, "renderer", None) is None:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
self._gridline_toggle_added = True
|
|
147
|
+
|
|
148
|
+
# Checkbox UI
|
|
149
|
+
self.grid_checkbox = QCheckBox("Show Gridlines")
|
|
150
|
+
self.grid_checkbox.setChecked(False)
|
|
151
|
+
self.grid_checkbox.setStyleSheet("""
|
|
152
|
+
QCheckBox {
|
|
153
|
+
color: white;
|
|
154
|
+
font-size: 10pt;
|
|
155
|
+
border: 1px solid white;
|
|
156
|
+
padding: 2px;
|
|
157
|
+
}
|
|
158
|
+
""")
|
|
159
|
+
|
|
160
|
+
self.toolbar.addSeparator()
|
|
161
|
+
self.toolbar.addWidget(self.grid_checkbox)
|
|
162
|
+
|
|
163
|
+
# Toggle behavior
|
|
164
|
+
self.grid_checkbox.stateChanged.connect(lambda _s: _rebuild_grid_overlay_if_enabled(self))
|
|
165
|
+
|
|
166
|
+
# Resize observer (install once)
|
|
167
|
+
if not getattr(self, "_grid_resize_observer_added", False):
|
|
168
|
+
self._grid_resize_observer_added = True
|
|
169
|
+
try:
|
|
170
|
+
win = self.renderer.GetRenderWindow()
|
|
171
|
+
|
|
172
|
+
def on_resize(_o, _e):
|
|
173
|
+
_rebuild_grid_overlay_if_enabled(self)
|
|
174
|
+
|
|
175
|
+
win.AddObserver("WindowResizeEvent", on_resize)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
# Activate Patch
|
|
180
|
+
def _patch_update_toolbar_for_gridlines():
|
|
181
|
+
"""Patch update_toolbar once (no run_solve wrapper stacking)."""
|
|
182
|
+
if getattr(VisualizeGeometryWidget, "_grid_update_toolbar_patched", False):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
old_update_toolbar = VisualizeGeometryWidget.update_toolbar
|
|
186
|
+
|
|
187
|
+
def wrapped_update_toolbar(self, *args, **kwargs):
|
|
188
|
+
old_update_toolbar(self, *args, **kwargs)
|
|
189
|
+
_add_gridline_toggle(self)
|
|
190
|
+
|
|
191
|
+
VisualizeGeometryWidget.update_toolbar = wrapped_update_toolbar
|
|
192
|
+
VisualizeGeometryWidget._grid_update_toolbar_patched = True
|
|
193
|
+
|
|
194
|
+
_patch_update_toolbar_for_gridlines()
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/visualize_geometry/features/measurement.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 math
|
|
11
|
+
import vtkmodules.all as vtk
|
|
12
|
+
|
|
13
|
+
def add_measure_tool(self):
|
|
14
|
+
"""
|
|
15
|
+
Injects Measure / Undo / Clear controls into the existing toolbar and wires
|
|
16
|
+
VTK click-picking to create measurement annotations.
|
|
17
|
+
|
|
18
|
+
Safe to call repeatedly; will not duplicate buttons for a live widget instance.
|
|
19
|
+
"""
|
|
20
|
+
# --- Preconditions ---
|
|
21
|
+
# Need a toolbar (to add buttons), a renderer (to place VTK actors), and a VTK widget (render window/interactor).
|
|
22
|
+
if not getattr(self, "toolbar", None) or getattr(self, "renderer", None) is None:
|
|
23
|
+
return
|
|
24
|
+
if not getattr(self, "vtkWidget", None):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# --- Idempotency guard ---
|
|
28
|
+
# update_toolbar can be called multiple times; if buttons already exist and are valid, do nothing.
|
|
29
|
+
btn_existing = getattr(self, "measure_button", None)
|
|
30
|
+
if btn_existing is not None:
|
|
31
|
+
try:
|
|
32
|
+
_ = btn_existing.isChecked() # if this works, button is still alive
|
|
33
|
+
return
|
|
34
|
+
except Exception:
|
|
35
|
+
# Button object was destroyed (e.g., UI rebuild); drop refs so we recreate below.
|
|
36
|
+
self.measure_button = None
|
|
37
|
+
|
|
38
|
+
# --- UI controls ---
|
|
39
|
+
btn = QPushButton("📏 Measure")
|
|
40
|
+
btn.setCheckable(True) # toggle on/off click observer
|
|
41
|
+
|
|
42
|
+
undo = QPushButton("↩ Undo") # remove last completed measurement
|
|
43
|
+
clr = QPushButton("🧹 Clear") # remove all measurements
|
|
44
|
+
|
|
45
|
+
# Add them to the existing toolbar
|
|
46
|
+
self.toolbar.addSeparator()
|
|
47
|
+
self.toolbar.addWidget(btn)
|
|
48
|
+
self.toolbar.addWidget(undo)
|
|
49
|
+
self.toolbar.addWidget(clr)
|
|
50
|
+
|
|
51
|
+
# Keep references on the widget so we can detect duplicates and wire events once.
|
|
52
|
+
self.measure_button = btn
|
|
53
|
+
self.measure_undo_button = undo
|
|
54
|
+
self.measure_clear_button = clr
|
|
55
|
+
|
|
56
|
+
# --- Persistent tool state ---
|
|
57
|
+
# State is stored on the widget instance, not in globals, so multiple VG widgets won't conflict.
|
|
58
|
+
if not hasattr(self, "_measure_state") or self._measure_state is None:
|
|
59
|
+
self._measure_state = {
|
|
60
|
+
"picker": vtk.vtkCellPicker(), # screen -> world pick
|
|
61
|
+
"pts": [], # in-progress points: [(p, dot_actor), ...]
|
|
62
|
+
"temp_dots": [], # actors created before a measurement is finalized
|
|
63
|
+
"measures": [], # completed measurements: [{"line":..., "txt":..., "d1":..., "d2":...}, ...]
|
|
64
|
+
"obs": None, # VTK observer id for LeftButtonPressEvent
|
|
65
|
+
}
|
|
66
|
+
self._measure_state["picker"].SetTolerance(0.0005)
|
|
67
|
+
|
|
68
|
+
st = self._measure_state
|
|
69
|
+
|
|
70
|
+
# --- Small helpers (render window / interactor) ---
|
|
71
|
+
def _rw():
|
|
72
|
+
"""Safely get the RenderWindow."""
|
|
73
|
+
try:
|
|
74
|
+
return self.vtkWidget.GetRenderWindow()
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def _iren():
|
|
79
|
+
"""
|
|
80
|
+
Safely get the interactor.
|
|
81
|
+
Prefer an existing attribute if your widget sets it; otherwise pull from RenderWindow.
|
|
82
|
+
"""
|
|
83
|
+
iren = getattr(self, "render_window_interactor", None)
|
|
84
|
+
if iren is not None:
|
|
85
|
+
return iren
|
|
86
|
+
rw = _rw()
|
|
87
|
+
return rw.GetInteractor() if rw is not None else None
|
|
88
|
+
|
|
89
|
+
# --- Scale helpers (dot radius relative to scene size) ---
|
|
90
|
+
def _scene_diag():
|
|
91
|
+
"""
|
|
92
|
+
Estimate a characteristic scene size so dot radius scales with the model.
|
|
93
|
+
Falls back to 1.0 if bounds are missing/invalid.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
b = self.renderer.ComputeVisiblePropBounds()
|
|
97
|
+
except Exception:
|
|
98
|
+
b = None
|
|
99
|
+
|
|
100
|
+
if not b or len(b) != 6:
|
|
101
|
+
return 1.0
|
|
102
|
+
|
|
103
|
+
dx = (b[1] - b[0])
|
|
104
|
+
dy = (b[3] - b[2])
|
|
105
|
+
dz = (b[5] - b[4])
|
|
106
|
+
s = max(dx, dy, dz)
|
|
107
|
+
return s if s > 0 else 1.0
|
|
108
|
+
|
|
109
|
+
# --- VTK actor constructors ---
|
|
110
|
+
def _make_dot(p, color):
|
|
111
|
+
"""Create a small sphere marker at world point p."""
|
|
112
|
+
r = 0.0075 * _scene_diag()
|
|
113
|
+
|
|
114
|
+
s = vtk.vtkSphereSource()
|
|
115
|
+
s.SetRadius(r)
|
|
116
|
+
|
|
117
|
+
m = vtk.vtkPolyDataMapper()
|
|
118
|
+
m.SetInputConnection(s.GetOutputPort())
|
|
119
|
+
|
|
120
|
+
a = vtk.vtkActor()
|
|
121
|
+
a.SetMapper(m)
|
|
122
|
+
a.SetPosition(p[0], p[1], p[2])
|
|
123
|
+
a.GetProperty().SetColor(color[0], color[1], color[2])
|
|
124
|
+
a.GetProperty().LightingOff()
|
|
125
|
+
|
|
126
|
+
self.renderer.AddActor(a)
|
|
127
|
+
return a
|
|
128
|
+
|
|
129
|
+
def _make_line(p1, p2):
|
|
130
|
+
"""Create a line actor between p1 and p2."""
|
|
131
|
+
l = vtk.vtkLineSource()
|
|
132
|
+
l.SetPoint1(p1)
|
|
133
|
+
l.SetPoint2(p2)
|
|
134
|
+
|
|
135
|
+
m = vtk.vtkPolyDataMapper()
|
|
136
|
+
m.SetInputConnection(l.GetOutputPort())
|
|
137
|
+
|
|
138
|
+
a = vtk.vtkActor()
|
|
139
|
+
a.SetMapper(m)
|
|
140
|
+
|
|
141
|
+
prop = a.GetProperty()
|
|
142
|
+
prop.SetColor(1.0, 1.0, 0.0)
|
|
143
|
+
prop.SetLineWidth(3.5)
|
|
144
|
+
prop.LightingOff()
|
|
145
|
+
|
|
146
|
+
self.renderer.AddActor(a)
|
|
147
|
+
return a
|
|
148
|
+
|
|
149
|
+
def _make_label(p1, p2, L):
|
|
150
|
+
"""Create a 2D text label anchored in world-space at the midpoint."""
|
|
151
|
+
mid = (
|
|
152
|
+
(p1[0] + p2[0]) * 0.5,
|
|
153
|
+
(p1[1] + p2[1]) * 0.5,
|
|
154
|
+
(p1[2] + p2[2]) * 0.5,
|
|
155
|
+
)
|
|
156
|
+
txt = f"{L:.3f} m"
|
|
157
|
+
|
|
158
|
+
coord = vtk.vtkCoordinate()
|
|
159
|
+
coord.SetCoordinateSystemToWorld()
|
|
160
|
+
coord.SetValue(mid[0], mid[1], mid[2])
|
|
161
|
+
|
|
162
|
+
t = vtk.vtkTextActor()
|
|
163
|
+
t.SetInput(txt)
|
|
164
|
+
|
|
165
|
+
tp = t.GetTextProperty()
|
|
166
|
+
tp.SetFontSize(26)
|
|
167
|
+
tp.SetBold(True)
|
|
168
|
+
tp.SetColor(0.4, 1.0, 1.0)
|
|
169
|
+
tp.SetShadow(True)
|
|
170
|
+
tp.SetShadowOffset(2, -2)
|
|
171
|
+
tp.SetJustificationToCentered()
|
|
172
|
+
tp.SetVerticalJustificationToCentered()
|
|
173
|
+
|
|
174
|
+
# Anchor the 2D text actor to a world coordinate
|
|
175
|
+
t.GetPositionCoordinate().SetReferenceCoordinate(coord)
|
|
176
|
+
t.GetPositionCoordinate().SetCoordinateSystemToWorld()
|
|
177
|
+
|
|
178
|
+
self.renderer.AddActor2D(t)
|
|
179
|
+
return t
|
|
180
|
+
|
|
181
|
+
# --- Render + cleanup helpers ---
|
|
182
|
+
def _render():
|
|
183
|
+
"""Request a render, safely."""
|
|
184
|
+
rw = _rw()
|
|
185
|
+
if rw is None:
|
|
186
|
+
return
|
|
187
|
+
try:
|
|
188
|
+
rw.Render()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
def _remove_temp():
|
|
193
|
+
"""
|
|
194
|
+
Remove any in-progress markers (first click / partial measurement)
|
|
195
|
+
and reset the in-progress points list.
|
|
196
|
+
"""
|
|
197
|
+
for d in st["temp_dots"]:
|
|
198
|
+
try:
|
|
199
|
+
self.renderer.RemoveActor(d)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
st["temp_dots"].clear()
|
|
203
|
+
st["pts"].clear()
|
|
204
|
+
|
|
205
|
+
# --- Main click handler ---
|
|
206
|
+
def _on_click(_obj, _evt):
|
|
207
|
+
"""
|
|
208
|
+
If Measure is enabled:
|
|
209
|
+
- pick a world point
|
|
210
|
+
- drop a dot
|
|
211
|
+
- after 2 points: create line + label and commit as a measurement
|
|
212
|
+
"""
|
|
213
|
+
if not btn.isChecked():
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
iren = _iren()
|
|
217
|
+
if iren is None:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
x, y = iren.GetEventPosition()
|
|
221
|
+
|
|
222
|
+
# vtkCellPicker.Pick returns truthy when it hits something in the renderer
|
|
223
|
+
if not st["picker"].Pick(x, y, 0, self.renderer):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
p = st["picker"].GetPickPosition()
|
|
227
|
+
|
|
228
|
+
# Color convention: first point cyan-ish, second point red-ish
|
|
229
|
+
color = (0.0, 0.7, 1.0) if len(st["pts"]) == 0 else (1.0, 0.2, 0.2)
|
|
230
|
+
dot = _make_dot(p, color)
|
|
231
|
+
|
|
232
|
+
st["pts"].append(((p[0], p[1], p[2]), dot))
|
|
233
|
+
st["temp_dots"].append(dot)
|
|
234
|
+
|
|
235
|
+
# If we have two points, finalize measurement
|
|
236
|
+
if len(st["pts"]) == 2:
|
|
237
|
+
(p1, d1), (p2, d2) = st["pts"]
|
|
238
|
+
|
|
239
|
+
L = math.dist(p1, p2)
|
|
240
|
+
line = _make_line(p1, p2)
|
|
241
|
+
label = _make_label(p1, p2, L)
|
|
242
|
+
|
|
243
|
+
st["measures"].append({"line": line, "txt": label, "d1": d1, "d2": d2})
|
|
244
|
+
|
|
245
|
+
# Reset in-progress state so next pair starts fresh
|
|
246
|
+
st["pts"].clear()
|
|
247
|
+
st["temp_dots"].clear()
|
|
248
|
+
_render()
|
|
249
|
+
|
|
250
|
+
# --- Toggle wiring (observer attach/detach) ---
|
|
251
|
+
def _toggle(on: bool):
|
|
252
|
+
"""
|
|
253
|
+
Turn measurement mode on/off by adding/removing a VTK observer.
|
|
254
|
+
Also cleans up partial state when turning off.
|
|
255
|
+
"""
|
|
256
|
+
iren = _iren()
|
|
257
|
+
if iren is None:
|
|
258
|
+
btn.setChecked(False)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
if on:
|
|
262
|
+
# Add observer once; keep id so we can remove it later
|
|
263
|
+
if st["obs"] is None:
|
|
264
|
+
st["obs"] = iren.AddObserver("LeftButtonPressEvent", _on_click, 1.0)
|
|
265
|
+
else:
|
|
266
|
+
# Remove observer and clear any partial click state
|
|
267
|
+
if st["obs"] is not None:
|
|
268
|
+
try:
|
|
269
|
+
iren.RemoveObserver(st["obs"])
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
st["obs"] = None
|
|
273
|
+
_remove_temp()
|
|
274
|
+
|
|
275
|
+
_render()
|
|
276
|
+
|
|
277
|
+
# --- Clear/Undo actions ---
|
|
278
|
+
def _do_clear():
|
|
279
|
+
"""Remove all measurements + any in-progress markers."""
|
|
280
|
+
_remove_temp()
|
|
281
|
+
|
|
282
|
+
for m in st["measures"]:
|
|
283
|
+
# Line
|
|
284
|
+
try:
|
|
285
|
+
self.renderer.RemoveActor(m["line"])
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
# Endpoints
|
|
290
|
+
try:
|
|
291
|
+
self.renderer.RemoveActor(m["d1"])
|
|
292
|
+
self.renderer.RemoveActor(m["d2"])
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
# Label (Actor2D)
|
|
297
|
+
try:
|
|
298
|
+
self.renderer.RemoveActor2D(m["txt"])
|
|
299
|
+
except Exception:
|
|
300
|
+
# Fallback if someone added it as a 3D actor in other variants
|
|
301
|
+
try:
|
|
302
|
+
self.renderer.RemoveActor(m["txt"])
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
st["measures"].clear()
|
|
307
|
+
_render()
|
|
308
|
+
|
|
309
|
+
def _do_undo():
|
|
310
|
+
"""Remove the most recent completed measurement."""
|
|
311
|
+
_remove_temp()
|
|
312
|
+
|
|
313
|
+
if not st["measures"]:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
m = st["measures"].pop()
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
self.renderer.RemoveActor(m["line"])
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
self.renderer.RemoveActor(m["d1"])
|
|
325
|
+
self.renderer.RemoveActor(m["d2"])
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
self.renderer.RemoveActor2D(m["txt"])
|
|
331
|
+
except Exception:
|
|
332
|
+
try:
|
|
333
|
+
self.renderer.RemoveActor(m["txt"])
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
_render()
|
|
338
|
+
|
|
339
|
+
# --- Qt signal hookups ---
|
|
340
|
+
btn.toggled.connect(_toggle)
|
|
341
|
+
clr.clicked.connect(_do_clear)
|
|
342
|
+
undo.clicked.connect(_do_undo)
|
|
343
|
+
|
|
344
|
+
# Activate Patch
|
|
345
|
+
def _patch_update_toolbar_for_measure_tool():
|
|
346
|
+
"""
|
|
347
|
+
- Wrap VisualizeGeometryWidget.update_toolbar so the measure tool is injected
|
|
348
|
+
after the toolbar is created/refreshed.
|
|
349
|
+
- Does not touch run_solve or any other global patches.
|
|
350
|
+
"""
|
|
351
|
+
# Guard so we only patch the class once per process
|
|
352
|
+
if getattr(VisualizeGeometryWidget, "_measure_update_toolbar_patched", False):
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
old_update_toolbar = VisualizeGeometryWidget.update_toolbar
|
|
356
|
+
|
|
357
|
+
def wrapped_update_toolbar(self, *args, **kwargs):
|
|
358
|
+
# 1) Build the toolbar using the original implementation
|
|
359
|
+
old_update_toolbar(self, *args, **kwargs)
|
|
360
|
+
# 2) Add measurement controls (idempotent per widget instance)
|
|
361
|
+
add_measure_tool(self)
|
|
362
|
+
|
|
363
|
+
VisualizeGeometryWidget.update_toolbar = wrapped_update_toolbar
|
|
364
|
+
VisualizeGeometryWidget._measure_update_toolbar_patched = True
|
|
365
|
+
|
|
366
|
+
# Activate patch at import time so any subsequently created VG widget gets the tool.
|
|
367
|
+
_patch_update_toolbar_for_measure_tool()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/visualize_geometry/features/screenshot.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, QFileDialog, QMessageBox
|
|
8
|
+
|
|
9
|
+
# Python imports
|
|
10
|
+
import datetime
|
|
11
|
+
import vtkmodules.all as vtk
|
|
12
|
+
|
|
13
|
+
def add_screenshot_button(self):
|
|
14
|
+
# --- need toolbar + vtkWidget ---
|
|
15
|
+
if not getattr(self, "toolbar", None) or not getattr(self, "vtkWidget", None):
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
# --- prevents adding the button twice ---
|
|
19
|
+
if getattr(self, "screenshot_button", None) is not None:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# --- add button to toolbar ---
|
|
23
|
+
btn = QPushButton("📷 Export View")
|
|
24
|
+
self.toolbar.addSeparator()
|
|
25
|
+
self.toolbar.addWidget(btn)
|
|
26
|
+
self.screenshot_button = btn
|
|
27
|
+
|
|
28
|
+
def save_screenshot():
|
|
29
|
+
# --- choose filename (default timestamp) ---
|
|
30
|
+
ts = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
31
|
+
default_name = f"screenshot_{ts}.png"
|
|
32
|
+
|
|
33
|
+
path, selected_filter = QFileDialog.getSaveFileName(
|
|
34
|
+
self,
|
|
35
|
+
"Save Screenshot As",
|
|
36
|
+
default_name,
|
|
37
|
+
"PNG Files (*.png);;JPEG Files (*.jpg *.jpeg);;All Files (*)",
|
|
38
|
+
)
|
|
39
|
+
if not path:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# --- add extension if missing ---
|
|
43
|
+
lower = path.lower()
|
|
44
|
+
if not (lower.endswith(".png") or lower.endswith(".jpg") or lower.endswith(".jpeg")):
|
|
45
|
+
path += ".jpg" if "JPEG" in selected_filter else ".png"
|
|
46
|
+
|
|
47
|
+
# --- grab current render window image ---
|
|
48
|
+
rw = self.vtkWidget.GetRenderWindow()
|
|
49
|
+
w2i = vtk.vtkWindowToImageFilter()
|
|
50
|
+
w2i.SetInput(rw)
|
|
51
|
+
w2i.ReadFrontBufferOff()
|
|
52
|
+
w2i.SetInputBufferTypeToRGBA()
|
|
53
|
+
w2i.Update()
|
|
54
|
+
|
|
55
|
+
# --- pick writer based on extension ---
|
|
56
|
+
if path.lower().endswith((".jpg", ".jpeg")):
|
|
57
|
+
writer = vtk.vtkJPEGWriter()
|
|
58
|
+
else:
|
|
59
|
+
writer = vtk.vtkPNGWriter()
|
|
60
|
+
|
|
61
|
+
# --- write file ---
|
|
62
|
+
writer.SetFileName(path)
|
|
63
|
+
writer.SetInputConnection(w2i.GetOutputPort())
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
writer.Write()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
QMessageBox.critical(self, "Export Failed", f"Could not save screenshot:\n{e}")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
QMessageBox.information(self, "Export Successful", f"Saved:\n{path}")
|
|
72
|
+
|
|
73
|
+
# --- connect click ---
|
|
74
|
+
btn.clicked.connect(save_screenshot)
|
|
75
|
+
|
|
76
|
+
# Activate Patch
|
|
77
|
+
def _patch_update_toolbar_for_screenshot():
|
|
78
|
+
# --- patch update_toolbar to add screenshot button ---
|
|
79
|
+
if getattr(VisualizeGeometryWidget, "_screenshot_update_toolbar_patched", False):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
old_update_toolbar = VisualizeGeometryWidget.update_toolbar
|
|
83
|
+
|
|
84
|
+
def wrapped_update_toolbar(self, *args, **kwargs):
|
|
85
|
+
old_update_toolbar(self, *args, **kwargs)
|
|
86
|
+
add_screenshot_button(self)
|
|
87
|
+
|
|
88
|
+
VisualizeGeometryWidget.update_toolbar = wrapped_update_toolbar
|
|
89
|
+
VisualizeGeometryWidget._screenshot_update_toolbar_patched = True
|
|
90
|
+
|
|
91
|
+
_patch_update_toolbar_for_screenshot()
|