# RCAIDE/Library/Plots/Geometry/plot_3d_vehicle.py
#
#
# Created: Jul 2023, M. Clarke
# ----------------------------------------------------------------------------------------------------------------------
# IMPORT
# ----------------------------------------------------------------------------------------------------------------------
import RCAIDE
from RCAIDE.Library.Components import Component
from RCAIDE.Library.Plots.Geometry.generate_3d_wing_points import *
from RCAIDE.Library.Plots.Geometry.generate_3d_fuselage_points import *
from RCAIDE.Library.Plots.Geometry.generate_3d_fuel_tank_points import *
from RCAIDE.Library.Plots.Geometry.plot_3d_rotor import generate_3d_blade_points, generate_vtk_object
from RCAIDE.Library.Plots.Geometry.generate_3d_nacelle_points import *
from RCAIDE.Library.Plots.Geometry.generate_3d_lopa_points import generate_3d_lopa_points
from RCAIDE.Library.Plots.Geometry.generate_3d_torus_points import generate_3d_torus_points
from RCAIDE.Library.Plots.Geometry.generate_3d_cuboid_points import generate_3d_cuboid_points
from RCAIDE.Library.Plots.Geometry.generate_3d_propulsor_points import generate_3d_propulsor_points
from RCAIDE.Library.Plots.Geometry.generate_3d_cargo_bay_points import generate_3d_cargo_bay_points
from RCAIDE.Library.Plots.Geometry.generate_3d_cabin_points import generate_3d_cabin_points
from RCAIDE.Library.Methods.Geometry.Planform import fuselage_planform, wing_planform , compute_fuel_volume
from RCAIDE.Library.Methods.Geometry.LOPA import compute_layout_of_passenger_accommodations
# python imports
import numpy as np
from copy import deepcopy
import pyvista as pv
import matplotlib.colors as mcolors
# ----------------------------------------------------------------------------------------------------------------------
# PLOTS
# ----------------------------------------------------------------------------------------------------------------------
[docs]
def plot_3d_vehicle(vehicle,
save_figure = False,
save_filename = "geometry",
top_view = False,
side_view = False,
front_view = False,
plot_centerline = False,
wing_color = 'grey',
fuselage_color = 'grey',
boom_color = 'grey',
nacelle_color = 'grey',
fuel_tank_color = 'orange',
rotor_color = 'black',
cargo_bay_color = 'blue',
battery_color = 'green',
systems_color = 'black',
propulsor_color = 'black',
cabin_color = 'grey',
landing_gear_color = 'grey',
plot_actuator_disc = False,
show_LOPA = True,
show_Cabin = True,
wing_opacity = 0.5,
fuselage_opacity = 0.5,
boom_opacity = 0.5,
nacelle_opacity = 0.5,
fuel_tank_opacity = 0.5,
lopa_opacity = 1.0,
rotor_opacity = 0.6,
cargo_bay_opacity = 0.6,
battery_opacity = 1.0,
propulsor_opacity = 0.5,
cabin_opacity = 0.75,
systems_opacity = 0.8,
landing_gear_opacity = 1.0,
number_of_airfoil_points = 101,
tessellation = 96,
camera_eye_x = -1,
camera_eye_y = -1,
camera_eye_z = 0.75,
overwrite_geometry = True,
export_gltf = False,
show_figure = True):
"""
Creates a complete 3D visualization of an aircraft including all major components.
Parameters
----------
geometry : geometry
RCAIDE geometry data structure containing all component geometries
show_axis : bool, optional
Flag to display coordinate axes (default: False)
save_figure : bool, optional
Flag for saving the figure (default: False)
save_filename : str, optional
Name of file for saved figure (default: "Vehicle_Geometry")
alpha : float, optional
Transparency value between 0 and 1 (default: 1.0)
camera_eye_x : float, optional
Camera eye x-position (default: -1.5)
camera_eye_y : float, optional
Camera eye y-position (default: -1.5)
camera_eye_z : float, optional
Camera eye z-position (default: 0.8)
camera_center_x : float, optional
Camera target x-position (default: 0.0)
camera_center_y : float, optional
Camera target y-position (default: 0.0)
camera_center_z : float, optional
Camera target z-position (default: -0.5)
show_figure : bool, optional
Flag to display the figure (default: True)
Returns
-------
None
Notes
-----
Creates an interactive 3D visualization showing:
- Wings and control surfaces
- Fuselage sections
- Propulsion systems
- Customizable view and camera angles
"""
# -------------------------------------------------------------------------
# Initalize Renderer
# -------------------------------------------------------------------------
if save_figure:
plotter = pv.Plotter(off_screen=True)
else:
plotter = pv.Plotter()
# -------------------------------------------------------------------------
# Object RGB Colors
# -------------------------------------------------------------------------
fuel_tank_rgb_color = mcolors.to_rgb(fuel_tank_color)
wing_rgb_color = mcolors.to_rgb(wing_color)
fuselage_rgb_color = mcolors.to_rgb(fuselage_color)
nacelle_rgb_color = mcolors.to_rgb(nacelle_color)
rotor_rgb_color = mcolors.to_rgb(rotor_color)
boom_rgb_color = mcolors.to_rgb(boom_color)
cargo_bay_rgb_color = mcolors.to_rgb(cargo_bay_color)
battery_rgb_color = mcolors.to_rgb(battery_color)
system_rgb_color = mcolors.to_rgb(systems_color)
propulsor_rgb_color = mcolors.to_rgb(propulsor_color)
cabin_rgb_color = mcolors.to_rgb(cabin_color)
landing_gear_rgb_color = mcolors.to_rgb(landing_gear_color)
# -------------------------------------------------------------------------
# Run Geometry Analysis
# -------------------------------------------------------------------------
L = 0
geometry = deepcopy(vehicle)
for wing in geometry.wings:
if isinstance(wing, RCAIDE.Library.Components.Wings.Blended_Wing_Body):
if overwrite_geometry:
wing_planform(wing)
compute_layout_of_passenger_accommodations(wing)
else:
if overwrite_geometry:
wing_planform(wing)
L = np.maximum(L, wing.spans.projected)
compute_fuel_volume(geometry, compute_fuel_volume=True)
for fuselage in geometry.fuselages:
compute_layout_of_passenger_accommodations(fuselage)
fuselage_planform(fuselage)
L = np.maximum(L, fuselage.lengths.total)
# -------------------------------------------------------------------------
# Plot landing gear
# -------------------------------------------------------------------------
for landing_gear in geometry.landing_gears:
N_t = landing_gear.number_of_gear_types_in_tandem
N_w = landing_gear.number_of_wheels_in_gear_type
D = landing_gear.tire_diameter
d = landing_gear.rim_diameter
w = landing_gear.tire_width
strut_length = landing_gear.strut_length
gear_origin = landing_gear.origin[0] # [x, y, z] attachment point on aircraft
# longitudinal spacing between wheels in the same gear type
longitudinal_spacing = landing_gear.longitudinal_wheel_spacing * (N_t - 1) if N_t > 1 else 0
total_wheel_x_span = D * (N_t - 1) + longitudinal_spacing
wheel_x_offsets = np.linspace(-total_wheel_x_span / 2, total_wheel_x_span / 2, N_t) if N_t > 1 else np.array([0.0])
# lateral spacing between gear types in tandem
total_wheel_y_span = w * (N_w - 1) + landing_gear.lateral_wheel_spacing * (N_w - 1) if N_w > 1 else 0
wheel_y_offsets = np.linspace(-total_wheel_y_span / 2, total_wheel_y_span / 2, N_w) if N_w > 1 else np.array([0.0])
for i in range(N_t):
for j in range(N_w):
wheel_origin = [
gear_origin[0] + wheel_x_offsets[i],
gear_origin[1] + wheel_y_offsets[j],
gear_origin[2] - strut_length,
]
pts = generate_3d_torus_points(wheel_origin, D, d, w, tessellation=24)
plotter.add_mesh(generate_vtk_object(pts), color=landing_gear_rgb_color, opacity=landing_gear_opacity)
if landing_gear.xz_plane_symmetric:
wheel_origin[1] = -wheel_origin[1]
pts = generate_3d_torus_points(wheel_origin, D, d, w, tessellation=24)
plotter.add_mesh(generate_vtk_object(pts), color=landing_gear_rgb_color, opacity=landing_gear_opacity)
# -------------------------------------------------------------------------
# Plot wings
# -------------------------------------------------------------------------
for wing in geometry.wings:
GEOM = generate_3d_wing_points(wing, number_of_airfoil_points, plot_centerline=False)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=wing_rgb_color, opacity=wing_opacity)
if wing.yz_plane_symmetric:
GEOM.PTS[:, :, 0] = -GEOM.PTS[:, :, 0]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=wing_rgb_color, opacity=wing_opacity)
if wing.xz_plane_symmetric:
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=wing_rgb_color, opacity=wing_opacity)
if wing.xy_plane_symmetric:
GEOM.PTS[:, :, 2] = -GEOM.PTS[:, :, 2]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=wing_rgb_color, opacity=wing_opacity)
if isinstance(wing, RCAIDE.Library.Components.Wings.Blended_Wing_Body):
if show_LOPA:
if len(wing.cabins) > 0 and len(list(wing.cabins.values())[0].segments_bounding_cabin) > 1:
lopa_geom = generate_3d_lopa_points(wing)
add_lopa_seats(plotter, lopa_geom, lopa_opacity)
if show_Cabin and len(wing.cabins) > 0:
GEOM = generate_3d_cabin_points(wing, number_of_airfoil_points, plot_centerline=False)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=cabin_rgb_color, opacity=cabin_opacity)
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=cabin_rgb_color, opacity=cabin_opacity)
# -------------------------------------------------------------------------
# Plot fuselage
# -------------------------------------------------------------------------
for fuselage in geometry.fuselages:
GEOM = generate_3d_fuselage_points(fuselage, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuselage_rgb_color, opacity=fuselage_opacity)
if show_Cabin:
if len(fuselage.cabins) > 0 and len(list(fuselage.cabins.values())[0].segments_bounding_cabin) > 1:
GEOM = generate_3d_cabin_points(fuselage, number_of_airfoil_points, plot_centerline=False)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=cabin_rgb_color, opacity=cabin_opacity)
if show_LOPA:
lopa_geom = generate_3d_lopa_points(fuselage)
add_lopa_seats(plotter, lopa_geom, lopa_opacity)
# -------------------------------------------------------------------------
# Plot systems
# -------------------------------------------------------------------------
for system in vehicle.systems:
if isinstance(system, Component):
GEOM = generate_3d_cuboid_points(system)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=system_rgb_color, opacity=systems_opacity)
# -------------------------------------------------------------------------
# Plot cargo bay
# -------------------------------------------------------------------------
for cargo_bay in geometry.cargo_bays:
GEOM = generate_3d_cargo_bay_points(cargo_bay)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=cargo_bay_rgb_color, opacity=cargo_bay_opacity)
# -------------------------------------------------------------------------
# Plot boom
# -------------------------------------------------------------------------
for boom in geometry.booms:
GEOM = generate_3d_fuselage_points(boom, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=boom_rgb_color, opacity=boom_opacity)
# -------------------------------------------------------------------------
# Plot Nacelle, Rotors and Fuel Tanks
# -------------------------------------------------------------------------
for network in geometry.networks:
for propulsor in network.propulsors:
if type(propulsor) in (RCAIDE.Library.Components.Powertrain.Propulsors.Turbofan,
RCAIDE.Library.Components.Powertrain.Propulsors.Turbojet,
RCAIDE.Library.Components.Powertrain.Propulsors.Turboprop):
GEOM = generate_3d_propulsor_points(propulsor, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=propulsor_rgb_color, opacity=propulsor_opacity)
# if nacelle geometry is defined, plot nacelle
if propulsor.nacelle != None:
if type(propulsor.nacelle) == RCAIDE.Library.Components.Nacelles.Stack_Nacelle:
GEOM = generate_3d_stack_nacelle_points(propulsor.nacelle, tessellation=tessellation, number_of_airfoil_points=number_of_airfoil_points)
elif type(propulsor.nacelle) == RCAIDE.Library.Components.Nacelles.Body_of_Revolution_Nacelle:
GEOM = generate_3d_BOR_nacelle_points(propulsor.nacelle, tessellation=tessellation, number_of_airfoil_points=number_of_airfoil_points)
else:
GEOM = generate_3d_basic_nacelle_points(propulsor.nacelle, tessellation=tessellation, number_of_airfoil_points=number_of_airfoil_points)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=nacelle_rgb_color, opacity=nacelle_opacity)
if 'rotor' in propulsor:
rot = propulsor.rotor
rot_x = rot.orientation_euler_angles[0]
rot_y = rot.orientation_euler_angles[1]
rot_z = rot.orientation_euler_angles[2]
num_B = int(rot.number_of_blades)
if (rot.radius_distribution) is None or (plot_actuator_disc == True):
make_actuator_disc(plotter, rot.hub_radius, rot.tip_radius, rot.origin, rot_x, rot_y, rot_z, rotor_rgb_color, rotor_opacity)
else:
rot_y += np.pi / 2
dim = len(rot.radius_distribution)
for i in range(num_B):
GEOM = generate_3d_blade_points(rot, number_of_airfoil_points, dim, i)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=rotor_rgb_color, opacity=rotor_opacity)
if 'propeller' in propulsor:
prop = propulsor.propeller
rot_x = prop.orientation_euler_angles[0]
rot_y = prop.orientation_euler_angles[1]
rot_z = prop.orientation_euler_angles[2]
num_B = int(prop.number_of_blades)
if (prop.radius_distribution is None) or (plot_actuator_disc == True):
make_actuator_disc(plotter, prop.hub_radius, prop.tip_radius, prop.origin, rot_x, rot_y, rot_z, rotor_rgb_color, rotor_opacity)
else:
dim = len(prop.radius_distribution)
for i in range(num_B):
GEOM = generate_3d_blade_points(prop, number_of_airfoil_points, dim, i)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=rotor_rgb_color, opacity=rotor_opacity)
for fuel_line in network.fuel_lines:
for fuel_tank in fuel_line.fuel_tanks:
if fuel_tank.wing_tag is not None:
wing = geometry.wings[fuel_tank.wing_tag]
if issubclass(type(fuel_tank), RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Non_Integral_Tank):
if issubclass(type(fuel_tank), RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Cryogenic_Tank) and fuel_tank.geometry_type == 'conformal' and fuel_tank.transverse_tank:
seg_bounds = fuel_tank.transverse_tank_chord_bounds
GEOM = generate_aft_integral_wing_tank_points(wing, 5, seg_bounds, fuel_tank)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
elif issubclass(type(fuel_tank), RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Cryogenic_Tank) and fuel_tank.geometry_type == 'conformal':
seg_bounds = fuel_tank.segments_bounding_tank
GEOM = generate_integral_wing_tank_points(wing, number_of_airfoil_points, seg_bounds, fuel_tank)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
if wing.xz_plane_symmetric:
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
else:
GEOM = generate_non_integral_fuel_tank_points(fuel_tank, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
if wing.xz_plane_symmetric:
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
if type(fuel_tank) == RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Integral_Tank:
seg_bounds = fuel_tank.segments_bounding_tank
GEOM = generate_integral_wing_tank_points(wing, number_of_airfoil_points, seg_bounds, fuel_tank, plot_centerline=False)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
if wing.xz_plane_symmetric:
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
elif fuel_tank.fuselage_tag is not None:
fuselage = geometry.fuselages[fuel_tank.fuselage_tag]
if type(fuel_tank) == RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Integral_Tank:
seg_bounds = fuel_tank.segments_bounding_tank
GEOM = generate_integral_fuel_tank_points(fuselage, fuel_tank, seg_bounds, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
elif issubclass(type(fuel_tank), RCAIDE.Library.Components.Powertrain.Sources.Fuel_Tanks.Non_Integral_Tank):
GEOM = generate_non_integral_fuel_tank_points(fuel_tank, tessellation)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
if wing.xz_plane_symmetric:
GEOM.PTS[:, :, 1] = -GEOM.PTS[:, :, 1]
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=fuel_tank_rgb_color, opacity=fuel_tank_opacity)
for bus in network.busses:
for battery in bus.battery_modules:
GEOM = generate_3d_cuboid_points(battery)
plotter.add_mesh(generate_vtk_object(GEOM.PTS), color=battery_rgb_color, opacity=battery_opacity)
if front_view:
plotter.camera_position = [(-2 * L , 0, 0), (0, 0,0), (0, 0, 1)]
elif side_view:
plotter.camera_position = [(L /2 , 2 * L, 0), (L /4, 0, 0), (0, 0, 1)]
elif top_view:
plotter.camera_position = [(L, 0 , 2 * L ), (L/4, 0,0), (0, 0, 1)]
else:
plotter.camera_position = [(L * camera_eye_x, L * camera_eye_y, L * camera_eye_z), (L /2, 0, 0), (0, 0, 1)]
plotter.window_size = [1500, 1500]
plotter.set_background('white')
if export_gltf:
plotter.export_gltf(save_filename + ".gltf")
if save_figure:
plotter.screenshot(save_filename + ".png", transparent_background=True)
else:
if show_figure:
plotter.show()
return plotter
[docs]
def add_lopa_seats(plotter, lopa_geometry, opacity):
color_map = {
"first": mcolors.to_rgb("indianred"),
"business": mcolors.to_rgb("seagreen"),
"economy": mcolors.to_rgb("steelblue"),
"galley_lav": mcolors.to_rgb("sandybrown"),
}
# Fast path: batched merged meshes (one add_mesh call per class/emergency group).
batches = getattr(lopa_geometry, "_lopa_batches", {})
if batches:
for (seat_class, is_em), merged_mesh in batches.items():
rgb = color_map.get(seat_class, mcolors.to_rgb("gray"))
actor = plotter.add_mesh(merged_mesh, color=rgb, opacity=float(opacity),
show_scalar_bar=False)
if is_em:
actor.GetProperty().EdgeVisibilityOn()
actor.GetProperty().SetEdgeColor(*rgb)
actor.GetProperty().SetLineWidth(1.0)
return
# Legacy path: individual seat dicts produced by older generate_3d_lopa_points.
seats = getattr(lopa_geometry, "_lopa_seats", [])
for seat in seats:
poly = seat.get("polydata", None)
if poly is None:
continue
seat_class = seat.get("class", "economy")
rgb = color_map.get(seat_class, mcolors.to_rgb("gray"))
mesh = pv.wrap(poly)
actor = plotter.add_mesh(mesh, color=rgb, opacity=float(opacity), show_scalar_bar=False)
if seat.get("emergency_row", False):
actor.GetProperty().EdgeVisibilityOn()
actor.GetProperty().SetEdgeColor(*rgb)
actor.GetProperty().SetLineWidth(1.0)
[docs]
def make_actuator_disc(plotter, inner_radius, outer_radius, origin, rot_x,rot_y,rot_z, rgb_color, opacity):
disc_points = np.array([[1],
[0],
[0]])
x_rotation = np.zeros(( 3, 3))
x_rotation[0,0] = 1
x_rotation[1,1] = np.cos(rot_x)
x_rotation[1,2] = -np.sin(rot_x)
x_rotation[2,1] = np.sin(rot_x)
x_rotation[2,2] = np.cos(rot_x)
y_rotation = np.zeros((3, 3))
y_rotation[0,0] = np.cos(rot_y)
y_rotation[0,2] = np.sin(rot_y)
y_rotation[1,1] = 1
y_rotation[2,0] = -np.sin(rot_y)
y_rotation[2,2] = np.cos(rot_y)
z_rotation = np.zeros(( 3, 3))
z_rotation[0,0] = np.cos(rot_z)
z_rotation[0,1] = -np.sin(rot_z)
z_rotation[1,0] = np.sin(rot_z)
z_rotation[1,1] = np.cos(rot_z)
z_rotation[2,2] = 1
R_total = z_rotation @ y_rotation @ x_rotation
disc_points_rotated =disc_points.T @ R_total.T
pyvista_mesh = pv.Disc(c_res=50,
inner=inner_radius,
outer=outer_radius,
normal=(disc_points_rotated[0][0], disc_points_rotated[0][1], disc_points_rotated[0][2]),
center= (origin[0][0], origin[0][1],origin[0][2]),
)
plotter.add_mesh(pyvista_mesh,color= rgb_color,opacity= opacity)
return