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