Skip to content

primitives

Static scene primitives for viser visualization.

Functions for adding non-animated elements: obstacles, gates, constraint cones, ghost trajectories, etc. Called once during scene setup.

add_ellipsoid_obstacles(server: viser.ViserServer, centers: list[np.ndarray], radii: list[np.ndarray], axes: list[np.ndarray] | None = None, color: tuple[int, int, int] = (255, 100, 100), opacity: float = 0.6, wireframe: bool = False, subdivisions: int = 2) -> list

Add ellipsoidal obstacles to the scene.

Parameters:

Name Type Description Default
server ViserServer

ViserServer instance

required
centers list[ndarray]

List of center positions, each shape (3,)

required
radii list[ndarray]

List of radii along principal axes, each shape (3,)

required
axes list[ndarray] | None

List of rotation matrices (3, 3) defining principal axes. If None, ellipsoids are axis-aligned.

None
color tuple[int, int, int]

RGB color tuple

(255, 100, 100)
opacity float

Opacity (0-1), only used when wireframe=False

0.6
wireframe bool

If True, render as wireframe instead of solid

False
subdivisions int

Icosphere subdivisions (higher = smoother, 2 is usually good)

2

Returns:

Type Description
list

List of mesh handles

Source code in openscvx/plotting/viser/primitives.py
def add_ellipsoid_obstacles(
    server: viser.ViserServer,
    centers: list[np.ndarray],
    radii: list[np.ndarray],
    axes: list[np.ndarray] | None = None,
    color: tuple[int, int, int] = (255, 100, 100),
    opacity: float = 0.6,
    wireframe: bool = False,
    subdivisions: int = 2,
) -> list:
    """Add ellipsoidal obstacles to the scene.

    Args:
        server: ViserServer instance
        centers: List of center positions, each shape (3,)
        radii: List of radii along principal axes, each shape (3,)
        axes: List of rotation matrices (3, 3) defining principal axes.
            If None, ellipsoids are axis-aligned.
        color: RGB color tuple
        opacity: Opacity (0-1), only used when wireframe=False
        wireframe: If True, render as wireframe instead of solid
        subdivisions: Icosphere subdivisions (higher = smoother, 2 is usually good)

    Returns:
        List of mesh handles
    """
    handles = []

    if axes is None:
        axes = [None] * len(centers)

    for i, (center, rad, ax) in enumerate(zip(centers, radii, axes)):
        # Convert JAX arrays to numpy if needed
        center = np.asarray(center, dtype=np.float64)
        rad = np.asarray(rad, dtype=np.float64)
        if ax is not None:
            ax = np.asarray(ax, dtype=np.float64)

        vertices, faces = _generate_ellipsoid_mesh(center, rad, ax, subdivisions)

        handle = server.scene.add_mesh_simple(
            f"/obstacles/ellipsoid_{i}",
            vertices=vertices,
            faces=faces,
            color=color,
            wireframe=wireframe,
            opacity=opacity if not wireframe else 1.0,
        )
        handles.append(handle)

    return handles

add_gates(server: viser.ViserServer, vertices: list, color: tuple[int, int, int] = (255, 165, 0), line_width: float = 3.0) -> None

Add gate/obstacle wireframes to the scene.

Parameters:

Name Type Description Default
server ViserServer

ViserServer instance

required
vertices list

List of vertex arrays (4 vertices for planar gate, 8 for box)

required
color tuple[int, int, int]

RGB color tuple

(255, 165, 0)
line_width float

Line width for wireframe

3.0
Source code in openscvx/plotting/viser/primitives.py
def add_gates(
    server: viser.ViserServer,
    vertices: list,
    color: tuple[int, int, int] = (255, 165, 0),
    line_width: float = 3.0,
) -> None:
    """Add gate/obstacle wireframes to the scene.

    Args:
        server: ViserServer instance
        vertices: List of vertex arrays (4 vertices for planar gate, 8 for box)
        color: RGB color tuple
        line_width: Line width for wireframe
    """
    for i, verts in enumerate(vertices):
        verts = np.array(verts)
        n_verts = len(verts)

        if n_verts == 4:
            # Planar gate: 4 vertices forming a closed loop
            edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
        elif n_verts == 8:
            # 3D box: 8 vertices
            edges = [
                [0, 1],
                [1, 2],
                [2, 3],
                [3, 0],  # front face
                [4, 5],
                [5, 6],
                [6, 7],
                [7, 4],  # back face
                [0, 4],
                [1, 5],
                [2, 6],
                [3, 7],  # connecting edges
            ]
        else:
            # Unknown format, skip
            continue

        # Shape (N, 2, 3) for N line segments
        points = np.array([[verts[e[0]], verts[e[1]]] for e in edges])
        server.scene.add_line_segments(
            f"/gates/gate_{i}",
            points=points,
            colors=color,
            line_width=line_width,
        )

add_ghost_trajectory(server: viser.ViserServer, pos: np.ndarray, colors: np.ndarray, opacity: float = 0.3, point_size: float = 0.05) -> None

Add a faint ghost trajectory showing the full path.

Parameters:

Name Type Description Default
server ViserServer

ViserServer instance

required
pos ndarray

Position array of shape (N, 3)

required
colors ndarray

RGB color array of shape (N, 3)

required
opacity float

Opacity factor (0-1) applied to colors

0.3
point_size float

Size of trajectory points

0.05
Source code in openscvx/plotting/viser/primitives.py
def add_ghost_trajectory(
    server: viser.ViserServer,
    pos: np.ndarray,
    colors: np.ndarray,
    opacity: float = 0.3,
    point_size: float = 0.05,
) -> None:
    """Add a faint ghost trajectory showing the full path.

    Args:
        server: ViserServer instance
        pos: Position array of shape (N, 3)
        colors: RGB color array of shape (N, 3)
        opacity: Opacity factor (0-1) applied to colors
        point_size: Size of trajectory points
    """
    ghost_colors = (colors * opacity).astype(np.uint8)
    server.scene.add_point_cloud(
        "/ghost_traj",
        points=pos,
        colors=ghost_colors,
        point_size=point_size,
    )

add_glideslope_cone(server: viser.ViserServer, apex: np.ndarray | tuple = (0.0, 0.0, 0.0), height: float = 2000.0, glideslope_angle_deg: float = 86.0, axis: np.ndarray | tuple = (0.0, 0.0, 1.0), color: tuple[int, int, int] = (100, 200, 100), opacity: float = 0.2, wireframe: bool = False, n_segments: int = 32) -> viser.MeshHandle

Add a glideslope/approach constraint cone to the scene.

The glideslope constraint typically has the form

||position_perp|| <= tan(angle) * position_along_axis

This creates a cone with apex at the target, opening along the specified axis.

Parameters:

Name Type Description Default
server ViserServer

ViserServer instance

required
apex ndarray | tuple

Apex position (docking/landing site), default is origin

(0.0, 0.0, 0.0)
height float

Height of the cone visualization

2000.0
glideslope_angle_deg float

Glideslope angle in degrees (measured from axis). For constraint ||r_perp|| <= tan(theta) * r_axis, pass theta here. Common values: 86 deg (very wide), 70 deg (moderate), 45 deg (steep)

86.0
axis ndarray | tuple

Unit vector direction the cone opens toward. Default (0,0,1) for +Z. Use (-1,0,0) for R-bar approach (from below in radial direction).

(0.0, 0.0, 1.0)
color tuple[int, int, int]

RGB color tuple

(100, 200, 100)
opacity float

Opacity (0-1)

0.2
wireframe bool

If True, render as wireframe

False
n_segments int

Number of segments for cone smoothness

32

Returns:

Type Description
MeshHandle

Mesh handle for the cone

Source code in openscvx/plotting/viser/primitives.py
def add_glideslope_cone(
    server: viser.ViserServer,
    apex: np.ndarray | tuple = (0.0, 0.0, 0.0),
    height: float = 2000.0,
    glideslope_angle_deg: float = 86.0,
    axis: np.ndarray | tuple = (0.0, 0.0, 1.0),
    color: tuple[int, int, int] = (100, 200, 100),
    opacity: float = 0.2,
    wireframe: bool = False,
    n_segments: int = 32,
) -> viser.MeshHandle:
    """Add a glideslope/approach constraint cone to the scene.

    The glideslope constraint typically has the form:
        ||position_perp|| <= tan(angle) * position_along_axis

    This creates a cone with apex at the target, opening along the specified axis.

    Args:
        server: ViserServer instance
        apex: Apex position (docking/landing site), default is origin
        height: Height of the cone visualization
        glideslope_angle_deg: Glideslope angle in degrees (measured from axis).
            For constraint ||r_perp|| <= tan(theta) * r_axis, pass theta here.
            Common values: 86 deg (very wide), 70 deg (moderate), 45 deg (steep)
        axis: Unit vector direction the cone opens toward. Default (0,0,1) for +Z.
            Use (-1,0,0) for R-bar approach (from below in radial direction).
        color: RGB color tuple
        opacity: Opacity (0-1)
        wireframe: If True, render as wireframe
        n_segments: Number of segments for cone smoothness

    Returns:
        Mesh handle for the cone
    """
    apex = np.asarray(apex, dtype=np.float32)

    vertices, faces = _generate_cone_mesh(apex, height, glideslope_angle_deg, n_segments, axis=axis)

    handle = server.scene.add_mesh_simple(
        "/constraints/glideslope_cone",
        vertices=vertices,
        faces=faces,
        color=color,
        wireframe=wireframe,
        opacity=opacity if not wireframe else 1.0,
    )

    return handle