view.py

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2025 SWGY, Inc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import matplotlib.pyplot as plt

def visualize_geometry(
        geometry: List[trimesh.base.Trimesh], 
        blast_point: np.ndarray, 
        sensor: Sensor
        ):
    """
    Visualize the simulation geometry, blast point, and sensor.

    Args:
        geometry: List of trimesh meshes
        blast_point: Blast source coordinate
        sensor: Sensor object
    """
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')

    # Plot meshes
    for i, mesh in enumerate(geometry):
        faces = mesh.faces
        vertices = mesh.vertices
        ax.plot_trisurf(
                vertices[:, 0], vertices[:, 1], vertices[:, 2], 
                triangles=faces, alpha=0.5, color=f'C{i}'
                )

    # Plot blast point
    ax.scatter(
            blast_point[0], blast_point[1], blast_point[2], 
            color='red', s=100, label='Blast Point'
            )

    # Plot sensor
    u, v = np.mgrid[0:2*np.pi:20j, 0:np.pi:10j]
    x = sensor.radius * np.cos(u) * np.sin(v) + sensor.origin[0]
    y = sensor.radius * np.sin(u) * np.sin(v) + sensor.origin[1]
    z = sensor.radius * np.cos(v) + sensor.origin[2]
    ax.plot_surface(x, y, z, color='blue', alpha=0.3)

    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title('Simulation Geometry')
    ax.legend()

    plt.tight_layout()
    plt.show()


def create_simulation_scene(
        geometry: List[trimesh.base.Trimesh],
        blast_point: np.ndarray,
        sensor: Sensor,
        extents: Extents,
        ray_length: float = 2.0,
        extent_resolution: int = 8
        ) -> trimesh.Scene:
    """
    Create a trimesh.Scene visualizing the simulation setup.

    Args:
        geometry: List of trimesh meshes.
        blast_point: Blast source coordinate.
        sensor: Sensor object.
        extents: Angular extents in radians.
        ray_length: Length of rays marking extents.
        extent_resolution: Number of rays per edge to indicate extents.

    Returns:
        trimesh.Scene object containing all visualization elements.
    """
    scene = trimesh.Scene()

    # Add geometry to scene
    for mesh in geometry:
        scene.add_geometry(mesh)

    # Add sensor visualization (blue sphere)
    sensor_sphere = trimesh.creation.icosphere(radius=sensor.radius, subdivisions=3)
    sensor_sphere.visual.face_colors = [0, 0, 255, 100]  # Semi-transparent blue
    sensor_sphere.apply_translation(sensor.origin)
    scene.add_geometry(sensor_sphere)

    # Add blast point visualization (yellow sphere)
    blast_sphere = trimesh.creation.icosphere(radius=0.05 * sensor.radius, subdivisions=2)
    blast_sphere.visual.face_colors = [255, 255, 0, 255]  # Opaque yellow
    blast_sphere.apply_translation(blast_point)
    scene.add_geometry(blast_sphere)

    # Coordinate frame based on sensor and blast point
    forward, right, up = create_coordinate_frame(sensor.origin - blast_point)


    # Generate rays marking the angular extents (red rays)
    azimuths = np.linspace(extents.left, extents.right, extent_resolution)
    elevations = np.linspace(extents.bottom, extents.top, extent_resolution)

    ray_origins = []
    ray_ends = []

    for az in azimuths:
        for el in [extents.top, extents.bottom]:
            direction = (
                    forward * np.cos(el) * np.cos(az) +
                    right * np.cos(el) * np.sin(az) +
                    up * np.sin(el)
                    )
            direction = unit_vector(direction)
            ray_origins.append(blast_point)
            ray_ends.append(blast_point + direction * ray_length)

    for el in elevations:
        for az in [extents.left, extents.right]:
            direction = (
                    forward * np.cos(el) * np.cos(az) +
                    right * np.cos(el) * np.sin(az) +
                    up * np.sin(el)
                    )
            direction = unit_vector(direction)
            ray_origins.append(blast_point)
            ray_ends.append(blast_point + direction * ray_length)

    # Create lines from rays
    for origin, end in zip(ray_origins, ray_ends):
        ray_line = trimesh.load_path(np.vstack([origin, end]))
        ray_line.colors = [(255, 0, 0, 255)]  # Red
        scene.add_geometry(ray_line)

    return scene