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,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()
@@ -0,0 +1,3 @@
1
+ # RCAIDE_GUI/tabs/visualize_geometry/geometry_helper_functions.py
2
+ # Thin shim — re-exports from RCAIDE so that legacy callers keep working.
3
+ from RCAIDE.Library.Plots.Geometry.plot_3d_rotor import generate_vtk_object, write_azimuthal_cell_values