fortplot_line_styles.f90 Source File


Source Code

module fortplot_line_styles
    !! Line pattern and style rendering algorithms
    !! 
    !! This module contains line style pattern rendering and segment
    !! drawing algorithms following the Single Responsibility Principle.
    !! 
    !! SOLID: Single responsibility for line style implementation
    
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_constants, only: SOLID_LINE_PATTERN_LENGTH, &
                                  DOTTED_PATTERN_PT, &
                                  DASHED_PATTERN_PT, &
                                  DASHDOT_PATTERN_PT
    implicit none

    private
    public :: get_line_pattern, get_pattern_length, scale_pattern_to_pixels
    public :: should_draw_at_distance, advance_pattern_state

contains

    subroutine get_line_pattern(linestyle, pattern, pattern_size)
        !! Get line pattern array for a given style, in points.
        !! Single source of truth: the matplotlib default dash sequences.
        !! On/off lengths are in points; callers scale by line width (points)
        !! and convert to device units (see scale_pattern_to_pixels).
        character(len=*), intent(in) :: linestyle
        real(wp), intent(out) :: pattern(20)
        integer, intent(out) :: pattern_size

        select case (trim(linestyle))
        case ('-', 'solid')
            pattern(1) = SOLID_LINE_PATTERN_LENGTH  ! Very long solid segment
            pattern_size = 1

        case ('--', 'dashed')
            pattern(1:2) = DASHED_PATTERN_PT
            pattern_size = 2

        case (':', 'dotted')
            pattern(1:2) = DOTTED_PATTERN_PT
            pattern_size = 2

        case ('-.', 'dashdot')
            pattern(1:4) = DASHDOT_PATTERN_PT
            pattern_size = 4

        case default
            pattern(1) = SOLID_LINE_PATTERN_LENGTH  ! Default to solid
            pattern_size = 1
        end select

    end subroutine get_line_pattern

    subroutine scale_pattern_to_pixels(pattern, pattern_size, dpi, &
                                       line_width_pt)
        !! Scale a point-based pattern in place to device pixels, matching
        !! matplotlib: multiply each on/off length (points) by the line width
        !! (points), then convert points to pixels via px = pt * dpi / 72.
        !! The solid sentinel is left untouched.
        real(wp), intent(inout) :: pattern(20)
        integer, intent(in) :: pattern_size
        real(wp), intent(in) :: dpi, line_width_pt

        real(wp) :: factor

        if (pattern_size <= 1) return  ! Solid: keep sentinel length
        factor = line_width_pt * dpi / 72.0_wp
        pattern(1:pattern_size) = pattern(1:pattern_size) * factor

    end subroutine scale_pattern_to_pixels

    real(wp) function get_pattern_length(pattern, pattern_size)
        !! Calculate total length of pattern cycle
        !! Following KISS principle - simple sum calculation
        real(wp), intent(in) :: pattern(20)
        integer, intent(in) :: pattern_size
        
        integer :: i
        
        get_pattern_length = 0.0_wp
        do i = 1, pattern_size
            get_pattern_length = get_pattern_length + pattern(i)
        end do
        
    end function get_pattern_length

    logical function should_draw_at_distance(distance, pattern, pattern_size, pattern_length)
        !! Determine if line should be drawn at given distance in pattern
        !! Following SRP - handles only pattern position logic
        real(wp), intent(in) :: distance, pattern_length
        real(wp), intent(in) :: pattern(20)
        integer, intent(in) :: pattern_size
        
        real(wp) :: pos_in_cycle, cumulative
        integer :: i
        
        ! Handle edge cases
        if (pattern_size <= 0 .or. pattern_length <= 0.0_wp) then
            should_draw_at_distance = .true.
            return
        end if
        
        ! Find position within pattern cycle
        pos_in_cycle = mod(distance, pattern_length)
        
        ! Determine if we're in a draw or gap segment
        cumulative = 0.0_wp
        do i = 1, pattern_size
            cumulative = cumulative + pattern(i)
            if (pos_in_cycle <= cumulative) then
                ! Odd indices (1,3,5...) are draw segments
                ! Even indices (2,4,6...) are gap segments
                should_draw_at_distance = (mod(i, 2) == 1)
                return
            end if
        end do
        
        ! Fallback
        should_draw_at_distance = .true.
        
    end function should_draw_at_distance

    subroutine advance_pattern_state(current_distance, segment_length, pattern_length, new_distance)
        !! Advance pattern state for continuous rendering
        !! Following SRP - handles only state advancement
        real(wp), intent(in) :: current_distance, segment_length, pattern_length
        real(wp), intent(out) :: new_distance
        
        new_distance = current_distance + segment_length
        
        ! Keep distance within reasonable bounds to avoid overflow
        if (pattern_length > 0.0_wp .and. new_distance > SOLID_LINE_PATTERN_LENGTH * pattern_length) then
            new_distance = mod(new_distance, pattern_length)
        end if
        
    end subroutine advance_pattern_state

end module fortplot_line_styles