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,685 @@
1
+ # RCAIDE_GUI/tabs/visualize_geometry/features/blueprint.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, QMenu, QWidget, QLabel, QSlider, QVBoxLayout, QWidgetAction
8
+ from PyQt6.QtCore import Qt, QTimer
9
+
10
+ # Python imports
11
+ import vtkmodules.all as vtk
12
+ import os
13
+
14
+ class BlueprintManager:
15
+ # Manages blueprint images (load, show, click-to-select).
16
+ def __init__(self, widget):
17
+ # parent widget (VisualizeGeometryWidget)
18
+ self.widget = widget
19
+
20
+ # vtk renderer (may change after solve)
21
+ self.renderer = getattr(widget, "renderer", None)
22
+
23
+ # blueprint actors by view key: "top" / "side" / "front"
24
+ self.actors = {}
25
+
26
+ # outline actors (only one should be visible at a time)
27
+ self.outlines = {}
28
+
29
+ # currently selected view key
30
+ self.selected_view = None
31
+
32
+ # current settings (sliders can change these)
33
+ self.opacity = 0.5
34
+ self.scale = 1.0
35
+
36
+ # per-view transform objects (used for scaling)
37
+ self.base_transforms = {}
38
+
39
+ # picker for selecting actors by clicking
40
+ self.picker = vtk.vtkPropPicker()
41
+
42
+ # observer id so we only attach click handling once
43
+ self._pick_obs_id = None
44
+
45
+ def _rw(self):
46
+ # get render window
47
+ try:
48
+ return self.widget.vtkWidget.GetRenderWindow()
49
+ except Exception:
50
+ return None
51
+
52
+ def _iren(self):
53
+ # get interactor (mouse events)
54
+ rw = self._rw()
55
+ try:
56
+ return rw.GetInteractor() if rw is not None else None
57
+ except Exception:
58
+ return None
59
+
60
+ def _store_camera(self):
61
+ # save camera so loading blueprints doesn't change the view
62
+ if self.renderer is None:
63
+ return None
64
+
65
+ cam = self.renderer.GetActiveCamera()
66
+ if cam is None:
67
+ return None
68
+
69
+ return {
70
+ "pos": cam.GetPosition(),
71
+ "focal": cam.GetFocalPoint(),
72
+ "viewup": cam.GetViewUp(),
73
+ "clipping": cam.GetClippingRange(),
74
+ }
75
+
76
+ def _restore_camera(self, state):
77
+ # restore camera after adding actor
78
+ if self.renderer is None or not state:
79
+ return
80
+
81
+ cam = self.renderer.GetActiveCamera()
82
+ if cam is None:
83
+ return
84
+
85
+ cam.SetPosition(*state["pos"])
86
+ cam.SetFocalPoint(*state["focal"])
87
+ cam.SetViewUp(*state["viewup"])
88
+ cam.SetClippingRange(*state["clipping"])
89
+
90
+ try:
91
+ self.renderer.ResetCameraClippingRange()
92
+ except Exception:
93
+ pass
94
+
95
+ def _read_image_as_texture(self, file_name):
96
+ # choose png vs jpeg reader
97
+ ext = os.path.splitext(file_name)[1].lower()
98
+ reader = vtk.vtkJPEGReader() if ext in [".jpg", ".jpeg"] else vtk.vtkPNGReader()
99
+
100
+ # read image
101
+ reader.SetFileName(file_name)
102
+ reader.Update()
103
+
104
+ # make texture
105
+ texture = vtk.vtkTexture()
106
+ texture.SetInputConnection(reader.GetOutputPort())
107
+
108
+ # texture options for nicer display
109
+ texture.InterpolateOn()
110
+ texture.RepeatOff()
111
+ texture.EdgeClampOn()
112
+
113
+ return texture, reader.GetOutput()
114
+
115
+ def _model_bounds(self):
116
+ # compute visible bounds (fallback if empty scene)
117
+ if self.renderer is None:
118
+ b = (-1, 1, -1, 1, -1, 1)
119
+ else:
120
+ b = self.renderer.ComputeVisiblePropBounds() or (-1, 1, -1, 1, -1, 1)
121
+
122
+ # center of model bounds
123
+ cx = (b[0] + b[1]) / 2
124
+ cy = (b[2] + b[3]) / 2
125
+ cz = (b[4] + b[5]) / 2
126
+
127
+ # sizes of bounds (avoid zeros)
128
+ sx = (b[1] - b[0]) or 1
129
+ sy = (b[3] - b[2]) or 1
130
+ sz = (b[5] - b[4]) or 1
131
+
132
+ # base scale for spacing
133
+ base = max(sx, sy, sz)
134
+
135
+ return b, (cx, cy, cz), (sx, sy, sz), base
136
+
137
+ def _make_outline(self, actor):
138
+ # build a wireframe cube around actor bounds
139
+ cube = vtk.vtkCubeSource()
140
+ cube.SetBounds(actor.GetBounds())
141
+
142
+ mapper = vtk.vtkPolyDataMapper()
143
+ mapper.SetInputConnection(cube.GetOutputPort())
144
+
145
+ outline = vtk.vtkActor()
146
+ outline.SetMapper(mapper)
147
+
148
+ # outline styling
149
+ outline.GetProperty().SetColor(0.2, 0.6, 1.0)
150
+ outline.GetProperty().SetLineWidth(3)
151
+ outline.GetProperty().LightingOff()
152
+ outline.GetProperty().SetRepresentationToWireframe()
153
+
154
+ return outline
155
+
156
+ def _update_outline(self, view):
157
+ # keep outline matching the actor after transform changes
158
+ if view not in self.actors or view not in self.outlines:
159
+ return
160
+
161
+ actor = self.actors[view]
162
+ outline = self.outlines[view]
163
+
164
+ try:
165
+ cube = outline.GetMapper().GetInputConnection(0, 0).GetProducer()
166
+ cube.SetBounds(actor.GetBounds())
167
+ except Exception:
168
+ pass
169
+
170
+ def _highlight_selected(self, view):
171
+ # renderer required
172
+ if self.renderer is None:
173
+ return
174
+
175
+ # remove all existing outlines
176
+ for o in list(self.outlines.values()):
177
+ try:
178
+ self.renderer.RemoveActor(o)
179
+ except Exception:
180
+ pass
181
+
182
+ # add outline for selected blueprint
183
+ if view in self.actors:
184
+ outline = self._make_outline(self.actors[view])
185
+ self.renderer.AddActor(outline)
186
+ self.outlines[view] = outline
187
+
188
+ # render now
189
+ rw = self._rw()
190
+ if rw is not None:
191
+ try:
192
+ rw.Render()
193
+ except Exception:
194
+ pass
195
+
196
+ def enable_picking(self):
197
+ # refresh renderer reference
198
+ self.renderer = getattr(self.widget, "renderer", None)
199
+
200
+ # need interactor
201
+ iren = self._iren()
202
+ if iren is None:
203
+ return
204
+
205
+ # only attach once
206
+ if self._pick_obs_id is not None:
207
+ return
208
+
209
+ # attach click handler
210
+ try:
211
+ self._pick_obs_id = iren.AddObserver("LeftButtonPressEvent", self._on_click, 1.0)
212
+ except Exception:
213
+ self._pick_obs_id = None
214
+
215
+ def _on_click(self, obj, event):
216
+ # refresh renderer reference
217
+ self.renderer = getattr(self.widget, "renderer", None)
218
+ if self.renderer is None:
219
+ return
220
+
221
+ # get mouse position
222
+ try:
223
+ x, y = obj.GetEventPosition()
224
+ except Exception:
225
+ return
226
+
227
+ # pick an actor
228
+ try:
229
+ self.picker.Pick(x, y, 0, self.renderer)
230
+ picked = self.picker.GetActor()
231
+ except Exception:
232
+ return
233
+
234
+ # ignore empty clicks
235
+ if not picked:
236
+ return
237
+
238
+ # match picked actor to a blueprint view
239
+ for v, a in self.actors.items():
240
+ if a == picked:
241
+ self.selected_view = v
242
+ self._highlight_selected(v)
243
+
244
+ # let widget sync sliders if it has helper
245
+ if hasattr(self.widget, "_reset_blueprint_sliders"):
246
+ try:
247
+ self.widget._reset_blueprint_sliders()
248
+ except Exception:
249
+ pass
250
+ break
251
+
252
+ def load_image(self, view):
253
+ # refresh renderer reference
254
+ self.renderer = getattr(self.widget, "renderer", None)
255
+ if self.renderer is None:
256
+ return
257
+
258
+ # ask user for file
259
+ file_name, _ = QFileDialog.getOpenFileName(
260
+ self.widget,
261
+ f"Select {view.capitalize()} Blueprint",
262
+ "",
263
+ "Image Files (*.png *.jpg *.jpeg)"
264
+ )
265
+ if not file_name:
266
+ return
267
+
268
+ # keep camera stable
269
+ cam_state = self._store_camera()
270
+
271
+ # load texture + image data
272
+ texture, img_data = self._read_image_as_texture(file_name)
273
+
274
+ # get image size
275
+ iw, ih, _ = img_data.GetDimensions()
276
+ if iw == 0 or ih == 0:
277
+ return
278
+
279
+ # preserve aspect ratio
280
+ aspect = iw / ih
281
+
282
+ # get model placement info
283
+ b, (cx, cy, cz), (sx, sy, sz), base = self._model_bounds()
284
+
285
+ # place blueprint a bit outside the model
286
+ spacing = 0.25 * base
287
+
288
+ # choose blueprint plane size
289
+ bp_width = 0.9 * sx
290
+ bp_height = bp_width / aspect
291
+
292
+ # build plane geometry
293
+ plane = vtk.vtkPlaneSource()
294
+
295
+ # place plane for "top"
296
+ if view == "top":
297
+ z = b[4] - spacing
298
+ plane.SetOrigin(cx - bp_width / 2, cy - bp_height / 2, z)
299
+ plane.SetPoint1(cx + bp_width / 2, cy - bp_height / 2, z)
300
+ plane.SetPoint2(cx - bp_width / 2, cy + bp_height / 2, z)
301
+
302
+ # place plane for "side"
303
+ elif view == "side":
304
+ y = b[3] + spacing
305
+ plane.SetOrigin(cx - bp_width / 2, y, cz - bp_height / 2)
306
+ plane.SetPoint1(cx + bp_width / 2, y, cz - bp_height / 2)
307
+ plane.SetPoint2(cx - bp_width / 2, y, cz + bp_height / 2)
308
+
309
+ # place plane for "front"
310
+ elif view == "front":
311
+ x = b[1] + spacing
312
+ plane.SetOrigin(x, cy - bp_width / 2, cz - bp_height / 2)
313
+ plane.SetPoint1(x, cy + bp_width / 2, cz - bp_height / 2)
314
+ plane.SetPoint2(x, cy - bp_width / 2, cz + bp_height / 2)
315
+
316
+ # unknown view key
317
+ else:
318
+ return
319
+
320
+ # map plane geometry
321
+ mapper = vtk.vtkPolyDataMapper()
322
+ mapper.SetInputConnection(plane.GetOutputPort())
323
+
324
+ # create actor
325
+ actor = vtk.vtkActor()
326
+ actor.SetMapper(mapper)
327
+ actor.SetTexture(texture)
328
+
329
+ # basic styling
330
+ actor.GetProperty().SetOpacity(self.opacity)
331
+ actor.GetProperty().LightingOff()
332
+ actor.SetPickable(True)
333
+
334
+ # remove existing actor for this view
335
+ if view in self.actors:
336
+ try:
337
+ self.renderer.RemoveActor(self.actors[view])
338
+ except Exception:
339
+ pass
340
+
341
+ # add actor to scene
342
+ self.renderer.AddActor(actor)
343
+ self.actors[view] = actor
344
+
345
+ # create stored transform (used by scale slider)
346
+ self.base_transforms[view] = vtk.vtkTransform()
347
+ actor.SetUserTransform(self.base_transforms[view])
348
+
349
+ # select this view
350
+ self.selected_view = view
351
+ self._highlight_selected(view)
352
+
353
+ # restore camera
354
+ self._restore_camera(cam_state)
355
+
356
+ # let widget sync sliders if it has helper
357
+ if hasattr(self.widget, "_reset_blueprint_sliders"):
358
+ try:
359
+ self.widget._reset_blueprint_sliders()
360
+ except Exception:
361
+ pass
362
+
363
+ # render now
364
+ rw = self._rw()
365
+ if rw is not None:
366
+ try:
367
+ rw.Render()
368
+ except Exception:
369
+ pass
370
+
371
+ def update_opacity(self, value):
372
+ # slider gives 0..100, convert to 0..1
373
+ self.opacity = value / 100.0
374
+
375
+ # update selected actor only
376
+ if self.selected_view in self.actors:
377
+ try:
378
+ self.actors[self.selected_view].GetProperty().SetOpacity(self.opacity)
379
+ self._rw().Render()
380
+ except Exception:
381
+ pass
382
+
383
+ def update_scale(self, value):
384
+ # slider gives 0..100, convert to 0..1
385
+ self.scale = value / 100.0
386
+
387
+ # update selected actor only
388
+ if self.selected_view in self.actors:
389
+ try:
390
+ transform = self.base_transforms[self.selected_view]
391
+ transform.Identity()
392
+ transform.Scale(self.scale, self.scale, self.scale)
393
+
394
+ self.actors[self.selected_view].SetUserTransform(transform)
395
+ self._update_outline(self.selected_view)
396
+
397
+ self._rw().Render()
398
+ except Exception:
399
+ pass
400
+
401
+ def clear_all(self):
402
+ # nothing to clear if renderer missing
403
+ if self.renderer is None:
404
+ return
405
+
406
+ # remove blueprint actors
407
+ for a in list(self.actors.values()):
408
+ try:
409
+ self.renderer.RemoveActor(a)
410
+ except Exception:
411
+ pass
412
+
413
+ # remove outlines
414
+ for o in list(self.outlines.values()):
415
+ try:
416
+ self.renderer.RemoveActor(o)
417
+ except Exception:
418
+ pass
419
+
420
+ # reset state
421
+ self.actors.clear()
422
+ self.outlines.clear()
423
+ self.selected_view = None
424
+
425
+ # render now
426
+ rw = self._rw()
427
+ if rw is not None:
428
+ try:
429
+ rw.Render()
430
+ except Exception:
431
+ pass
432
+
433
+ def add_blueprint_menu(self):
434
+ # don’t add twice
435
+ if getattr(self, "_blueprint_menu_loaded", False):
436
+ return
437
+ self._blueprint_menu_loaded = True
438
+
439
+ # create the manager (stores state on the widget)
440
+ self.blueprint_manager = BlueprintManager(self)
441
+
442
+ def _add_menu():
443
+ # toolbar must exist
444
+ if not getattr(self, "toolbar", None):
445
+ return
446
+
447
+ # button + menu
448
+ btn = QPushButton("📐 Blueprint")
449
+ menu = QMenu(btn)
450
+
451
+ # load actions
452
+ menu.addAction("Load Top View", lambda: self.blueprint_manager.load_image("top"))
453
+ menu.addAction("Load Side View", lambda: self.blueprint_manager.load_image("side"))
454
+ menu.addAction("Load Front View", lambda: self.blueprint_manager.load_image("front"))
455
+
456
+ # clear action
457
+ menu.addSeparator()
458
+ menu.addAction("🧹 Clear All Blueprints", lambda: self.blueprint_manager.clear_all())
459
+
460
+ # attach menu to button
461
+ btn.setMenu(menu)
462
+ self.toolbar.addWidget(btn)
463
+
464
+ # enable click selection
465
+ self.blueprint_manager.enable_picking()
466
+
467
+ # keep toolbar visible on top
468
+ try:
469
+ self.toolbar.raise_()
470
+ except Exception:
471
+ pass
472
+
473
+ # add the menu after Qt finishes building the toolbar
474
+ QTimer.singleShot(0, _add_menu)
475
+
476
+ # Activate Patch
477
+ def _patch_run_solve_for_blueprints():
478
+ # patch run_solve to add blueprint menu after solve
479
+ if getattr(VisualizeGeometryWidget, "_blueprint_patched", False):
480
+ return
481
+
482
+ base = VisualizeGeometryWidget.run_solve
483
+
484
+ def wrapped(self, *args, **kwargs):
485
+ base(self, *args, **kwargs)
486
+ add_blueprint_menu(self)
487
+
488
+ VisualizeGeometryWidget.run_solve = wrapped
489
+ VisualizeGeometryWidget._blueprint_patched = True
490
+
491
+ _patch_run_solve_for_blueprints()
492
+
493
+ # ================================
494
+ # BLUEPRINT ADJUSTOR DROPDOWN
495
+ # ================================
496
+ # Adds an "Adjustor" button to the toolbar that opens a dropdown menu
497
+ # with sliders to adjust the opacity and size of the selected blueprint.
498
+
499
+ def add_blueprint_adjustor_dropdown(self):
500
+ # don’t add this twice
501
+ if getattr(self, "_blueprint_adjustor_loaded", False):
502
+ return
503
+ self._blueprint_adjustor_loaded = True
504
+
505
+ # create the button that opens the dropdown
506
+ adjustor_btn = QPushButton("Adjustor")
507
+ adjustor_btn.setFlat(True)
508
+
509
+ # style the button to match the toolbar
510
+ adjustor_btn.setStyleSheet("""
511
+ QPushButton {
512
+ color: #a8c7ff;
513
+ background-color: transparent;
514
+ border: 1px solid #3a3f47;
515
+ border-radius: 4px;
516
+ padding: 5px 10px;
517
+ font-weight: 500;
518
+ }
519
+ QPushButton:hover {
520
+ background-color: rgba(74,144,226,25);
521
+ border: 1px solid #4a90e2;
522
+ color: #b7dcff;
523
+ }
524
+ QPushButton:pressed {
525
+ background-color: rgba(74,144,226,60);
526
+ color: #d9e9ff;
527
+ }
528
+ """)
529
+ # menu that shows when you click the button
530
+ menu = QMenu(adjustor_btn)
531
+
532
+ # menu styling
533
+ menu.setStyleSheet("""
534
+ QMenu {
535
+ background-color: #2b2e35;
536
+ color: #dcdfe4;
537
+ border: 1px solid #4a90e2;
538
+ padding: 8px 10px 10px 10px;
539
+ }
540
+ """)
541
+
542
+ # widget that holds the sliders inside the menu
543
+ content = QWidget()
544
+
545
+ # vertical layout for title + sliders
546
+ layout = QVBoxLayout(content)
547
+ layout.setContentsMargins(10, 12, 10, 10)
548
+ layout.setSpacing(10)
549
+
550
+ # shows which blueprint is currently selected
551
+ title = QLabel("Active Blueprint: None")
552
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
553
+ title.setWordWrap(False)
554
+
555
+ # title styling
556
+ title.setStyleSheet("""
557
+ font-weight: bold;
558
+ color: #b7dcff;
559
+ font-size: 13px;
560
+ padding: 4px 2px 6px 2px;
561
+ """)
562
+ layout.addWidget(title)
563
+
564
+ # transparency header
565
+ t_label = QLabel("Transparency")
566
+ t_label.setStyleSheet("font-weight:bold;color:#b7dcff;font-size:12px;")
567
+ layout.addWidget(t_label)
568
+
569
+ # transparency slider (0..100)
570
+ t_slider = QSlider(Qt.Orientation.Horizontal)
571
+ t_slider.setRange(0, 100)
572
+ t_slider.setValue(50)
573
+ layout.addWidget(t_slider)
574
+
575
+ # size header
576
+ s_label = QLabel("Size")
577
+ s_label.setStyleSheet("font-weight:bold;color:#b7dcff;font-size:12px;")
578
+ layout.addWidget(s_label)
579
+
580
+ # size slider (50..200)
581
+ s_slider = QSlider(Qt.Orientation.Horizontal)
582
+ s_slider.setRange(50, 200)
583
+ s_slider.setValue(100)
584
+ layout.addWidget(s_slider)
585
+
586
+ def bind_blueprint_manager():
587
+ # BlueprintManager is created later, so retry until it exists
588
+ if not hasattr(self, "blueprint_manager"):
589
+ QTimer.singleShot(300, bind_blueprint_manager)
590
+ return
591
+
592
+ bm = self.blueprint_manager
593
+
594
+ # store slider values per view ("top"/"side"/"front")
595
+ bm._view_states = {}
596
+
597
+ def refresh_label():
598
+ # update the “Active Blueprint” label
599
+ active = getattr(bm, "selected_view", None) or "None"
600
+ title.setText(f"Active Blueprint: {active.capitalize()}")
601
+
602
+ def on_opacity_change(val):
603
+ # if a blueprint is selected, apply opacity and save the slider value
604
+ if bm.selected_view:
605
+ bm.update_opacity(val)
606
+ bm._view_states.setdefault(bm.selected_view, {})["opacity"] = val
607
+
608
+ def on_scale_change(val):
609
+ # if a blueprint is selected, apply scale and save the slider value
610
+ if bm.selected_view:
611
+ bm.update_scale(val)
612
+ bm._view_states.setdefault(bm.selected_view, {})["scale"] = val
613
+
614
+ # slider -> blueprint updates
615
+ t_slider.valueChanged.connect(on_opacity_change)
616
+ s_slider.valueChanged.connect(on_scale_change)
617
+
618
+ # when you load a blueprint, update label + restore stored slider values
619
+ old_load = bm.load_image
620
+
621
+ def wrapped_load_image(view):
622
+ old_load(view)
623
+ bm.selected_view = view
624
+ refresh_label()
625
+
626
+ state = bm._view_states.get(view, {"opacity": 50, "scale": 100})
627
+ t_slider.setValue(state["opacity"])
628
+ s_slider.setValue(state["scale"])
629
+
630
+ bm.load_image = wrapped_load_image
631
+ # when you click a blueprint, update label + restore stored slider values
632
+ old_click = bm._on_click
633
+
634
+ def wrapped_click(obj, event):
635
+ old_click(obj, event)
636
+ refresh_label()
637
+
638
+ if bm.selected_view:
639
+ state = bm._view_states.get(bm.selected_view, {"opacity": 50, "scale": 100})
640
+ t_slider.setValue(state["opacity"])
641
+ s_slider.setValue(state["scale"])
642
+
643
+ bm._on_click = wrapped_click
644
+
645
+ refresh_label()
646
+
647
+ # start binding (will retry until blueprint_manager exists)
648
+ bind_blueprint_manager()
649
+
650
+ # put the content widget inside the menu
651
+ widget_action = QWidgetAction(menu)
652
+ widget_action.setDefaultWidget(content)
653
+ menu.addAction(widget_action)
654
+
655
+ # attach the menu to the button
656
+ adjustor_btn.setMenu(menu)
657
+
658
+ def insert_adjustor_button():
659
+ # put Adjustor next to the "📐 Blueprint" button in the toolbar
660
+ for act in self.toolbar.actions():
661
+ w = self.toolbar.widgetForAction(act)
662
+ if hasattr(w, "text") and "Blueprint" in w.text():
663
+ idx = self.toolbar.actions().index(act)
664
+
665
+ if idx < len(self.toolbar.actions()) - 1:
666
+ self.toolbar.insertWidget(self.toolbar.actions()[idx + 1], adjustor_btn)
667
+ else:
668
+ self.toolbar.addWidget(adjustor_btn)
669
+ return
670
+
671
+ # retry if toolbar isn’t ready yet
672
+ QTimer.singleShot(500, insert_adjustor_button)
673
+
674
+ insert_adjustor_button()
675
+
676
+ # patch run_solve once so adjustor gets added after solve builds UI
677
+ if not hasattr(VisualizeGeometryWidget, "_blueprint_adjustor_patched"):
678
+ base_run = VisualizeGeometryWidget.run_solve
679
+
680
+ def wrapped_run(self):
681
+ base_run(self)
682
+ add_blueprint_adjustor_dropdown(self)
683
+
684
+ VisualizeGeometryWidget.run_solve = wrapped_run
685
+ VisualizeGeometryWidget._blueprint_adjustor_patched = True