two_sided Function

private pure function two_sided(self)

Whether both sides are drawn.

Type Bound

Polyhedron

Arguments

Type IntentOptional Attributes Name
class(Polyhedron), intent(in) :: self

Return Value logical


Source Code

    function surface_node(xyz, idx, rgba, normals, colors, two_sided, sphere) result(node)
        real(wp), intent(in) :: xyz(:,:)                    !! (3, n) vertices
        integer, intent(in) :: idx(:,:)                     !! (3, m) triangles, vertex numbers (1-based)
        real(wp), intent(in) :: rgba(4)                     !! color [r, g, b, a] (0 to 1)
        real(wp), intent(in), optional :: normals(:,:)      !! (3, n): lit
        real(wp), intent(in), optional :: colors(:,:)       !! (4, n)
        logical, intent(in), optional :: two_sided          !! (lit) both faces (default: front only)
        real(wp), intent(in), optional :: sphere(4)         !! (lit, translucent) center and radius
        type(c_ptr) :: node
        real(c_float), dimension(:,:), pointer :: v, n, uv, c
        integer(c_int32_t), dimension(:), pointer :: indices
        integer :: nv, ni, flags, i
        logical :: lit, translucent
        node = c_null_ptr
        nv = size(xyz, 2)
        ni = size(idx)
        if (nv < 3 .or. ni < 3) return
        lit = present(normals)
        flags = 1
        if (lit) then
            translucent = rgba(4) < 1.0_wp
            if (present(colors)) translucent = translucent .or. any(colors(4, :) < 1.0_wp)
            flags = 0
            if (translucent) flags = flags + 8
            if (present(two_sided)) then
                if (two_sided) flags = flags + 16
            end if
        end if
        if (present(colors)) flags = flags + 32
        node = vkf_mesh_create(int(nv, c_int), int(ni, c_int), real(rgba, c_float), c_null_ptr, c_null_ptr, &
                               int(flags, c_int))
        if (.not. c_associated(node)) return
        call mesh_arrays(node, nv, ni, v, n, uv, indices)
        v = real(xyz, c_float)
        if (lit) then
            n = real(normals, c_float)
        else
            n = 0.0_c_float
            n(3, :) = 1.0_c_float
        end if
        uv = 0.0_c_float
        indices = int(reshape(idx, [ni]) - 1, c_int32_t)
        if (present(colors)) then
            call c_f_pointer(vkf_mesh_array(node, 5_c_int), c, [4, nv])
            do i = 1, nv
                c(:, i) = real(colors(:, i) * rgba, c_float)
            end do
        end if
        if (lit .and. iand(flags, 8) /= 0 .and. present(sphere)) &
            call check_status(vkf_mesh_set_bound(node, real(sphere(1), c_double), real(sphere(2), c_double), &
                                                 real(sphere(3), c_double), real(sphere(4), c_double)), 'shapes')
    end function surface_node

    !> Points along a closed curve, as separate segments: pairs (p(i), p(i+1)).
    pure function closed_segments(p) result(s)
        real(wp), intent(in) :: p(:,:)                      !! (3, n) points of the curve
        real(wp) :: s(3, 2 * size(p, 2))
        integer :: i, n
        n = size(p, 2)
        do i = 1, n
            s(:, 2*i-1) = p(:, i)
            s(:, 2*i) = p(:, modulo(i, n) + 1)
        end do
    end function closed_segments

    !> Points along an open curve, as separate segments.
    pure function open_segments(p) result(s)
        real(wp), intent(in) :: p(:,:)                      !! (3, n) points of the curve
        real(wp) :: s(3, 2 * (size(p, 2) - 1))
        integer :: i
        do i = 1, size(p, 2) - 1
            s(:, 2*i-1) = p(:, i)
            s(:, 2*i) = p(:, i + 1)
        end do
    end function open_segments

    !> Append points b to a (allocatable, (3, n)).
    pure subroutine append(a, b)
        real(wp), allocatable, intent(inout) :: a(:,:)      !! (3, n) points (unallocated: none)
        real(wp), intent(in) :: b(:,:)                      !! (3, m) points to append
        real(wp), allocatable :: tmp(:,:)
        if (.not. allocated(a)) allocate(a(3, 0))
        allocate(tmp(3, size(a, 2) + size(b, 2)))
        tmp(:, 1:size(a, 2)) = a
        tmp(:, size(a, 2)+1:) = b
        call move_alloc(tmp, a)
    end subroutine append

    ! ==================================================================
    ! CoordinateAxes

    !> Coordinate axes named `name`, of color `color` ([r, g, b] or [r, g, b,
    !> a]), 1 long, with major ticks every 0.5 and minor every 0.25 (OpenFrames'
    !> defaults).
    function new_coordinate_axes(name, color) result(a)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(CoordinateAxes) :: a
        a%ReferenceFrame = ReferenceFrame(name, color)
        a%image_file = ''
        call place_axes(a, a%length_)
    end function new_coordinate_axes

    !> The frame's own axes (arrows) at `length` along x, y and z, half as long.
    subroutine place_axes(self, length)
        class(ReferenceFrame), intent(inout) :: self
        real(wp), intent(in) :: length                      !! scene units
        call self%move_x_axis([length, 0.0_wp, 0.0_wp], 0.5_wp * length)
        call self%move_y_axis([0.0_wp, length, 0.0_wp], 0.5_wp * length)
        call self%move_z_axis([0.0_wp, 0.0_wp, length], 0.5_wp * length)
    end subroutine place_axes

    !> The axes' length (scene units, >= 0; default 1); the frame's own axes are
    !> moved to the lines' ends.
    subroutine set_axis_length(self, length)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), intent(in) :: length                      !! scene units (< 0: ignored)
        if (length < 0.0_wp) return
        self%length_ = length
        call place_axes(self, length)
        call make_axes(self)
    end subroutine set_axis_length

    !> The axes' length (scene units).
    pure real(wp) function axis_length(self)
        class(CoordinateAxes), intent(in) :: self
        axis_length = self%length_
    end function axis_length

    !> Which axes to draw: an ior of X_AXIS, Y_AXIS, Z_AXIS.
    subroutine set_draw_axes(self, axes)
        class(CoordinateAxes), intent(inout) :: self
        integer, intent(in) :: axes                         !! an ior of X_AXIS, Y_AXIS, Z_AXIS
        self%drawn = axes
        call make_axes(self)
    end subroutine set_draw_axes

    !> Which axes are drawn: an ior of X_AXIS, Y_AXIS, Z_AXIS.
    pure integer function draw_axes(self)
        class(CoordinateAxes), intent(in) :: self
        draw_axes = self%drawn
    end function draw_axes

    !> The axis lines' width (window units; default 1).
    subroutine set_axis_width(self, width)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), intent(in) :: width                       !! window units (<= 0: ignored)
        if (width <= 0.0_wp) return
        self%width_ = width
        call make_axes(self)
    end subroutine set_axis_width

    !> The axis lines' width (window units).
    pure real(wp) function axis_width(self)
        class(CoordinateAxes), intent(in) :: self
        axis_width = self%width_
    end function axis_width

    !> Major and minor tick spacings (>= 0; 0 for none).
    subroutine set_tick_spacing(self, major, minor)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), intent(in) :: major, minor                !! scene units (< 0: unchanged)
        if (major >= 0.0_wp) self%major = major
        if (minor >= 0.0_wp) self%minor = minor
        call make_axes(self)
    end subroutine set_tick_spacing

    !> Major and minor tick spacings.
    pure subroutine tick_spacing(self, major, minor)
        class(CoordinateAxes), intent(in) :: self
        real(wp), intent(out) :: major, minor               !! scene units
        major = self%major
        minor = self%minor
    end subroutine tick_spacing

    !> Major and minor tick sizes (window units; default 8 and 4).
    subroutine set_tick_size(self, major, minor)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), intent(in) :: major, minor                !! window units (<= 0: unchanged)
        if (major > 0.0_wp) self%major_size = major
        if (minor > 0.0_wp) self%minor_size = minor
        call make_axes(self)
    end subroutine set_tick_size

    !> Major and minor tick sizes (window units).
    pure subroutine tick_size(self, major, minor)
        class(CoordinateAxes), intent(in) :: self
        real(wp), intent(out) :: major, minor               !! window units
        major = self%major_size
        minor = self%minor_size
    end subroutine tick_size

    !> Tick shape: a MARKER_* shape (default MARKER_DISK; OpenFrames'
    !> setTickShader with its marker shaders).
    subroutine set_tick_shape(self, shape)
        class(CoordinateAxes), intent(inout) :: self
        integer, intent(in) :: shape                        !! a MARKER_* shape
        self%shape_ = shape
        call make_axes(self)
    end subroutine set_tick_shape

    !> Tick shape: a MARKER_* shape.
    pure integer function tick_shape(self)
        class(CoordinateAxes), intent(in) :: self
        tick_shape = self%shape_
    end function tick_shape

    !> Draw the ticks as an image (OpenFrames' setTickImage; found with
    !> find_data_file; '' for disks again). False if it cannot be read.
    logical function set_tick_image(self, file) result(ok)
        class(CoordinateAxes), intent(inout) :: self
        character(len=*), intent(in) :: file                !! image file ('' for disks)
        character(len=:), allocatable :: path
        type(c_ptr) :: image
        ok = .true.
        image = c_null_ptr
        if (len_trim(file) > 0) then
            path = find_data_file(file)
            if (len(path) > 0) image = vkf_image_read(path, len(path, kind=c_int))
            ok = c_associated(image)
            if (.not. ok) return
        end if
        if (c_associated(self%image)) call vkf_image_delete(self%image)
        self%image = image
        self%image_file = trim(file)
        self%shape_ = merge(MARKER_IMAGE, MARKER_DISK, c_associated(image))
        call make_axes(self)
    end function set_tick_image

    !> The tick image file ('' for none).
    function tick_image(self) result(file)
        class(CoordinateAxes), intent(in) :: self
        character(len=:), allocatable :: file
        file = self%image_file
    end function tick_image

    !> The axes' color (lines and ticks).
    subroutine axes_set_color(self, color)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call self%ReferenceFrame%set_color(color)
        call make_axes(self)
    end subroutine axes_set_color

    !> What kind of frame this is: 'CoordinateAxes'.
    function axes_frame_info(self) result(info)
        class(CoordinateAxes), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'CoordinateAxes'
    end function axes_frame_info

    !> The bounding sphere: the origin, radius the axis length.
    subroutine axes_bound(self, center, radius)
        class(CoordinateAxes), intent(in) :: self
        real(wp), intent(out) :: center(3), radius          !! the bounding sphere, frame coordinates (scene units)
        center = 0.0_wp
        radius = self%length_
    end subroutine axes_bound

    !> Draw the axes under `group` (the contents); with a window's `viewer`, later
    !> drawings are compiled.
    subroutine axes_build_contents(self, viewer, group)
        class(CoordinateAxes), intent(inout) :: self
        type(c_ptr), intent(in) :: viewer                   !! a window's viewer (null: headless)
        type(c_ptr), intent(in) :: group                    !! the frame's contents group
        self%nodes%contents = group
        call make_axes(self)
        self%nodes%shown = c_associated(viewer)
    end subroutine axes_build_contents

    !> Make the axes' lines and tick marks, in place of the old (nothing before
    !> build_contents).
    subroutine make_axes(self)
        class(CoordinateAxes), intent(inout) :: self
        real(wp), allocatable :: lines(:,:), major(:,:), minor(:,:)
        real(wp) :: e(3), mark, f
        type(c_ptr) :: group
        integer :: k, i
        if (.not. c_associated(self%nodes%contents)) return
        allocate(lines(3, 0), major(3, 0), minor(3, 0))
        do k = 1, 3
            if (iand(self%drawn, 2**(k-1)) == 0) cycle
            e = 0.0_wp
            e(k) = 1.0_wp
            call append(lines, reshape([0.0_wp, 0.0_wp, 0.0_wp, self%length_ * e], [3, 2]))
            if (self%major > 0.0_wp) then
                do i = 1, int(self%length_ / self%major + 1.0e-9_wp)
                    call append(major, reshape(i * self%major * e, [3, 1]))
                end do
            end if
            if (self%minor > 0.0_wp) then
                do i = 1, int(self%length_ / self%minor + 1.0e-9_wp)
                    mark = i * self%minor
                    if (self%major > 0.0_wp) then                   ! not where a major tick is
                        f = mark / self%major
                        if (abs(f - nint(f)) < 1.0e-6_wp) cycle
                    end if
                    call append(minor, reshape(mark * e, [3, 1]))
                end do
            end if
        end do
        group = vkf_switch_create()
        call add(group, line_node(lines, self%color(), self%width_, .true.))
        call add(group, marker_node(major, self%color(), self%major_size, self%shape_, self%length_, self%image))
        call add(group, marker_node(minor, self%color(), self%minor_size, self%shape_, self%length_, self%image))
        call self%nodes%replace(group)
    end subroutine make_axes

    ! ==================================================================
    ! LatLonGrid

    !> A latitude/longitude grid named `name`, of color `color`, on a unit
    !> sphere, every 30 degrees (OpenFrames' defaults).
    function new_lat_lon_grid(name, color) result(g)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(LatLonGrid) :: g
        g%ReferenceFrame = ReferenceFrame(name, color)
        call place_grid_axes(g)
    end function new_lat_lon_grid

    !> The frame's own axes (arrows) at the ellipsoid's semi-axes, half as long.
    subroutine place_grid_axes(self)
        type(LatLonGrid), intent(inout) :: self
        call self%move_x_axis([self%radii_(1), 0.0_wp, 0.0_wp], 0.5_wp * self%radii_(1))
        call self%move_y_axis([0.0_wp, self%radii_(2), 0.0_wp], 0.5_wp * self%radii_(2))
        call self%move_z_axis([0.0_wp, 0.0_wp, self%radii_(3)], 0.5_wp * self%radii_(3))
    end subroutine place_grid_axes

    !> The ellipsoid's semi-axes (> 0; e.g. a Sphere's radii, a little larger
    !> to draw above its surface) and the line spacings (radians; 0 for no
    !> latitude or longitude lines), as OpenFrames' setParameters.
    subroutine grid_set_parameters(self, radius_x, radius_y, radius_z, lat_spacing, lon_spacing)
        class(LatLonGrid), intent(inout) :: self
        real(wp), intent(in) :: radius_x, radius_y, radius_z  !! scene units (<= 0: unchanged)
        real(wp), intent(in) :: lat_spacing, lon_spacing    !! radians (< 0: unchanged)
        if (radius_x > 0.0_wp) self%radii_(1) = radius_x
        if (radius_y > 0.0_wp) self%radii_(2) = radius_y
        if (radius_z > 0.0_wp) self%radii_(3) = radius_z
        if (lat_spacing >= 0.0_wp) self%lat_spacing = lat_spacing
        if (lon_spacing >= 0.0_wp) self%lon_spacing = lon_spacing
        call place_grid_axes(self)
        call make_grid(self)
    end subroutine grid_set_parameters

    !> The ellipsoid's semi-axes and the line spacings, as OpenFrames' getParameters.
    pure subroutine grid_parameters(self, radius_x, radius_y, radius_z, lat_spacing, lon_spacing)
        class(LatLonGrid), intent(in) :: self
        real(wp), intent(out) :: radius_x, radius_y, radius_z  !! scene units
        real(wp), intent(out) :: lat_spacing, lon_spacing   !! radians
        radius_x = self%radii_(1)
        radius_y = self%radii_(2)
        radius_z = self%radii_(3)
        lat_spacing = self%lat_spacing
        lon_spacing = self%lon_spacing
    end subroutine grid_parameters

    !> Widths of the grid lines and of the equator and prime meridian (window
    !> units; defaults 1 and 3).
    subroutine set_line_widths(self, width, main_width)
        class(LatLonGrid), intent(inout) :: self
        real(wp), intent(in) :: width, main_width           !! window units (<= 0: unchanged)
        if (width > 0.0_wp) self%width_ = width
        if (main_width > 0.0_wp) self%main_width = main_width
        call make_grid(self)
    end subroutine set_line_widths

    !> Widths of the grid lines and of the equator and prime meridian (window
    !> units).
    pure subroutine line_widths(self, width, main_width)
        class(LatLonGrid), intent(in) :: self
        real(wp), intent(out) :: width, main_width          !! window units
        width = self%width_
        main_width = self%main_width
    end subroutine line_widths

    !> The lines' color.
    subroutine grid_set_color(self, color)
        class(LatLonGrid), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call self%ReferenceFrame%set_color(color)
        call make_grid(self)
    end subroutine grid_set_color

    !> What kind of frame this is: 'LatLonGrid'.
    function grid_frame_info(self) result(info)
        class(LatLonGrid), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'LatLonGrid'
    end function grid_frame_info

    !> The bounding sphere: the origin, radius the largest semi-axis.
    subroutine grid_bound(self, center, radius)
        class(LatLonGrid), intent(in) :: self
        real(wp), intent(out) :: center(3), radius          !! the bounding sphere, frame coordinates (scene units)
        center = 0.0_wp
        radius = maxval(self%radii_)
    end subroutine grid_bound

    !> Draw the grid under `group` (the contents); with a window's `viewer`, later
    !> drawings are compiled.
    subroutine grid_build_contents(self, viewer, group)
        class(LatLonGrid), intent(inout) :: self
        type(c_ptr), intent(in) :: viewer                   !! a window's viewer (null: headless)
        type(c_ptr), intent(in) :: group                    !! the frame's contents group
        self%nodes%contents = group
        call make_grid(self)
        self%nodes%shown = c_associated(viewer)
    end subroutine grid_build_contents

    !> The point at geodetic latitude lat and longitude lon on the ellipsoid
    !> (OpenFrames' LatLonGrid formula: the surface point whose normal has
    !> those angles).
    pure function surface_point(r, lat, lon) result(p)
        real(wp), intent(in) :: r(3)                        !! semi-axes (scene units)
        real(wp), intent(in) :: lat, lon                    !! radians
        real(wp) :: p(3), n(3), r2(3)
        n = [cos(lat) * cos(lon), cos(lat) * sin(lon), sin(lat)]
        r2 = r**2
        p = r2 * n / sqrt(dot_product(r2, n**2))
    end function surface_point

    !> Make the grid's lines (circles in 1 degree steps, meridians in 0.5 degree
    !> steps), in place of the old.
    subroutine make_grid(self)
        type(LatLonGrid), intent(inout) :: self
        integer, parameter :: n_around = 360, n_meridian = 360          ! 1 and 0.5 degree steps
        real(wp), allocatable :: grid(:,:), main(:,:)
        real(wp) :: circle(3, n_around), meridian(3, n_meridian + 1), lat, lon
        type(c_ptr) :: group
        integer :: i, k, sgn
        if (.not. c_associated(self%nodes%contents)) return
        allocate(grid(3, 0), main(3, 0))
        if (self%lat_spacing > 0.0_wp) then                    ! the equator, then pairs north and south
            k = 0
            do while (k * self%lat_spacing < 0.5_wp * pi - 1.0e-6_wp)
                do sgn = 1, merge(1, 2, k == 0)
                    lat = merge(1.0_wp, -1.0_wp, sgn == 1) * k * self%lat_spacing
                    do i = 1, n_around
                        circle(:, i) = surface_point(self%radii_, lat, 2.0_wp * pi * (i - 1) / n_around)
                    end do
                    if (k == 0) then
                        call append(main, closed_segments(circle))
                    else
                        call append(grid, closed_segments(circle))
                    end if
                end do
                k = k + 1
            end do
        end if
        if (self%lon_spacing > 0.0_wp) then                    ! the prime meridian, then the others
            k = 0
            do while (k * self%lon_spacing < 2.0_wp * pi - 1.0e-6_wp)
                lon = k * self%lon_spacing
                do i = 1, n_meridian + 1
                    meridian(:, i) = surface_point(self%radii_, -0.5_wp * pi + pi * (i - 1) / n_meridian, lon)
                end do
                if (k == 0) then
                    call append(main, open_segments(meridian))
                else
                    call append(grid, open_segments(meridian))
                end if
                k = k + 1
            end do
        end if
        group = vkf_switch_create()
        call add(group, line_node(grid, self%color(), self%width_, .true.))
        call add(group, line_node(main, self%color(), self%main_width, .true.))
        call self%nodes%replace(group)
    end subroutine make_grid

    ! ==================================================================
    ! RadialPlane

    !> A radial plane named `name` (decorations of color `color`): radius 1,
    !> circles every 0.5, radial lines every 30 degrees, light gray and
    !> transparent (OpenFrames' defaults).
    function new_radial_plane(name, color) result(p)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(RadialPlane) :: p
        p%ReferenceFrame = ReferenceFrame(name, color)
        call place_axes(p, p%radius_)
    end function new_radial_plane

    !> The plane's radius (> 0), circle spacing and radial line spacing
    !> (radians; 0 for none), as OpenFrames' setParameters.
    subroutine plane_set_parameters(self, radius, radial_spacing, lon_spacing)
        class(RadialPlane), intent(inout) :: self
        real(wp), intent(in) :: radius                      !! scene units (<= 0: unchanged)
        real(wp), intent(in) :: radial_spacing              !! scene units (< 0: unchanged)
        real(wp), intent(in) :: lon_spacing                 !! radians (< 0: unchanged)
        if (radius > 0.0_wp) self%radius_ = radius
        if (radial_spacing >= 0.0_wp) self%radial_spacing = radial_spacing
        if (lon_spacing >= 0.0_wp) self%lon_spacing = lon_spacing
        call place_axes(self, self%radius_)
        call make_plane(self)
    end subroutine plane_set_parameters

    !> The plane's radius, circle spacing and radial line spacing, as OpenFrames'
    !> getParameters.
    pure subroutine plane_parameters(self, radius, radial_spacing, lon_spacing)
        class(RadialPlane), intent(in) :: self
        real(wp), intent(out) :: radius, radial_spacing     !! scene units
        real(wp), intent(out) :: lon_spacing                !! radians
        radius = self%radius_
        radial_spacing = self%radial_spacing
        lon_spacing = self%lon_spacing
    end subroutine plane_parameters

    !> The plane's color ([r, g, b] or [r, g, b, a]; default 0.8 gray, 0.2 opaque).
    subroutine set_plane_color(self, color)
        class(RadialPlane), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%plane_color_, color)
        call make_plane(self)
    end subroutine set_plane_color

    !> The plane's color [r, g, b, a].
    pure function plane_color(self) result(c)
        class(RadialPlane), intent(in) :: self
        real(wp) :: c(4)
        c = self%plane_color_
    end function plane_color

    !> The lines' color (default white, 0.2 opaque).
    subroutine plane_set_line_color(self, color)
        class(RadialPlane), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%line_color_, color)
        call make_plane(self)
    end subroutine plane_set_line_color

    !> The lines' color [r, g, b, a].
    pure function plane_line_color(self) result(c)
        class(RadialPlane), intent(in) :: self
        real(wp) :: c(4)
        c = self%line_color_
    end function plane_line_color

    !> What kind of frame this is: 'RadialPlane'.
    function plane_frame_info(self) result(info)
        class(RadialPlane), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'RadialPlane'
    end function plane_frame_info

    !> The bounding sphere: the origin, radius the plane's radius.
    subroutine plane_bound(self, center, radius)
        class(RadialPlane), intent(in) :: self
        real(wp), intent(out) :: center(3), radius          !! the bounding sphere, frame coordinates (scene units)
        center = 0.0_wp
        radius = self%radius_
    end subroutine plane_bound

    !> Draw the plane under `group` (the contents); with a window's `viewer`, later
    !> drawings are compiled.
    subroutine plane_build_contents(self, viewer, group)
        class(RadialPlane), intent(inout) :: self
        type(c_ptr), intent(in) :: viewer                   !! a window's viewer (null: headless)
        type(c_ptr), intent(in) :: group                    !! the frame's contents group
        self%nodes%contents = group
        call make_plane(self)
        self%nodes%shown = c_associated(viewer)
    end subroutine plane_build_contents

    !> Make the disk, its circles and its radial lines (1 degree steps), in place of
    !> the old.
    subroutine make_plane(self)
        type(RadialPlane), intent(inout) :: self
        integer, parameter :: n = 360                          ! points around a circle (1 degree)
        real(wp) :: disk(3, n + 1), circle(3, n), a, r
        integer :: idx(3, n), i, k
        real(wp), allocatable :: lines(:,:), main(:,:)
        type(c_ptr) :: group
        if (.not. c_associated(self%nodes%contents)) return
        ! the disk: a fan around the center
        disk(:, n + 1) = 0.0_wp
        do i = 1, n
            a = 2.0_wp * pi * (i - 1) / n
            disk(:, i) = self%radius_ * [cos(a), sin(a), 0.0_wp]
            idx(:, i) = [n + 1, i, modulo(i, n) + 1]
        end do
        allocate(lines(3, 0), main(3, 0))
        ! circles: the edge, and every radial_spacing from the center
        if (self%radial_spacing > 0.0_wp) then
            call append(lines, closed_segments(disk(:, 1:n)))
            k = 1
            do while (k * self%radial_spacing < self%radius_ * (1.0_wp - 1.0e-6_wp))
                r = k * self%radial_spacing
                circle = r / self%radius_ * disk(:, 1:n)
                call append(lines, closed_segments(circle))
                k = k + 1
            end do
        end if
        ! radial lines: along +x (thicker), then every lon_spacing
        if (self%lon_spacing > 0.0_wp) then
            k = 0
            do while (k * self%lon_spacing < 2.0_wp * pi - 1.0e-6_wp)
                a = k * self%lon_spacing
                if (k == 0) then
                    call append(main, reshape([0.0_wp, 0.0_wp, 0.0_wp, self%radius_ * [cos(a), sin(a), 0.0_wp]], [3, 2]))
                else
                    call append(lines, reshape([0.0_wp, 0.0_wp, 0.0_wp, self%radius_ * [cos(a), sin(a), 0.0_wp]], [3, 2]))
                end if
                k = k + 1
            end do
        end if
        group = vkf_switch_create()
        call add(group, surface_node(disk, idx, self%plane_color_))
        call add(group, line_node(lines, self%line_color_, 1.0_wp, .true.))
        call add(group, line_node(main, self%line_color_, 3.0_wp, .true.))
        call self%nodes%replace(group)
    end subroutine make_plane

    ! ==================================================================
    ! PolyhedralCone

    !> A polyhedral cone named `name`, of color `color` (its sides and lines):
    !> 3 vertices at clock angles 0, 120 and 240 degrees, cone angles 45
    !> degrees, 1 long (OpenFrames' defaults).
    function new_polyhedral_cone(name, color) result(c)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(PolyhedralCone) :: c
        c%ReferenceFrame = ReferenceFrame(name, color)
        c%cone_color_ = c%color()
        c%line_color_ = c%color()
        c%clock = [0.0_wp, 120.0_wp, 240.0_wp] * pi / 180.0_wp
        c%cone = [45.0_wp, 45.0_wp, 45.0_wp] * pi / 180.0_wp
    end function new_polyhedral_cone

    !> The vertices: clock angles (from +x toward +y) and cone angles (from the
    !> boresight, -z; below 90 degrees), radians, the same number of each (>= 3).
    subroutine set_vertex_angles(self, clock, cone)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), intent(in) :: clock(:), cone(:)           !! radians
        if (size(clock) /= size(cone) .or. size(clock) < 3) return
        self%clock = clock
        self%cone = cone
        call make_cone(self)
    end subroutine set_vertex_angles

    !> The vertices' clock angles (radians).
    pure function clock_angles(self) result(a)
        class(PolyhedralCone), intent(in) :: self
        real(wp), allocatable :: a(:)
        a = self%clock
    end function clock_angles

    !> The vertices' cone angles (radians).
    pure function cone_angles(self) result(a)
        class(PolyhedralCone), intent(in) :: self
        real(wp), allocatable :: a(:)
        a = self%cone
    end function cone_angles

    !> The cone's length along the boresight (> 0; default 1).
    subroutine set_cone_length(self, length)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), intent(in) :: length                      !! scene units (<= 0: ignored)
        if (length <= 0.0_wp) return
        self%length_ = length
        call make_cone(self)
    end subroutine set_cone_length

    !> The cone's length along the boresight (scene units).
    pure real(wp) function cone_length(self)
        class(PolyhedralCone), intent(in) :: self
        cone_length = self%length_
    end function cone_length

    !> The sides' color ([r, g, b] or [r, g, b, a]; their alpha makes them
    !> transparent).
    subroutine set_cone_color(self, color)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%cone_color_, color)
        call make_cone(self)
    end subroutine set_cone_color

    !> The sides' color [r, g, b, a].
    pure function cone_color(self) result(c)
        class(PolyhedralCone), intent(in) :: self
        real(wp) :: c(4)
        c = self%cone_color_
    end function cone_color

    !> The edges' and base outline's color.
    subroutine cone_set_line_color(self, color)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%line_color_, color)
        call make_cone(self)
    end subroutine cone_set_line_color

    !> The edges' and base outline's color [r, g, b, a].
    pure function cone_line_color(self) result(c)
        class(PolyhedralCone), intent(in) :: self
        real(wp) :: c(4)
        c = self%line_color_
    end function cone_line_color

    !> What to draw: an ior of CONE_SIDES, CONE_EDGES, CONE_BASE_OUTLINE.
    subroutine set_draw_mode(self, mode)
        class(PolyhedralCone), intent(inout) :: self
        integer, intent(in) :: mode                         !! an ior of CONE_SIDES, CONE_EDGES, CONE_BASE_OUTLINE
        self%mode = mode
        call make_cone(self)
    end subroutine set_draw_mode

    !> What is drawn: an ior of CONE_SIDES, CONE_EDGES, CONE_BASE_OUTLINE.
    pure integer function draw_mode(self)
        class(PolyhedralCone), intent(in) :: self
        draw_mode = self%mode
    end function draw_mode

    !> Place the cone's apex at `apex` (in the parent frame) with its boresight
    !> along `dir` and its +y axis toward `up` (OpenFrames' makeConeLookAt).
    subroutine make_cone_look_at(self, apex, dir, up)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), intent(in) :: apex(3)                     !! parent frame coordinates (scene units)
        real(wp), intent(in) :: dir(3)                      !! boresight direction (any length > 0)
        real(wp), intent(in) :: up(3)                       !! toward the cone's +y axis (any length)
        real(wp) :: f(3), s(3), u(3), m(3,3)
        if (norm2(dir) <= 0.0_wp) return
        f = dir / norm2(dir)
        s = cross(f, up)
        if (norm2(s) <= 1.0e-12_wp) s = cross(f, [1.0_wp, 0.0_wp, 0.0_wp])
        if (norm2(s) <= 1.0e-12_wp) s = cross(f, [0.0_wp, 1.0_wp, 0.0_wp])
        s = s / norm2(s)
        u = cross(s, f)
        m(:, 1) = s                                             ! the cone's axes in the parent frame
        m(:, 2) = u
        m(:, 3) = -f
        call self%set_position(apex)
        call self%set_attitude(quat_from_matrix(m))
    end subroutine make_cone_look_at

    !> Whether `point` (in the cone's frame) is inside the cone, between
    !> distances `min_distance` and `max_distance` along the boresight
    !> (defaults 0 and unlimited): inside the polygon of the vertices' directions
    !> (OpenFrames' isVisible; its PolyhedralCone always answers false).
    logical function cone_is_visible(self, point, min_distance, max_distance) result(visible)
        class(PolyhedralCone), intent(in) :: self
        real(wp), intent(in) :: point(3)                    !! in the cone's frame (scene units)
        real(wp), intent(in), optional :: min_distance, max_distance  !! along the boresight (scene units)
        real(wp) :: q(2), a(2), b(2)
        integer :: i, n, winding
        visible = in_range(point, min_distance, max_distance)
        if (.not. visible .or. norm2(point) == 0.0_wp) return
        q = point(1:2) / (-point(3))                            ! on the plane z = -1
        n = size(self%clock)
        winding = 0                                             ! winding number of the polygon around q
        do i = 1, n
            a = tan(self%cone(i)) * [cos(self%clock(i)), sin(self%clock(i))]
            b = tan(self%cone(modulo(i, n) + 1)) * [cos(self%clock(modulo(i, n) + 1)), sin(self%clock(modulo(i, n) + 1))]
            if (a(2) <= q(2)) then
                if (b(2) > q(2) .and. side(a, b, q) > 0.0_wp) winding = winding + 1
            else
                if (b(2) <= q(2) .and. side(a, b, q) < 0.0_wp) winding = winding - 1
            end if
        end do
        visible = winding /= 0
    end function cone_is_visible

    !> Along the boresight (-z) between the distances (a point ahead of the apex).
    pure logical function in_range(point, min_distance, max_distance)
        real(wp), intent(in) :: point(3)                    !! in the cone's frame (scene units)
        real(wp), intent(in), optional :: min_distance, max_distance  !! along the boresight (scene units)
        real(wp) :: d
        d = -point(3)
        in_range = d >= 0.0_wp
        if (present(min_distance)) in_range = in_range .and. d >= min_distance
        if (present(max_distance)) in_range = in_range .and. d <= max_distance
    end function in_range

    !> Which side of the line from a to b q is on: > 0 left, < 0 right, 0 on it
    !> (twice the signed area of the triangle a, b, q).
    pure real(wp) function side(a, b, q)
        real(wp), intent(in) :: a(2), b(2), q(2)            !! the line's two points, and the point tested
        side = (b(1) - a(1)) * (q(2) - a(2)) - (q(1) - a(1)) * (b(2) - a(2))
    end function side

    !> What kind of frame this is: 'PolyhedralCone'.
    function cone_frame_info(self) result(info)
        class(PolyhedralCone), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'PolyhedralCone'
    end function cone_frame_info

    !> The bound of the apex and the base.
    subroutine cone_bound(self, center, radius)
        class(PolyhedralCone), intent(in) :: self
        real(wp), intent(out) :: center(3), radius          !! the bounding sphere, frame coordinates (scene units)
        real(wp) :: r
        r = self%length_ * maxval(tan(self%cone))
        center = [0.0_wp, 0.0_wp, -0.5_wp * self%length_]
        radius = sqrt((0.5_wp * self%length_)**2 + r**2)
    end subroutine cone_bound

    !> Draw the cone under `group` (the contents); with a window's `viewer`, later
    !> drawings are compiled.
    subroutine cone_build_contents(self, viewer, group)
        class(PolyhedralCone), intent(inout) :: self
        type(c_ptr), intent(in) :: viewer                   !! a window's viewer (null: headless)
        type(c_ptr), intent(in) :: group                    !! the frame's contents group
        self%nodes%contents = group
        call make_cone(self)
        self%nodes%shown = c_associated(viewer)
    end subroutine cone_build_contents

    !> Make the sides, edges and base outline (those the draw mode says), in place
    !> of the old.
    subroutine make_cone(self)
        class(PolyhedralCone), intent(inout) :: self
        real(wp), allocatable :: v(:,:), edges(:,:)
        integer, allocatable :: idx(:,:)
        type(c_ptr) :: group
        integer :: i, n
        if (.not. c_associated(self%nodes%contents)) return
        n = size(self%clock)
        allocate(v(3, n + 1), idx(3, n), edges(3, 2 * n))
        v(:, n + 1) = 0.0_wp                                    ! the apex
        do i = 1, n
            v(:, i) = self%length_ * [tan(self%cone(i)) * cos(self%clock(i)), tan(self%cone(i)) * sin(self%clock(i)), -1.0_wp]
            idx(:, i) = [n + 1, i, modulo(i, n) + 1]
            edges(:, 2*i-1) = 0.0_wp
            edges(:, 2*i) = v(:, i)
        end do
        group = vkf_switch_create()
        if (iand(self%mode, CONE_SIDES) /= 0) call add(group, surface_node(v, idx, self%cone_color_))
        if (iand(self%mode, CONE_EDGES) /= 0) call add(group, line_node(edges, self%line_color_, self%width_, .true.))
        if (iand(self%mode, CONE_BASE_OUTLINE) /= 0) &
            call add(group, line_node(closed_segments(v(:, 1:n)), self%line_color_, self%width_, .true.))
        call self%nodes%replace(group)
    end subroutine make_cone

    ! ==================================================================
    ! EllipticCone and RectangularCone

    !> An elliptic cone named `name`, of color `color` (sides and base outline drawn).
    function new_elliptic_cone(name, color) result(c)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(EllipticCone) :: c
        c%PolyhedralCone = PolyhedralCone(name, color)
        c%mode = ior(CONE_SIDES, CONE_BASE_OUTLINE)
        call elliptic_vertices(c)
    end function new_elliptic_cone

    !> The half-angles along x (a) and y (b), radians (0 to 90 degrees).
    subroutine elliptic_set_primary_angles(self, a, b)
        class(EllipticCone), intent(inout) :: self
        real(wp), intent(in) :: a, b                        !! radians
        self%a = a
        self%b = b
        call elliptic_vertices(self)
        call make_cone(self)
    end subroutine elliptic_set_primary_angles

    !> The half-angles along x (a) and y (b), radians.
    pure subroutine elliptic_primary_angles(self, a, b)
        class(EllipticCone), intent(in) :: self
        real(wp), intent(out) :: a, b                       !! radians
        a = self%a
        b = self%b
    end subroutine elliptic_primary_angles

    !> 120 vertices (every 3 degrees), cone angle a b / sqrt(b^2 cos^2 + a^2 sin^2)
    !> (OpenFrames' createEllipticCone).
    subroutine elliptic_vertices(self)
        type(EllipticCone), intent(inout) :: self
        integer, parameter :: n = 120
        integer :: i
        real(wp) :: t
        if (allocated(self%clock)) deallocate(self%clock, self%cone)
        allocate(self%clock(n), self%cone(n))
        do i = 1, n
            t = 2.0_wp * pi * (i - 1) / n
            self%clock(i) = t
            self%cone(i) = self%a * self%b / sqrt(self%b**2 * cos(t)**2 + self%a**2 * sin(t)**2)
        end do
    end subroutine elliptic_vertices

    !> Inside the elliptic cone (exactly; OpenFrames' EllipticCone::isVisible).
    logical function elliptic_is_visible(self, point, min_distance, max_distance) result(visible)
        class(EllipticCone), intent(in) :: self
        real(wp), intent(in) :: point(3)                    !! in the cone's frame (scene units)
        real(wp), intent(in), optional :: min_distance, max_distance  !! along the boresight (scene units)
        real(wp) :: p(3), cone, clock
        visible = in_range(point, min_distance, max_distance)
        if (.not. visible .or. norm2(point) == 0.0_wp) return
        p = point / (-point(3))
        cone = atan(norm2(p(1:2)))
        clock = atan2(p(2), p(1))
        visible = cone <= self%a * self%b / sqrt(self%b**2 * cos(clock)**2 + self%a**2 * sin(clock)**2)
    end function elliptic_is_visible

    !> What kind of frame this is: 'EllipticCone'.
    function elliptic_frame_info(self) result(info)
        class(EllipticCone), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'EllipticCone'
    end function elliptic_frame_info

    !> A rectangular cone named `name`, of color `color`.
    function new_rectangular_cone(name, color) result(c)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(RectangularCone) :: c
        c%PolyhedralCone = PolyhedralCone(name, color)
        call rect_vertices(c)
    end function new_rectangular_cone

    !> The half-angles along x and y, radians (0 to 90 degrees).
    subroutine rect_set_primary_angles(self, x, y)
        class(RectangularCone), intent(inout) :: self
        real(wp), intent(in) :: x, y                        !! radians
        self%x = x
        self%y = y
        call rect_vertices(self)
        call make_cone(self)
    end subroutine rect_set_primary_angles

    !> The half-angles along x and y, radians.
    pure subroutine rect_primary_angles(self, x, y)
        class(RectangularCone), intent(in) :: self
        real(wp), intent(out) :: x, y                       !! radians
        x = self%x
        y = self%y
    end subroutine rect_primary_angles

    !> The 4 corners (OpenFrames' createRectangularCone).
    subroutine rect_vertices(self)
        type(RectangularCone), intent(inout) :: self
        real(wp) :: xl, yl, cone, clock
        xl = tan(self%x)
        yl = tan(self%y)
        cone = atan(sqrt(xl**2 + yl**2))
        clock = atan2(yl, xl)
        self%clock = [clock, pi - clock, pi + clock, -clock]
        self%cone = [cone, cone, cone, cone]
    end subroutine rect_vertices

    !> What kind of frame this is: 'RectangularCone'.
    function rect_frame_info(self) result(info)
        class(RectangularCone), intent(in) :: self
        character(len=:), allocatable :: info
        info = 'RectangularCone'
    end function rect_frame_info

    ! ==================================================================
    ! Polyhedron

    !> A polyhedron named `name`, of color `color` (its faces, edges and
    !> vertices), with no faces yet (set_faces, set_triangles or set_grid).
    function new_polyhedron(name, color) result(p)
        character(len=*), intent(in) :: name                !! the frame's name
        real(wp), intent(in), optional :: color(:)          !! [r, g, b] or [r, g, b, a] (0 to 1)
        type(Polyhedron) :: p
        p%ReferenceFrame = ReferenceFrame(name, color)
        p%face_color_ = p%color()
        p%line_color_ = p%color()
        p%point_color_ = p%color()
        allocate(p%v(3, 0), p%tri(3, 0), p%edge(2, 0))
    end function new_polyhedron

    !> Faces given as polygons: `faces` the vertex numbers (1-based) of each
    !> polygon in turn, `sizes` how many each has (>= 3; convex, or at least
    !> star-shaped from its first vertex: drawn as a fan of triangles from
    !> it); the edges are the polygons' sides. E.g. a cube: 8 vertices, 6
    !> faces of 4. A face's front: its vertices counterclockwise.
    subroutine set_faces(self, vertices, faces, sizes)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: vertices(:,:)               !! (3, n), frame coordinates
        integer, intent(in) :: faces(:)                     !! (sum(sizes)) vertex numbers
        integer, intent(in) :: sizes(:)                     !! each face's number of vertices
        integer :: f, k, j, nt, ne, first
        integer, allocatable :: sides(:,:)
        self%v = vertices
        nt = sum(max(sizes - 2, 0))
        if (allocated(self%tri)) deallocate(self%tri)
        allocate(self%tri(3, nt), sides(2, sum(sizes)))
        nt = 0
        ne = 0
        first = 0
        do f = 1, size(sizes)
            do k = 2, sizes(f) - 1                          ! a fan from the first vertex
                nt = nt + 1
                self%tri(:, nt) = [faces(first + 1), faces(first + k), faces(first + k + 1)]
            end do
            do k = 1, sizes(f)
                j = modulo(k, sizes(f)) + 1
                ne = ne + 1
                sides(:, ne) = [faces(first + k), faces(first + j)]
            end do
            first = first + sizes(f)
        end do
        self%edge = unique_edges(sides(:, 1:ne), size(vertices, 2))
        call drop_colors(self)
        call make_polyhedron(self)
    end subroutine set_faces

    !> Faces given as triangles (3, m) of vertex numbers (1-based); the edges
    !> are their sides (each once).
    subroutine set_triangles(self, vertices, triangles)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: vertices(:,:)               !! (3, n), frame coordinates
        integer, intent(in) :: triangles(:,:)               !! (3, m)
        self%v = vertices
        self%tri = triangles
        self%edge = unique_edges(reshape([triangles(1:2, :), triangles(2:3, :), triangles(3:1:-2, :)], &
                                         [2, 3 * size(triangles, 2)]), size(vertices, 2))
        call drop_colors(self)
        call make_polyhedron(self)
    end subroutine set_triangles

    !> Faces given as a grid of points (3, nu, nv): quadrilaterals between
    !> neighbours (two triangles each), joined around if `closed_u` (the last
    !> point i to the first, for each j; e.g. a tube's circumference) or
    !> `closed_v`. The edges: the grid's lines, those of constant i (along j)
    !> every lines(1) and of constant j (along i) every lines(2) (default:
    !> every one; 0 for none).
    subroutine set_grid(self, points, closed_u, closed_v, lines)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: points(:,:,:)               !! (3, nu, nv), frame coordinates
        logical, intent(in), optional :: closed_u, closed_v !! joined around in i, in j (default false)
        integer, intent(in), optional :: lines(2)           !! edges every lines(1) in i, lines(2) in j (default 1; 0: none)
        integer :: nu, nv, mu, mv, i, j, i2, j2, nt, ne, every(2)
        logical :: cu, cv
        nu = size(points, 2)
        nv = size(points, 3)
        cu = .false.
        cv = .false.
        if (present(closed_u)) cu = closed_u .and. nu > 2
        if (present(closed_v)) cv = closed_v .and. nv > 2
        every = 1
        if (present(lines)) every = max(lines, 0)
        self%v = reshape(points, [3, nu * nv])
        mu = merge(nu, nu - 1, cu)                          ! quadrilaterals along i, j
        mv = merge(nv, nv - 1, cv)
        if (allocated(self%tri)) deallocate(self%tri)
        allocate(self%tri(3, 2 * max(mu, 0) * max(mv, 0)))
        nt = 0
        do j = 1, mv
            j2 = modulo(j, nv) + 1
            do i = 1, mu
                i2 = modulo(i, nu) + 1
                self%tri(:, nt + 1) = [at(i, j), at(i2, j), at(i2, j2)]
                self%tri(:, nt + 2) = [at(i, j), at(i2, j2), at(i, j2)]
                nt = nt + 2
            end do
        end do
        if (allocated(self%edge)) deallocate(self%edge)
        allocate(self%edge(2, nu * nv * 2))
        ne = 0
        if (every(1) > 0) then                              ! lines of constant i, along j
            do i = 1, nu
                if (.not. shown_line(i, nu, every(1), cu)) cycle
                do j = 1, mv
                    ne = ne + 1
                    self%edge(:, ne) = [at(i, j), at(i, modulo(j, nv) + 1)]
                end do
            end do
        end if
        if (every(2) > 0) then                              ! lines of constant j, along i
            do j = 1, nv
                if (.not. shown_line(j, nv, every(2), cv)) cycle
                do i = 1, mu
                    ne = ne + 1
                    self%edge(:, ne) = [at(i, j), at(modulo(i, nu) + 1, j)]
                end do
            end do
        end if
        self%edge = self%edge(:, 1:ne)
        call drop_colors(self)
        call make_polyhedron(self)

    contains

        pure integer function at(i_, j_)
            integer, intent(in) :: i_, j_                       !! grid indices
            at = i_ + (j_ - 1) * nu
        end function at

        !> Line k of n drawn, every `step` (and the last, if the grid is open).
        pure logical function shown_line(k, n, step, closed)
            integer, intent(in) :: k, n, step                   !! the line, number of lines, and spacing
            logical, intent(in) :: closed                       !! the grid is joined around (else the last line drawn too)
            shown_line = modulo(k - 1, step) == 0 .or. (k == n .and. .not. closed)
        end function shown_line

    end subroutine set_grid

    !> The edges drawn (2, k) (vertex numbers), in place of the faces' (after
    !> set_faces, set_triangles or set_grid).
    subroutine set_edges(self, edges)
        class(Polyhedron), intent(inout) :: self
        integer, intent(in) :: edges(:,:)                   !! (2, k) vertex numbers (1-based)
        self%edge = edges
        call make_polyhedron(self)
    end subroutine set_edges

    !> Move the vertices (3, n), keeping the faces and edges (as many as
    !> before; e.g. a surface changing in time).
    subroutine set_vertices(self, vertices)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: vertices(:,:)               !! (3, n), frame coordinates (as many as before)
        if (size(vertices, 2) /= size(self%v, 2)) then
            write(*, '(a)') 'vkframes: Polyhedron '//self%name()//': set_vertices needs as many vertices as before'
            return
        end if
        self%v = vertices
        call make_polyhedron(self)
    end subroutine set_vertices

    !> The number of vertices.
    pure integer function num_vertices(self)
        class(Polyhedron), intent(in) :: self
        num_vertices = size(self%v, 2)
    end function num_vertices

    !> The number of triangles drawn (polygons: as fans).
    pure integer function num_triangles(self)
        class(Polyhedron), intent(in) :: self
        num_triangles = size(self%tri, 2)
    end function num_triangles

    !> The number of edges.
    pure integer function num_edges(self)
        class(Polyhedron), intent(in) :: self
        num_edges = size(self%edge, 2)
    end function num_edges

    !> A color per vertex (4, n) or (3, n) ([r, g, b(, a)], 0 to 1), for the
    !> faces, blended across them (in place of the face color; the opacity
    !> still applies). Dropped when the faces are set again.
    subroutine set_vertex_colors(self, colors)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: colors(:,:)                 !! (4, n) or (3, n): [r, g, b(, a)] (0 to 1)
        integer :: i
        if (size(colors, 2) /= size(self%v, 2) .or. size(colors, 1) < 3) then
            write(*, '(a)') 'vkframes: Polyhedron '//self%name()//': set_vertex_colors needs a color per vertex'
            return
        end if
        if (allocated(self%vertex_colors_)) deallocate(self%vertex_colors_)
        allocate(self%vertex_colors_(4, size(colors, 2)))
        do i = 1, size(colors, 2)
            call set_rgba(self%vertex_colors_(:, i), colors(:, i))
        end do
        call make_polyhedron(self)
    end subroutine set_vertex_colors

    !> The face color again, for every vertex.
    subroutine remove_vertex_colors(self)
        class(Polyhedron), intent(inout) :: self
        call drop_colors(self)
        call make_polyhedron(self)
    end subroutine remove_vertex_colors

    !> Whether the faces have a color per vertex.
    pure logical function has_vertex_colors(self)
        class(Polyhedron), intent(in) :: self
        has_vertex_colors = allocated(self%vertex_colors_)
    end function has_vertex_colors

    !> The faces' color (the alpha times the opacity).
    subroutine set_face_color(self, color)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%face_color_, color)
        call make_polyhedron(self)
    end subroutine set_face_color

    !> The faces' color [r, g, b, a].
    pure function face_color(self) result(c)
        class(Polyhedron), intent(in) :: self
        real(wp) :: c(4)
        c = self%face_color_
    end function face_color

    !> The edges' color.
    subroutine poly_set_line_color(self, color)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%line_color_, color)
        call make_polyhedron(self)
    end subroutine poly_set_line_color

    !> The edges' color [r, g, b, a].
    pure function poly_line_color(self) result(c)
        class(Polyhedron), intent(in) :: self
        real(wp) :: c(4)
        c = self%line_color_
    end function poly_line_color

    !> The vertices' color (POLY_VERTICES).
    subroutine set_point_color(self, color)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: color(:)                    !! [r, g, b] or [r, g, b, a] (0 to 1)
        call set_rgba(self%point_color_, color)
        call make_polyhedron(self)
    end subroutine set_point_color

    !> The vertices' color [r, g, b, a].
    pure function point_color(self) result(c)
        class(Polyhedron), intent(in) :: self
        real(wp) :: c(4)
        c = self%point_color_
    end function point_color

    !> The faces' opacity (0 to 1; default 1, opaque): below 1 translucent.
    subroutine poly_set_opacity(self, opacity)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: opacity                     !! 0 to 1 (1: opaque)
        self%opacity_ = max(0.0_wp, min(1.0_wp, opacity))
        call make_polyhedron(self)
    end subroutine poly_set_opacity

    !> The faces' opacity.
    pure real(wp) function poly_opacity(self)
        class(Polyhedron), intent(in) :: self
        poly_opacity = self%opacity_
    end function poly_opacity

    !> The edges' width (window units; default 1).
    subroutine poly_set_line_width(self, width)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: width                       !! window units
        self%width_ = max(0.0_wp, width)
        call make_polyhedron(self)
    end subroutine poly_set_line_width

    !> The edges' width (window units).
    pure real(wp) function poly_line_width(self)
        class(Polyhedron), intent(in) :: self
        poly_line_width = self%width_
    end function poly_line_width

    !> The vertices' size (window units; default 6).
    subroutine set_point_size(self, size_)
        class(Polyhedron), intent(inout) :: self
        real(wp), intent(in) :: size_                       !! window units
        self%point_size_ = max(0.0_wp, size_)
        call make_polyhedron(self)
    end subroutine set_point_size

    !> The vertices' size (window units).
    pure real(wp) function point_size(self)
        class(Polyhedron), intent(in) :: self
        point_size = self%point_size_
    end function point_size

    !> What to draw: an ior of POLY_FACES, POLY_EDGES, POLY_VERTICES (default
    !> faces and edges).
    subroutine poly_set_draw_mode(self, mode)
        class(Polyhedron), intent(inout) :: self
        integer, intent(in) :: mode                         !! an ior of POLY_FACES, POLY_EDGES, POLY_VERTICES
        self%mode = mode
        call make_polyhedron(self)
    end subroutine poly_set_draw_mode

    !> What is drawn: an ior of POLY_FACES, POLY_EDGES, POLY_VERTICES.
    pure integer function poly_draw_mode(self)
        class(Polyhedron), intent(in) :: self
        poly_draw_mode = self%mode
    end function poly_draw_mode

    !> Faces lit by the scene's lights (the default), or unlit (their color
    !> as is, transparent as a cone's sides: writing no depth).
    subroutine set_lit(self, lit)
        class(Polyhedron), intent(inout) :: self
        logical, intent(in) :: lit                          !! true: lit; false: unlit and transparent
        self%lit_ = lit
        call make_polyhedron(self)
    end subroutine set_lit

    !> Whether the faces are lit.
    pure logical function lit(self)
        class(Polyhedron), intent(in) :: self
        lit = self%lit_
    end function lit

    !> Shading smooth across the vertices (normals averaged over the faces
    !> around each: a curved surface, e.g. a grid's), or flat faces (the
    !> default: a polyhedron's).
    subroutine set_smooth(self, smooth)
        class(Polyhedron), intent(inout) :: self
        logical, intent(in) :: smooth                       !! true: smooth shading; false: flat faces
        self%smooth_ = smooth
        call make_polyhedron(self)
    end subroutine set_smooth

    !> Whether the shading is smooth.
    pure logical function smooth(self)
        class(Polyhedron), intent(in) :: self
        smooth = self%smooth_
    end function smooth

    !> Both sides of the faces drawn and lit (the default: open surfaces), or
    !> only their fronts (a closed surface, its faces counterclockwise seen
    !> from outside: translucent, its far side isn't drawn through it).
    subroutine set_two_sided(self, two_sided)
        class(Polyhedron), intent(inout) :: self
        logical, intent(in) :: two_sided                    !! true: both sides; false: fronts only
        self%two_sided_ = two_sided
        call make_polyhedron(self)
    end subroutine set_two_sided

    !> Whether both sides are drawn.
    pure logical function two_sided(self)
        class(Polyhedron), intent(in) :: self
        two_sided = self%two_sided_
    end function two_sided