!> Reference frames: the tree of frames that every scene object belongs to, as !> OpenFrames' ReferenceFrame. !> !> A frame has a position and attitude relative to its parent, a color for its !> decorations (axes, labels), and children. It can follow trajectories !> ([[follow]]), which then set its position and/or attitude at each [[update]]. !> Scene objects (spheres, models, trajectories' artists, ...) extend this type. !> !> Frames are owned by the program, not by the tree: [[add_child]] stores !> pointers, so a frame (declared `target`, or allocated) must stay alive while !> it is in a tree, or be removed first ([[detach]]). A frame may have several !> parents; adding a child that is an ancestor of the parent is refused. !> !> Rendering: when a window first shows a tree ([[realize]]), each frame gets a !> transform node, with its contents ([[build_contents]], which extensions !> override) under a switch for [[show_contents]]; children's nodes go under the !> parent's transform, so a frame with several parents is drawn under each. !> Poses at any time ([[world_matrix_at]]): a frame's transform to the root !> at a time other than the current one, from what moves it: its fixed !> position and attitude, and the trajectories it follows (their state at that !> time, [[TrajectoryFollower:state_at]]). A frame the program moves (sets its !> position each frame) has no history: its current pose is used for every !> time. Trajectories drawn in a frame other than the one their data are in !> (DrawableTrajectory's set_data_frame) use these, each point at its own !> time; [[motion_key]] tells them when a frame's motion changed (its pose !> set, its parents, what it follows and how, the followed data). !> !> Each frame, [[update]] moves followed frames to the time, then [[late_update]] !> lets contents depend on other frames' positions at that time (e.g. !> CustomLineSegments between spacecraft), and [[sync]] copies positions and !> attitudes to the transforms. A shown !> tree can change: children added to a shown frame are realized and compiled !> (for the windows running) and drawn from the next frame; !> removed ones stop being drawn; moved axes (move_x_axis, ...) are made again. !> !> Labels: the name label (at the origin, facing the screen) and the axes' !> labels can change while shown: the name ([[set_name]], e.g. a readout that !> follows the frame), color, label size, alignment and font, and the axes' !> label texts. !> !> Picking ([[WindowProxy:pick]]): each frame tests what it draws against a !> PickCamera (vkf_pick) in its [[pick]] procedure, given its transform to the !> root frame, and adds a PickHit for each thing within reach (the window !> sorts them nearest first). A plain frame is picked at its origin if it !> shows axes or its name label (PICK_FRAME); extensions pick their contents !> (bodies, trajectories). module vkf_frames use, intrinsic :: iso_c_binding, only: c_ptr, c_null_ptr, c_associated, c_int, c_float, c_double use vkf_layer, only: check_status, vkf_transform_create, vkf_transform_set_matrix, vkf_switch_create, vkf_switch_set, & vkf_node_add_child, vkf_node_remove_child, vkf_node_delete, vkf_compile_node, vkf_viewer_any, & vkf_light_create, vkf_light_set, vkf_label_set_text, vkf_label_set_color, vkf_label_set_on_top use vkf_geometry, only: arrow_create use vkf_text, only: label_create, label_set_layout use vkf_math, only: wp, quat_identity, quat_normalize, frame_matrix, rigid_inverse, identity_matrix, & quat_multiply, quat_from_matrix use vkf_pick, only: PickCamera, point_pick, ray, ray_ellipsoid, wants use vkf_trajectory, only: Trajectory use vkf_follower, only: TrajectoryFollower, FOLLOW_LOOP, FOLLOW_POSITION, FOLLOW_ATTITUDE, next_motion_stamp implicit none private !> which axes to show (OpenFrames' AxesType), combined with ior integer, parameter, public :: NO_AXES = 0 !! none integer, parameter, public :: X_AXIS = 1 !! the x axis integer, parameter, public :: Y_AXIS = 2 !! the y axis integer, parameter, public :: Z_AXIS = 4 !! the z axis integer, parameter, public :: ALL_AXES = 7 !! all three !> text alignment of labels (osgText's AlignmentType, as OpenFrames uses) integer, parameter, public :: ALIGN_LEFT_TOP = 0, ALIGN_LEFT_CENTER = 1, ALIGN_LEFT_BOTTOM = 2 integer, parameter, public :: ALIGN_CENTER_TOP = 3, ALIGN_CENTER_CENTER = 4, ALIGN_CENTER_BOTTOM = 5 integer, parameter, public :: ALIGN_RIGHT_TOP = 6, ALIGN_RIGHT_CENTER = 7, ALIGN_RIGHT_BOTTOM = 8 integer, parameter, public :: ALIGN_LEFT_BASE_LINE = 9, ALIGN_CENTER_BASE_LINE = 10, ALIGN_RIGHT_BASE_LINE = 11 !> A frame in a list of frames (a frame's children and parents): a pointer !> to it (Fortran has no arrays of pointers). type :: FramePointer class(ReferenceFrame), pointer :: p => null() !! the frame (not owned) end type FramePointer !> what a pick hit (PickHit%kind) integer, parameter, public :: PICK_FRAME = 1 !! a frame's origin (its axes or name label shown) integer, parameter, public :: PICK_BODY = 2 !! a body's surface (Sphere, Model, TerrainBody) integer, parameter, public :: PICK_CURVE = 3 !! a trajectory's curve (CurveArtist) integer, parameter, public :: PICK_SEGMENT = 4 !! a trajectory's segment (SegmentArtist) integer, parameter, public :: PICK_MARKER = 5 !! a trajectory's marker (MarkerArtist) integer, parameter, public :: PICK_HANDLE = 6 !! a handle the mouse drags (vkf_handles: VectorHandle) !> What a pick found ([[WindowProxy:pick]]): the frame it belongs to and, for !> trajectories, the artist (select type to use it) and the trajectory. type, public :: PickHit integer :: kind = 0 !! PICK_* class(ReferenceFrame), pointer :: frame => null() !! the frame hit (what it draws, or its origin) class(*), pointer :: artist => null() !! the trajectory artist (curves, segments, markers) type(Trajectory), pointer :: trajectory => null() !! (trajectories: the artist's trajectory) !> For curves, the trajectory point the hit is after (on the segment from it !> to the next); segments: the segment (from point 1 + (index - 1) * !> stride); markers: the point (start, end) or the intermediate !> marker's number integer :: index = 0 integer :: marker = 0 !! (markers: MARKER_START, MARKER_END or MARKER_INTERMEDIATE) real(wp) :: time = 0.0_wp !! (trajectories: the time there (interpolated on curves)) real(wp) :: position(3) = 0.0_wp !! the point hit, in the root frame real(wp) :: local(3) = 0.0_wp !! the point hit, in the frame's coordinates real(wp) :: distance = 0.0_wp !! its depth from the eye (nearest first) real(wp) :: pixels = 0.0_wp !! its distance from the cursor (framebuffer pixels; 0 on a body) real(wp) :: latitude = 0.0_wp, longitude = 0.0_wp !! (bodies: geodetic, radians (Sphere, TerrainBody)) end type PickHit public :: add_pick_hit, pick_ellipsoid !> A reference frame (see the module documentation). type, public :: ReferenceFrame private character(len=:), allocatable :: name_ !! the name (shown by the name label) real(wp) :: color_(4) = [1.0_wp, 1.0_wp, 1.0_wp, 0.9_wp] !! decorations' color [r, g, b, a] (0 to 1) real(wp) :: position_(3) = 0.0_wp !! relative to the parent real(wp) :: attitude_(4) = quat_identity !! relative to the parent: quaternion [x, y, z, w] type(FramePointer), allocatable :: children(:), parents(:) !! not owned (first n_children, n_parents used) integer :: n_children = 0, n_parents = 0 !! number of children and parents type(TrajectoryFollower) :: follower_ !! sets position and attitude from followed trajectories logical :: following = .false. !! following trajectories ([[follow]], until [[unfollow]]) class(ReferenceFrame), pointer :: follow_frame_ => null() !! the frame the followed data are in (null: the parent) integer :: stamp_ = 0 !! motion stamp of the last change of its pose or parents ! decorations (drawn by the renderer) integer :: axes = ALL_AXES !! OpenFrames' defaults: all shown integer :: axes_labels = ALL_AXES !! axes labels shown (as axes) logical :: name_label = .true. !! the name label shown logical :: name_on_top = .false. !! the name label drawn on top of the scene character(len=:), allocatable :: x_label_, y_label_, z_label_ !! axes labels (unallocated: 'X', 'Y', 'Z') character(len=:), allocatable :: label_font_ !! labels' font (unallocated: the renderer's default) integer :: label_size_ = 20 !! labels' size (pixels) integer :: label_alignment_ = ALIGN_LEFT_BASE_LINE !! labels' alignment (ALIGN_*) logical :: light_source = .false. !! carries a light source (set_light_source_enabled) ! the light source's colors (OSG's default light, as OpenFrames') real(wp) :: light_ambient_(3) = 0.05_wp, light_diffuse_(3) = 0.8_wp, light_specular_(3) = 0.05_wp !! [r, g, b] type(c_ptr) :: light_node = c_null_ptr, light_switch = c_null_ptr !! light and switch (made when enabled) logical :: contents = .true. !! what the frame draws shown (show_contents) logical :: pickable_ = .true. !! picked ([[WindowProxy:pick]]); its children aside ! rendering type(c_ptr) :: node_ = c_null_ptr !! the frame's transform node type(c_ptr) :: contents_node = c_null_ptr !! switch holding the contents logical :: shown_ = .false. !! realized for a window (not headless): later nodes are compiled logical :: realized = .false. !! render nodes made (realize) logical :: synced = .false. !! the node has synced_position/attitude real(wp) :: synced_position(3) = 0.0_wp, synced_attitude(4) = 0.0_wp !! last copied to the node ! axes (OpenFrames' moveXAxis/...: base, length, head ratio, radii; 0 radius: auto) real(wp) :: axis_base(3,3) = 0.0_wp !! each axis's base (columns; frame coordinates) real(wp) :: axis_len(3) = 1.0_wp !! each axis's length real(wp) :: axis_head_ratio(3) = 0.3_wp !! each axis's head, as a fraction of its length real(wp) :: axis_body_radius(3) = 0.0_wp, axis_head_radius(3) = 0.0_wp !! (0: 0.05 and 0.1 of the length) logical :: axes_moved = .false. !! set by move_*_axis (else reposition_axes places them) type(c_ptr) :: axis_node(3) = c_null_ptr !! the axes arrows' switches type(c_ptr) :: axis_label_node(3) = c_null_ptr !! the axes labels' switches type(c_ptr) :: axis_arrow(3) = c_null_ptr !! the axes arrows (in axis_node; made again when an axis moves) type(c_ptr) :: axis_text(3) = c_null_ptr !! the axes labels (in axis_label_node) type(c_ptr) :: name_label_node = c_null_ptr !! the name label's switch type(c_ptr) :: name_text = c_null_ptr !! the name label (in name_label_node) contains procedure :: set_name, name procedure :: set_color, color procedure :: set_position, position procedure :: set_attitude, attitude procedure :: local_matrix, world_matrix, position_of procedure :: local_matrix_at, world_matrix_at, motion_key procedure :: show_axes, axes_shown procedure :: show_axes_labels, axes_labels_shown procedure :: show_name_label, name_label_shown procedure :: show_decorations procedure :: set_name_label_on_top, name_label_on_top procedure :: set_x_label, set_y_label, set_z_label, axis_label procedure :: set_label_font, label_font procedure :: set_label_size, label_size procedure :: set_label_alignment, label_alignment procedure :: set_light_source_enabled, light_source_enabled, has_light_source procedure :: set_light_ambient, set_light_diffuse, set_light_specular procedure :: light_ambient, light_diffuse, light_specular procedure :: show_contents, contents_shown procedure :: frame_info procedure :: add_child, remove_child, detach procedure :: num_children, child, child_index procedure :: num_parents, parent, parent_index procedure :: frame_string procedure :: follow, unfollow, follower, is_following, follow_frame procedure :: update, late_update procedure :: realize, build_contents, sync, node procedure :: bound, pick, set_pickable, pickable procedure :: move_x_axis, move_y_axis, move_z_axis, reposition_axes end type ReferenceFrame !> The ReferenceFrame constructor: `ReferenceFrame(...)` makes one (see [[new_reference_frame]] for its !> arguments). interface ReferenceFrame module procedure new_reference_frame end interface ReferenceFrame public :: FramePointer contains !> A frame named `name`, with decorations of color `color` ([r, g, b] or !> [r, g, b, a], 0 to 1; default white, 90% opaque). function new_reference_frame(name, color) result(frame) character(len=*), intent(in) :: name !! the frame's name (shown by its name label) real(wp), intent(in), optional :: color(:) !! [r, g, b] or [r, g, b, a] (0 to 1; default white, 90% opaque) type(ReferenceFrame) :: frame frame%name_ = name if (present(color)) call frame%set_color(color) end function new_reference_frame ! ================================================================== ! name, color, position, attitude !> The frame's name, shown by its name label (also once shown: e.g. a !> readout that follows the frame). subroutine set_name(self, name) class(ReferenceFrame), intent(inout) :: self character(len=*), intent(in) :: name !! the name (shown by the name label) self%name_ = name if (c_associated(self%name_text)) call check_status(vkf_label_set_text(self%name_text, name, len(name, kind=c_int))) end subroutine set_name !> The frame's name ('' if none). function name(self) result(n) class(ReferenceFrame), intent(in) :: self character(len=:), allocatable :: n if (allocated(self%name_)) then n = self%name_ else n = '' end if end function name !> The color of the frame's decorations: [r, g, b] (alpha unchanged) or [r, g, b, a]. subroutine set_color(self, color) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: color(:) !! [r, g, b] (alpha unchanged) or [r, g, b, a] (0 to 1) integer :: n n = min(4, size(color)) self%color_(1:n) = color(1:n) if (.not. self%realized) return if (c_associated(self%name_text)) call check_status(vkf_label_set_color(self%name_text, real(self%color_, c_float))) call remake_axes(self) end subroutine set_color !> The color of the frame's decorations, [r, g, b, a] (0 to 1). pure function color(self) result(c) class(ReferenceFrame), intent(in) :: self real(wp) :: c(4) c = self%color_ end function color !> Position relative to the parent frame (overridden by a followed trajectory). subroutine set_position(self, position) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: position(3) !! parent frame's coordinates if (any(position /= self%position_)) self%stamp_ = next_motion_stamp() self%position_ = position end subroutine set_position !> Position relative to the parent frame. pure function position(self) result(p) class(ReferenceFrame), intent(in) :: self real(wp) :: p(3) p = self%position_ end function position !> Attitude relative to the parent frame, a quaternion [x, y, z, w] !> (normalized here; overridden by a followed trajectory). subroutine set_attitude(self, q) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: q(4) !! attitude quaternion [x, y, z, w] (normalized here) real(wp) :: qn(4) qn = quat_normalize(q) if (any(qn /= self%attitude_)) self%stamp_ = next_motion_stamp() self%attitude_ = qn end subroutine set_attitude !> Attitude relative to the parent frame, a quaternion [x, y, z, w]. pure function attitude(self) result(q) class(ReferenceFrame), intent(in) :: self real(wp) :: q(4) q = self%attitude_ end function attitude !> The transform from this frame to its parent (4x4). pure function local_matrix(self) result(m) class(ReferenceFrame), intent(in) :: self real(wp) :: m(4,4) m = frame_matrix(self%position_, self%attitude_) end function local_matrix !> The transform from this frame to the root of its tree, through the first !> parent at each level. recursive function world_matrix(self) result(m) class(ReferenceFrame), intent(in) :: self real(wp) :: m(4,4) m = self%local_matrix() if (self%n_parents > 0) m = matmul(self%parents(1)%p%world_matrix(), m) end function world_matrix !> The transform from this frame to its parent at simulation time `t`: its !> fixed position and attitude, with what it follows at time t (the !> followed trajectories' state then, in the parent's coordinates). The !> same as [[local_matrix]] after [[update]] at time t, for any t, without !> changing anything. recursive function local_matrix_at(self, t) result(m) class(ReferenceFrame), intent(in) :: self real(wp), intent(in) :: t !! simulation time real(wp) :: m(4,4) real(wp) :: pos(3), att(4) pos = self%position_ att = self%attitude_ if (self%following) call followed_pose(self, t, pos, att) m = frame_matrix(pos, att) end function local_matrix_at !> The transform from this frame to the root of its tree at simulation !> time `t` (through the first parent at each level): each frame's !> [[local_matrix_at]] time t. The same as [[world_matrix]] after the tree's !> update at time t, for any t, without changing anything. recursive function world_matrix_at(self, t) result(m) class(ReferenceFrame), intent(in) :: self real(wp), intent(in) :: t !! simulation time real(wp) :: m(4,4) integer, save :: depth = 0 depth = depth + 1 if (depth > 256) error stop 'vkframes: frames following data in frames that depend on them (a cycle)' m = self%local_matrix_at(t) if (self%n_parents > 0) m = matmul(self%parents(1)%p%world_matrix_at(t), m) depth = depth - 1 end function world_matrix_at !> What the frame's motion depends on, for drawings that depend on it to !> know when to redraw: `stamp`, the latest change of the motion of it and !> its ancestors (through first parents), and of the frames their followed !> data are in (poses set, parents changed, what is followed and how); and !> `data`, the followed trajectories' revisions summed. Either changes !> when the frame's motion over time changes. recursive subroutine motion_key(self, stamp, data) class(ReferenceFrame), intent(in) :: self integer, intent(out) :: stamp, data !! the latest change; the followed data's revisions integer :: s, d stamp = self%stamp_ data = 0 if (self%following) then stamp = max(stamp, self%follower_%motion_stamp()) data = self%follower_%data_revision() if (associated(self%follow_frame_)) then call self%follow_frame_%motion_key(s, d) stamp = max(stamp, s) data = data + d end if end if if (self%n_parents > 0) then call self%parents(1)%p%motion_key(s, d) stamp = max(stamp, s) data = data + d end if end subroutine motion_key !> The origin of `other` (a frame of the same tree) in this frame's !> coordinates, from both frames' transforms to the root (through their !> first parents). function position_of(self, other) result(p) class(ReferenceFrame), intent(in) :: self, other !! (other: a frame of the same tree) real(wp) :: p(3) real(wp) :: m(4,4) m = matmul(rigid_inverse(self%world_matrix()), other%world_matrix()) p = m(1:3, 4) end function position_of ! ================================================================== ! decorations !> Which axes vectors to show: NO_AXES, or X_AXIS, Y_AXIS, Z_AXIS combined with ior. subroutine show_axes(self, axes) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: axes !! NO_AXES, or X_AXIS, Y_AXIS, Z_AXIS combined with ior self%axes = iand(axes, ALL_AXES) call update_switches(self) end subroutine show_axes !> Which axes vectors are shown (NO_AXES, or X_AXIS, Y_AXIS, Z_AXIS combined). pure integer function axes_shown(self) class(ReferenceFrame), intent(in) :: self axes_shown = self%axes end function axes_shown !> Which axes labels to show (as [[show_axes]]). subroutine show_axes_labels(self, labels) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: labels !! NO_AXES, or X_AXIS, Y_AXIS, Z_AXIS combined with ior self%axes_labels = iand(labels, ALL_AXES) call update_switches(self) end subroutine show_axes_labels !> Which axes labels are shown (as [[axes_shown]]). pure integer function axes_labels_shown(self) class(ReferenceFrame), intent(in) :: self axes_labels_shown = self%axes_labels end function axes_labels_shown !> Show or hide the name label (at the frame's origin; default shown). subroutine show_name_label(self, show) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: show !! (true: shown) self%name_label = show call update_switches(self) end subroutine show_name_label !> Show (true) or hide all the frame's decorations at once: its axes, !> their labels and its name label (show_axes, show_axes_labels, !> show_name_label). subroutine show_decorations(self, show) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: show !! true: all axes, axes labels and the name label call self%show_axes(merge(ALL_AXES, NO_AXES, show)) call self%show_axes_labels(merge(ALL_AXES, NO_AXES, show)) call self%show_name_label(show) end subroutine show_decorations !> Draw the name label on top of the scene (true: not hidden by bodies in !> front of it, as HUD text; e.g. a readout) or depth-tested (false, the !> default, as OpenFrames'). subroutine set_name_label_on_top(self, on_top) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: on_top !! (true: drawn on top of the scene; false: depth-tested) self%name_on_top = on_top if (c_associated(self%name_text)) & call check_status(vkf_label_set_on_top(self%name_text, merge(1_c_int, 0_c_int, on_top))) end subroutine set_name_label_on_top !> Whether the name label is drawn on top of the scene ([[set_name_label_on_top]]). pure logical function name_label_on_top(self) class(ReferenceFrame), intent(in) :: self name_label_on_top = self%name_on_top end function name_label_on_top !> Whether the name label is shown. pure logical function name_label_shown(self) class(ReferenceFrame), intent(in) :: self name_label_shown = self%name_label end function name_label_shown !> The text of the x-axis label (default 'X'). subroutine set_x_label(self, text) class(ReferenceFrame), intent(inout) :: self character(len=*), intent(in) :: text !! the x-axis label (default 'X') self%x_label_ = text if (self%realized) call make_axis(self, 1) end subroutine set_x_label !> The text of the y-axis label (default 'Y'). subroutine set_y_label(self, text) class(ReferenceFrame), intent(inout) :: self character(len=*), intent(in) :: text !! the y-axis label (default 'Y') self%y_label_ = text if (self%realized) call make_axis(self, 2) end subroutine set_y_label !> The text of the z-axis label (default 'Z'). subroutine set_z_label(self, text) class(ReferenceFrame), intent(inout) :: self character(len=*), intent(in) :: text !! the z-axis label (default 'Z') self%z_label_ = text if (self%realized) call make_axis(self, 3) end subroutine set_z_label !> The label text of axis X_AXIS, Y_AXIS or Z_AXIS. function axis_label(self, axis) result(text) class(ReferenceFrame), intent(in) :: self integer, intent(in) :: axis !! X_AXIS, Y_AXIS or Z_AXIS (else: '') character(len=:), allocatable :: text select case (axis) case (X_AXIS) text = 'X' if (allocated(self%x_label_)) text = self%x_label_ case (Y_AXIS) text = 'Y' if (allocated(self%y_label_)) text = self%y_label_ case (Z_AXIS) text = 'Z' if (allocated(self%z_label_)) text = self%z_label_ case default text = '' end select end function axis_label !> The font of the labels: a font file name (found on the data search !> path) or a full path; default: the renderer's default font. subroutine set_label_font(self, font) class(ReferenceFrame), intent(inout) :: self character(len=*), intent(in) :: font !! a font file name (on the data search path) or a full path self%label_font_ = font if (.not. self%realized) return call make_name_label(self) ! (another font: made again) call remake_axes(self) end subroutine set_label_font !> The labels' font as set ('' for the renderer's default). function label_font(self) result(font) class(ReferenceFrame), intent(in) :: self character(len=:), allocatable :: font if (allocated(self%label_font_)) then font = self%label_font_ else font = '' end if end function label_font !> The size of the labels in pixels (default 20): the name label's size, !> and the maximum size of the axes labels (which shrink with distance). subroutine set_label_size(self, size) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: size !! pixels (default 20) self%label_size_ = size call relayout_labels(self) end subroutine set_label_size !> The labels' size in pixels. pure integer function label_size(self) class(ReferenceFrame), intent(in) :: self label_size = self%label_size_ end function label_size !> The alignment of the labels relative to their positions (ALIGN_*; !> default ALIGN_LEFT_BASE_LINE). subroutine set_label_alignment(self, alignment) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: alignment !! ALIGN_* (default ALIGN_LEFT_BASE_LINE) self%label_alignment_ = alignment call relayout_labels(self) end subroutine set_label_alignment !> The labels' alignment (ALIGN_*). pure integer function label_alignment(self) class(ReferenceFrame), intent(in) :: self label_alignment = self%label_alignment_ end function label_alignment !> Whether this frame carries a light source: a point light at its origin !> (e.g. the Sun), not dimmed with distance (default false; OpenFrames' !> setLightSourceEnabled). While any frame of a window's scene has one, the !> window's own light (WindowProxy%set_light_*) is off. subroutine set_light_source_enabled(self, enable) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: enable !! (true: a point light at the frame's origin) self%light_source = enable if (self%realized .and. enable .and. .not. c_associated(self%light_node)) call build_light(self) if (c_associated(self%light_switch)) & call check_status(vkf_switch_set(self%light_switch, merge(1_c_int, 0_c_int, enable))) end subroutine set_light_source_enabled !> Whether this frame carries a light source. pure logical function light_source_enabled(self) class(ReferenceFrame), intent(in) :: self light_source_enabled = self%light_source end function light_source_enabled !> Whether this frame or one of its descendants has a light source enabled. recursive logical function has_light_source(self) result(has) class(ReferenceFrame), intent(in) :: self integer :: k has = self%light_source do k = 1, self%n_children if (has) return has = self%children(k)%p%has_light_source() end do end function has_light_source !> The light source's ambient, diffuse and specular colors ([r, g, b], 0 !> to 1; defaults 0.05, 0.8 and 0.05 gray). subroutine set_light_ambient(self, rgb) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: rgb(3) !! ambient color [r, g, b] (0 to 1; default 0.05 gray) self%light_ambient_ = rgb call apply_light(self) end subroutine set_light_ambient !> The light source's diffuse color (see [[set_light_ambient]]). subroutine set_light_diffuse(self, rgb) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: rgb(3) !! diffuse color [r, g, b] (0 to 1; default 0.8 gray) self%light_diffuse_ = rgb call apply_light(self) end subroutine set_light_diffuse !> The light source's specular color (see [[set_light_ambient]]). subroutine set_light_specular(self, rgb) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: rgb(3) !! specular color [r, g, b] (0 to 1; default 0.05 gray) self%light_specular_ = rgb call apply_light(self) end subroutine set_light_specular !> The light source's ambient color [r, g, b]. pure function light_ambient(self) result(rgb) class(ReferenceFrame), intent(in) :: self real(wp) :: rgb(3) rgb = self%light_ambient_ end function light_ambient !> The light source's diffuse color [r, g, b]. pure function light_diffuse(self) result(rgb) class(ReferenceFrame), intent(in) :: self real(wp) :: rgb(3) rgb = self%light_diffuse_ end function light_diffuse !> The light source's specular color [r, g, b]. pure function light_specular(self) result(rgb) class(ReferenceFrame), intent(in) :: self real(wp) :: rgb(3) rgb = self%light_specular_ end function light_specular !> The light source's node: a point light at the origin, under a switch. subroutine build_light(self) class(ReferenceFrame), intent(inout) :: self self%light_node = vkf_light_create() if (.not. c_associated(self%light_node)) return self%light_switch = vkf_switch_create() call check_status(vkf_node_add_child(self%light_switch, self%light_node)) call check_status(vkf_node_add_child(self%node_, self%light_switch)) call apply_light(self) end subroutine build_light !> Give the light source's node its colors (if it has been made). subroutine apply_light(self) class(ReferenceFrame), intent(inout) :: self if (.not. c_associated(self%light_node)) return call check_status(vkf_light_set(self%light_node, real(self%light_ambient_, c_float), real(self%light_diffuse_, c_float), & real(self%light_specular_, c_float), [0.0_c_double, 0.0_c_double, 0.0_c_double, 1.0_c_double])) end subroutine apply_light !> Show or hide what the frame draws (its sphere, model, ...), but not its !> axes, labels or children. subroutine show_contents(self, show) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: show !! (true: shown) self%contents = show if (c_associated(self%contents_node)) call check_status(vkf_switch_set(self%contents_node, merge(1_c_int, 0_c_int, show))) end subroutine show_contents !> Whether what the frame draws is shown ([[show_contents]]). pure logical function contents_shown(self) class(ReferenceFrame), intent(in) :: self contents_shown = self%contents end function contents_shown !> Whether [[pick]] tests what this frame draws (default true; e.g. false !> for an overlay such as a selection marker). Its children are picked or !> not by their own setting. subroutine set_pickable(self, pickable) class(ReferenceFrame), intent(inout) :: self logical, intent(in) :: pickable !! (true: tested by [[pick]]) self%pickable_ = pickable end subroutine set_pickable !> Whether [[pick]] tests what this frame draws ([[set_pickable]]). pure logical function pickable(self) class(ReferenceFrame), intent(in) :: self pickable = self%pickable_ end function pickable !> Pick what the frame draws (see the module documentation): `to_root` !> is its transform to the root frame; hits are added to hits(1:n). A !> plain frame: its origin, if it shows axes or its name label. subroutine pick(self, cam, to_root, hits, n) class(ReferenceFrame), intent(in), target :: self type(PickCamera), intent(in) :: cam !! the cursor's ray and the pick's settings real(wp), intent(in) :: to_root(4,4) !! the frame's transform to the root frame type(PickHit), allocatable, intent(inout) :: hits(:) !! hits so far (grown as needed) integer, intent(inout) :: n !! number of hits in hits type(PickHit) :: h if (self%axes == NO_AXES .and. .not. self%name_label) return if (.not. wants(cam, PICK_FRAME)) return if (.not. point_pick(cam, to_root(1:3, 4), h%pixels, h%distance)) return h%kind = PICK_FRAME h%frame => self h%position = to_root(1:3, 4) call add_pick_hit(hits, n, h) end subroutine pick !> Pick a body of frame `self`: the ellipsoid centered at `center` with !> semi-axes `radii` along the columns of `axes` (unit vectors; all in the !> frame's coordinates), hit by the ray through the cursor. Adds a !> PICK_BODY hit (with the geodetic latitude and longitude of the point !> on the ellipsoid if `geodetic`); true if hit. logical function pick_ellipsoid(self, cam, to_root, center, axes, radii, geodetic, hits, n) result(hit) class(ReferenceFrame), intent(in), target :: self type(PickCamera), intent(in) :: cam !! the cursor's ray and the pick's settings real(wp), intent(in) :: to_root(4,4), center(3), axes(3,3), radii(3) !! center, axes: frame coordinates logical, intent(in) :: geodetic !! (true: also the hit's geodetic latitude and longitude) type(PickHit), allocatable, intent(inout) :: hits(:) !! hits so far (grown as needed) integer, intent(inout) :: n !! number of hits in hits type(PickHit) :: h real(wp) :: origin(3), dir(3), along, local(3), normal(3) hit = .false. if (.not. wants(cam, PICK_BODY)) return call ray(cam, origin, dir) hit = ray_ellipsoid(origin, dir, matmul(to_root(1:3, 1:3), center) + to_root(1:3, 4), & matmul(to_root(1:3, 1:3), axes), radii, along, h%position, local) if (hit) hit = dot_product(h%position - cam%eye, cam%forward) <= cam%max_depth if (.not. hit) return h%kind = PICK_BODY h%frame => self h%local = matmul(transpose(to_root(1:3, 1:3)), h%position - to_root(1:3, 4)) h%distance = dot_product(h%position - cam%eye, cam%forward) h%pixels = 0.0_wp if (geodetic) then normal = local / radii**2 ! (the surface normal there) h%latitude = asin(max(-1.0_wp, min(1.0_wp, normal(3) / norm2(normal)))) h%longitude = atan2(normal(2), normal(1)) end if call add_pick_hit(hits, n, h) end function pick_ellipsoid !> Add `hit` to hits(1:n) (growing the array). subroutine add_pick_hit(hits, n, hit) type(PickHit), allocatable, intent(inout) :: hits(:) !! grown as needed integer, intent(inout) :: n !! number of hits in hits type(PickHit), intent(in) :: hit !! the hit to add type(PickHit), allocatable :: more(:) if (.not. allocated(hits)) allocate(hits(8)) if (n == size(hits)) then allocate(more(2 * n)) more(1:n) = hits(1:n) call move_alloc(more, hits) end if n = n + 1 hits(n) = hit end subroutine add_pick_hit !> The bounding sphere of what the frame draws, in its own coordinates !> (radius 0 if it draws nothing; extensions override it). Views use it for !> their default distance. subroutine bound(self, center, radius) class(ReferenceFrame), intent(in) :: self real(wp), intent(out) :: center(3), radius !! frame's coordinates center = 0.0_wp radius = 0.0_wp end subroutine bound !> Place the x axis vector: from `base`, `length` long, with a head of !> `head_ratio` of the length (default 0.3) and body and head radii (default !> 0.05 and 0.1 of the length); its label is at the tip, 0.4 of the length !> tall (at most [[label_size]] pixels). As OpenFrames' moveXAxis. Set before !> the frame is first shown. subroutine move_x_axis(self, base, length, head_ratio, body_radius, head_radius) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: base(3), length !! (base: frame's coordinates) real(wp), intent(in), optional :: head_ratio, body_radius, head_radius !! defaults: 0.3; 0.05 and 0.1 of the length call move_axis(self, 1, base, length, head_ratio, body_radius, head_radius) end subroutine move_x_axis !> Place the y axis vector (as [[move_x_axis]]; OpenFrames' moveYAxis). subroutine move_y_axis(self, base, length, head_ratio, body_radius, head_radius) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: base(3), length !! (base: frame's coordinates) real(wp), intent(in), optional :: head_ratio, body_radius, head_radius !! defaults: 0.3; 0.05 and 0.1 of the length call move_axis(self, 2, base, length, head_ratio, body_radius, head_radius) end subroutine move_y_axis !> Place the z axis vector (as [[move_x_axis]]; OpenFrames' moveZAxis). subroutine move_z_axis(self, base, length, head_ratio, body_radius, head_radius) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: base(3), length !! (base: frame's coordinates) real(wp), intent(in), optional :: head_ratio, body_radius, head_radius !! defaults: 0.3; 0.05 and 0.1 of the length call move_axis(self, 3, base, length, head_ratio, body_radius, head_radius) end subroutine move_z_axis !> Place axis k (as [[move_x_axis]]), made again if the frame is shown. subroutine move_axis(self, k, base, length, head_ratio, body_radius, head_radius) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: k !! the axis: 1 (x), 2 (y) or 3 (z) real(wp), intent(in) :: base(3), length !! (base: frame's coordinates) real(wp), intent(in), optional :: head_ratio, body_radius, head_radius !! defaults: 0.3; 0.05 and 0.1 of the length self%axis_base(:, k) = base self%axis_len(k) = length self%axis_head_ratio(k) = 0.3_wp self%axis_body_radius(k) = 0.0_wp self%axis_head_radius(k) = 0.0_wp if (present(head_ratio)) self%axis_head_ratio(k) = head_ratio if (present(body_radius)) self%axis_body_radius(k) = body_radius if (present(head_radius)) self%axis_head_radius(k) = head_radius self%axes_moved = .true. if (self%realized) call make_axis(self, k) ! shown: made again end subroutine move_axis !> Place the axes for what the frame draws, when it is first shown (unless !> move_*_axis was called): at its bound's surface, half the bound's radius !> long (as OpenFrames' Sphere and Model do); at the origin, 1 long, for a !> frame that draws nothing. subroutine reposition_axes(self) class(ReferenceFrame), intent(inout) :: self real(wp) :: c(3), r, e(3) integer :: k call self%bound(c, r) if (r <= 0.0_wp) return do k = 1, 3 e = 0.0_wp e(k) = 1.0_wp self%axis_base(:, k) = c + r * e self%axis_len(k) = 0.5_wp * r end do end subroutine reposition_axes !> What kind of frame this is (shown by [[frame_string]]; extensions override it). function frame_info(self) result(info) class(ReferenceFrame), intent(in) :: self character(len=:), allocatable :: info info = 'ReferenceFrame' end function frame_info ! ================================================================== ! the tree !> Add `child` to this frame's children. Returns false (and does nothing) !> for this frame itself or an ancestor of it; true if it is already a child. !> !> A scene: the Earth, with a spacecraft and the Moon as its children (their !> positions are relative to it): !> !> ```fortran !> earth = Sphere('Earth', radius=6378.137_wp) !> craft = Sphere('Spacecraft', radius=200.0_wp, color=[1.0_wp, 0.3_wp, 0.2_wp]) !> moon = Sphere('Moon', radius=1737.4_wp) !> call moon%set_position([384400.0_wp, 0.0_wp, 0.0_wp]) !> if (.not. (earth%add_child(craft) .and. earth%add_child(moon))) error stop 'add_child failed' !> scene = FrameManager(earth) !> ``` logical function add_child(self, child) result(ok) class(ReferenceFrame), intent(inout), target :: self class(ReferenceFrame), intent(inout), target :: child !! the frame to add (not owned) ok = .false. if (same_frame(self, child)) return if (self%child_index(child) > 0) then ok = .true. return end if if (is_descendant(child, self)) return call append(self%children, self%n_children, child) call append(child%parents, child%n_parents, self) if (child%n_parents == 1) child%stamp_ = next_motion_stamp() ! (its first parent: where it is) ok = .true. if (self%realized) then ! shown: draw the child from the next frame if (.not. child%realized) then if (self%shown_) then ! (for a window that exists now) call child%realize(vkf_viewer_any()) call check_status(vkf_compile_node(child%node_)) else call child%realize(c_null_ptr) end if end if call check_status(vkf_node_add_child(self%node_, child%node_)) end if end function add_child !> Remove `child` from this frame's children. Returns false if it is not one. logical function remove_child(self, child) result(ok) class(ReferenceFrame), intent(inout), target :: self class(ReferenceFrame), intent(inout), target :: child !! the child to remove ok = remove(self%children, self%n_children, child) if (ok) ok = remove(child%parents, child%n_parents, self) if (ok) child%stamp_ = next_motion_stamp() ! (its first parent may have changed) if (ok .and. self%realized .and. child%realized) call check_status(vkf_node_remove_child(self%node_, child%node_)) end function remove_child !> Remove this frame from all its parents and children (before it goes out !> of scope or is deallocated while in a tree). subroutine detach(self) class(ReferenceFrame), intent(inout), target :: self logical :: ok do while (self%n_parents > 0) ok = self%parents(self%n_parents)%p%remove_child(self) end do do while (self%n_children > 0) ok = self%remove_child(self%children(self%n_children)%p) end do end subroutine detach !> Number of children. pure integer function num_children(self) class(ReferenceFrame), intent(in) :: self num_children = self%n_children end function num_children !> Child i (1 to num_children). function child(self, i) result(c) class(ReferenceFrame), intent(in) :: self integer, intent(in) :: i !! 1 to num_children class(ReferenceFrame), pointer :: c c => self%children(i)%p end function child !> Index of `frame` among the children, or 0. integer function child_index(self, frame) class(ReferenceFrame), intent(in) :: self class(ReferenceFrame), intent(in), target :: frame !! the frame to find child_index = find(self%children, self%n_children, frame) end function child_index !> Number of parents. pure integer function num_parents(self) class(ReferenceFrame), intent(in) :: self num_parents = self%n_parents end function num_parents !> Parent i (1 to num_parents). function parent(self, i) result(p) class(ReferenceFrame), intent(in) :: self integer, intent(in) :: i !! 1 to num_parents class(ReferenceFrame), pointer :: p p => self%parents(i)%p end function parent !> Index of `frame` among the parents, or 0. integer function parent_index(self, frame) class(ReferenceFrame), intent(in) :: self class(ReferenceFrame), intent(in), target :: frame !! the frame to find parent_index = find(self%parents, self%n_parents, frame) end function parent_index !> The tree below this frame, one line per frame ("name (info)"), in !> OpenFrames' createFrameString format: children are prefixed "|-->". recursive function frame_string(self, prefix) result(str) class(ReferenceFrame), intent(in) :: self character(len=*), intent(in), optional :: prefix !! before this frame's line (default ' ') character(len=:), allocatable :: str, pre integer :: k pre = ' ' if (present(prefix)) pre = prefix str = pre//self%name()//' ('//self%frame_info()//')'//new_line('a') if (pre(len(pre):len(pre)) == '>') pre(len(pre):len(pre)) = '-' pre = pre//'|-->' do k = 1, self%n_children str = str//self%children(k)%p%frame_string(pre) end do end function frame_string ! ================================================================== ! following trajectories !> Follow `traj` (in addition to trajectories already followed, if `add` !> is true), with what to follow (`data`: FOLLOW_POSITION and/or !> FOLLOW_ATTITUDE; default both) and how (`mode`: FOLLOW_LOOP, the !> default, or FOLLOW_LIMIT). `traj` must stay alive while followed. !> !> The followed data are in the parent's coordinates, unless `data_frame` !> (a frame of the same tree) says which frame they are in: e.g. a !> spacecraft's inertial trajectory followed by a frame shown in a !> rotating frame, placed (and turned) by both frames' poses at each time !> (see [[world_matrix_at]]), so that it rides on the trajectory drawn in !> that frame (DrawableTrajectory's set_data_frame). `data_frame` must not !> depend on this frame (be it or one of its descendants). !> !> ```fortran !> call craft%follow(orbit) ! position (and attitude, if any), looping !> call craft%follow(orbit, data=FOLLOW_POSITION, mode=FOLLOW_LIMIT) ! position only, held at the ends !> call craft%follow(flyby, data_frame=eci) ! inertial data, the craft under a rotating frame !> ``` subroutine follow(self, traj, data, mode, add, data_frame) class(ReferenceFrame), intent(inout) :: self type(Trajectory), intent(in), target :: traj !! the trajectory to follow (not owned) integer, intent(in), optional :: data, mode !! FOLLOW_POSITION/FOLLOW_ATTITUDE; FOLLOW_LOOP/FOLLOW_LIMIT logical, intent(in), optional :: add !! (true: in addition to those followed (default false)) class(ReferenceFrame), intent(in), target, optional :: data_frame !! the frame the data are in (default: the parent) type(Trajectory), pointer :: p integer :: d, m logical :: adding p => traj adding = .false. if (present(add)) adding = add d = ior(FOLLOW_POSITION, FOLLOW_ATTITUDE) m = FOLLOW_LOOP if (present(data)) d = data if (present(mode)) m = mode if (adding) then call self%follower_%add_trajectory(p) else call self%follower_%set_trajectory(p) end if call self%follower_%set_follow_type(d, m) self%following = .true. self%follow_frame_ => null() if (present(data_frame)) self%follow_frame_ => data_frame self%stamp_ = next_motion_stamp() end subroutine follow !> Stop following trajectories (the frame keeps its last position and attitude). subroutine unfollow(self) class(ReferenceFrame), intent(inout) :: self call self%follower_%remove_all() self%following = .false. self%follow_frame_ => null() self%stamp_ = next_motion_stamp() end subroutine unfollow !> The frame the followed data are in ([[follow]]'s `data_frame`; null: !> the parent). function follow_frame(self) result(f) class(ReferenceFrame), intent(in) :: self class(ReferenceFrame), pointer :: f f => self%follow_frame_ end function follow_frame !> The followed position and attitude at time t, in the parent's !> coordinates (from the data frame's, if [[follow]] had one, by both !> frames' poses at time t); `pos` and `att` unchanged where nothing is !> followed. recursive subroutine followed_pose(self, t, pos, att) class(ReferenceFrame), intent(in) :: self real(wp), intent(in) :: t !! simulation time real(wp), intent(inout) :: pos(3), att(4) !! the pose (in the parent's coordinates) real(wp) :: m(4,4), p(3), q(4) logical :: has_position, has_attitude p = pos q = att call self%follower_%state_at(t, p, q, has_position, has_attitude) if (.not. (has_position .or. has_attitude)) return if (associated(self%follow_frame_)) then ! the data frame's coordinates to the parent's, at t m = self%follow_frame_%world_matrix_at(t) if (self%n_parents > 0) m = matmul(rigid_inverse(self%parents(1)%p%world_matrix_at(t)), m) if (has_position) p = matmul(m(1:3, 1:3), p) + m(1:3, 4) if (has_attitude) q = quat_normalize(quat_multiply(quat_from_matrix(m(1:3, 1:3)), q)) end if if (has_position) pos = p if (has_attitude) att = q end subroutine followed_pose !> The frame's trajectory follower, for finer control (data sources, time !> offset, ...). function follower(self) result(f) class(ReferenceFrame), intent(in), target :: self type(TrajectoryFollower), pointer :: f f => self%follower_ end function follower !> Whether the frame follows trajectories (at least one, since [[follow]]). pure logical function is_following(self) class(ReferenceFrame), intent(in) :: self is_following = self%following .and. self%follower_%num_trajectories() > 0 end function is_following !> Update this frame and its descendants for simulation time `t` (followed !> trajectories set positions and attitudes). recursive subroutine update(self, t) class(ReferenceFrame), intent(inout), target :: self real(wp), intent(in) :: t !! simulation time logical :: has_position, has_attitude real(wp) :: pos(3), att(4) integer :: k if (self%following) then if (associated(self%follow_frame_)) then ! (the data in another frame's coordinates) pos = self%position_ att = self%attitude_ call self%follower_%update(t, pos, att, has_position, has_attitude) ! (its time and trajectory) call followed_pose(self, t, self%position_, self%attitude_) else call self%follower_%update(t, self%position_, self%attitude_, has_position, has_attitude) end if end if do k = 1, self%n_children call self%children(k)%p%update(t) end do end subroutine update !> After the whole tree's update for time t (all frames at their positions !> for t): for contents that depend on other frames' positions, e.g. !> CustomLineSegments between spacecraft. Here, only the descendants'. recursive subroutine late_update(self, t) class(ReferenceFrame), intent(inout) :: self real(wp), intent(in) :: t !! simulation time integer :: k do k = 1, self%n_children call self%children(k)%p%late_update(t) end do end subroutine late_update ! ================================================================== ! rendering !> Create the render nodes of this frame and its descendants (once; for !> `viewer`, a window's viewer handle, which some contents need while they !> are built (not kept); null: headless). recursive subroutine realize(self, viewer) class(ReferenceFrame), intent(inout) :: self type(c_ptr), intent(in) :: viewer !! a window's viewer handle (null: headless) integer :: k if (self%realized) return self%realized = .true. self%shown_ = c_associated(viewer) self%node_ = vkf_transform_create() self%synced = .false. self%contents_node = vkf_switch_create() call check_status(vkf_node_add_child(self%node_, self%contents_node)) call self%build_contents(viewer, self%contents_node) call build_decorations(self) if (self%light_source) call build_light(self) if (.not. self%contents) call check_status(vkf_switch_set(self%contents_node, 0_c_int)) call self%sync() do k = 1, self%n_children call self%children(k)%p%realize(viewer) call check_status(vkf_node_add_child(self%node_, self%children(k)%p%node_)) end do end subroutine realize !> Add what this frame draws to `group` (a node handle). ReferenceFrame !> draws nothing itself (its axes and labels come later); extensions such as !> Sphere and Model override this. subroutine build_contents(self, viewer, group) class(ReferenceFrame), intent(inout) :: self type(c_ptr), intent(in) :: viewer, group !! (viewer: as [[realize]]'s) end subroutine build_contents !> Copy the positions and attitudes of this frame and its descendants to !> their transform nodes (only those that changed since the last sync). recursive subroutine sync(self) class(ReferenceFrame), intent(inout) :: self integer :: k if (.not. self%realized) return if (.not. (self%synced .and. all(self%position_ == self%synced_position) .and. & all(self%attitude_ == self%synced_attitude))) then call check_status(vkf_transform_set_matrix(self%node_, self%local_matrix())) self%synced_position = self%position_ self%synced_attitude = self%attitude_ self%synced = .true. end if do k = 1, self%n_children call self%children(k)%p%sync() end do end subroutine sync !> The frame's transform node (null until realized). function node(self) result(n) class(ReferenceFrame), intent(in) :: self type(c_ptr) :: n n = self%node_ end function node !> The axes (arrows), axes labels and name label, each under a switch, in !> the frame's color (OpenFrames' ReferenceFrame decorations). subroutine build_decorations(self) class(ReferenceFrame), intent(inout) :: self integer :: k if (.not. self%axes_moved) call self%reposition_axes() do k = 1, 3 self%axis_node(k) = vkf_switch_create() self%axis_label_node(k) = vkf_switch_create() call make_axis(self, k) call check_status(vkf_node_add_child(self%node_, self%axis_node(k))) call check_status(vkf_node_add_child(self%node_, self%axis_label_node(k))) end do ! the name label at the origin, label_size pixels tall self%name_label_node = vkf_switch_create() call make_name_label(self) call check_status(vkf_node_add_child(self%node_, self%name_label_node)) call update_switches(self) end subroutine build_decorations !> The name label (in place of the old one; compiled first if shown). subroutine make_name_label(self) class(ReferenceFrame), intent(inout) :: self call replace_child(self, self%name_label_node, self%name_text, & label_create(self%name(), [0.0_wp, 0.0_wp, 0.0_wp], self%color_, & real(self%label_size_, wp), 0.0_wp, self%label_alignment_, self%label_font())) if (self%name_on_top .and. c_associated(self%name_text)) call check_status(vkf_label_set_on_top(self%name_text, 1_c_int)) call update_switches(self) ! (a node added to a switch is shown) end subroutine make_name_label !> The axes and their labels made again (another color, size or font). subroutine remake_axes(self) class(ReferenceFrame), intent(inout) :: self integer :: k if (.not. self%realized) return do k = 1, 3 call make_axis(self, k) end do end subroutine remake_axes !> The labels' size and alignment changed: the name label's in place, the !> axes' labels made again. subroutine relayout_labels(self) class(ReferenceFrame), intent(inout) :: self if (.not. self%realized) return if (c_associated(self%name_text)) then if (.not. label_set_layout(self%name_text, [0.0_wp, 0.0_wp, 0.0_wp], real(self%label_size_, wp), 0.0_wp, & self%label_alignment_)) call check_status(-1_c_int) end if call remake_axes(self) end subroutine relayout_labels !> Axis k's arrow and label, in their switches (in place of the old ones; !> compiled first if the window already runs). subroutine make_axis(self, k) class(ReferenceFrame), intent(inout) :: self integer, intent(in) :: k !! the axis: 1 (x), 2 (y) or 3 (z) real(wp) :: e(3), len, head, rb, rh, tip(3) e = 0.0_wp e(k) = 1.0_wp len = self%axis_len(k) head = self%axis_head_ratio(k) if (head <= 0.0_wp .or. head >= 1.0_wp) head = 0.3_wp rb = self%axis_body_radius(k) if (rb <= 0.0_wp) rb = 0.05_wp * len rh = self%axis_head_radius(k) if (rh <= 0.0_wp) rh = 0.1_wp * len call replace_child(self, self%axis_node(k), self%axis_arrow(k), & arrow_create(self%axis_base(:, k), e, (1.0_wp - head) * len, head * len, rb, rh, & real(self%color_, c_float))) ! the label at the tip, 0.4 of the axis length tall, at most label_size pixels tip = self%axis_base(:, k) + len * e call replace_child(self, self%axis_label_node(k), self%axis_text(k), & label_create(self%axis_label(2**(k-1)), tip, self%color_, -0.4_wp * len, & real(self%label_size_, wp), self%label_alignment_, self%label_font())) call update_switches(self) ! (a node added to a switch is shown) end subroutine make_axis !> Put `new` in `switch` in place of `old` (a kept handle, deleted). subroutine replace_child(self, switch, old, new) class(ReferenceFrame), intent(in) :: self type(c_ptr), intent(in) :: switch, new !! the switch; the new node (may be null) type(c_ptr), intent(inout) :: old !! the node replaced (deleted; set to new) if (c_associated(old)) then call check_status(vkf_node_remove_child(switch, old)) call vkf_node_delete(old) end if old = new if (.not. c_associated(new)) return if (self%shown_) call check_status(vkf_compile_node(new)) call check_status(vkf_node_add_child(switch, new)) end subroutine replace_child !> Show or hide the decorations' switches as set. subroutine update_switches(self) class(ReferenceFrame), intent(inout) :: self integer :: k if (.not. self%realized) return do k = 1, 3 if (c_associated(self%axis_node(k))) & call check_status(vkf_switch_set(self%axis_node(k), merge(1_c_int, 0_c_int, iand(self%axes, 2**(k-1)) /= 0))) if (c_associated(self%axis_label_node(k))) & call check_status(vkf_switch_set(self%axis_label_node(k), & merge(1_c_int, 0_c_int, iand(self%axes_labels, 2**(k-1)) /= 0))) end do if (c_associated(self%name_label_node)) & call check_status(vkf_switch_set(self%name_label_node, merge(1_c_int, 0_c_int, self%name_label))) end subroutine update_switches ! ================================================================== ! helpers !> True if `a` and `b` are the same frame (object). logical function same_frame(a, b) class(ReferenceFrame), intent(in), target :: a, b !! the frames compared class(ReferenceFrame), pointer :: pa pa => a same_frame = associated(pa, b) end function same_frame !> True if `frame` is `root` or one of its descendants. recursive logical function is_descendant(root, frame) result(yes) class(ReferenceFrame), intent(in), target :: root, frame !! the subtree's root; the frame looked for integer :: k yes = same_frame(root, frame) do k = 1, root%n_children if (yes) return yes = is_descendant(root%children(k)%p, frame) end do end function is_descendant !> Index of `frame` in list(1:n), or 0. integer function find(list, n, frame) type(FramePointer), allocatable, intent(in) :: list(:) !! first n used integer, intent(in) :: n !! number of frames in list class(ReferenceFrame), intent(in), target :: frame !! the frame to find integer :: k find = 0 do k = 1, n if (associated(list(k)%p, frame)) then find = k return end if end do end function find !> Add `frame` to list(1:n) (growing the array). subroutine append(list, n, frame) type(FramePointer), allocatable, intent(inout) :: list(:) !! first n used (grown as needed) integer, intent(inout) :: n !! number of frames in list (incremented) class(ReferenceFrame), intent(in), target :: frame !! the frame to add (not owned) type(FramePointer), allocatable :: tmp(:) if (.not. allocated(list)) allocate(list(4)) if (n == size(list)) then allocate(tmp(2 * n)) tmp(1:n) = list(1:n) call move_alloc(tmp, list) end if n = n + 1 list(n)%p => frame end subroutine append !> Remove `frame` from list(1:n); false if it is not in it. logical function remove(list, n, frame) type(FramePointer), allocatable, intent(inout) :: list(:) !! first n used integer, intent(inout) :: n !! number of frames in list (decremented if removed) class(ReferenceFrame), intent(in), target :: frame !! the frame to remove integer :: k k = find(list, n, frame) remove = k > 0 if (.not. remove) return list(k:n-1) = list(k+1:n) list(n)%p => null() n = n - 1 end function remove end module vkf_frames