fortplot_raster_line_styles.f90 Source File


Source Code

module fortplot_raster_line_styles
    !! Module for handling line styling in raster images
    use iso_c_binding
    use fortplot_raster_drawing, only: draw_line_distance_aa
    use fortplot_line_styles, only: get_line_pattern, get_pattern_length
    use, intrinsic :: iso_fortran_env, only: wp => real64
    implicit none

    private
    public :: draw_styled_line, set_raster_line_style, reset_pattern_distance
    public :: PATTERN_SCALE_FACTOR

    ! Named constant to replace magic number
    ! Pattern scaling factor controls how many pattern units correspond
    ! to one pixel of geometric distance. Use a non-integer factor to
    ! avoid aliasing where fixed 2px segments never enter gap regions.
    ! Scale of pattern units to pixels. Using 1.0 aligns
    ! pattern units with pixel distances for intuitive lengths
    real(wp), parameter :: PATTERN_SCALE_FACTOR = 1.0_wp

contains

    subroutine draw_styled_line(image_data, img_w, img_h, px1, py1, px2, py2, &
                               r, g, b, line_width, line_style, line_pattern, &
                               pattern_size, pattern_length, pattern_distance)
        !! Draw line with pattern support (dashed, dotted, etc.)
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: img_w, img_h
        real(wp), intent(in) :: px1, py1, px2, py2
        real(wp), intent(in) :: r, g, b, line_width
        character(len=*), intent(in) :: line_style
        real(wp), contiguous, intent(in) :: line_pattern(:)
        integer, intent(in) :: pattern_size
        real(wp), intent(in) :: pattern_length
        real(wp), intent(inout) :: pattern_distance
        
        real(wp) :: dx, dy, line_length
        real(wp) :: unit_x, unit_y
        real(wp) :: pos, phase, seg_end, draw_start, draw_end
        real(wp) :: cap_inset, sx, sy, ex, ey
        integer :: phase_index
        logical :: in_draw

        ! Calculate line geometry
        dx = px2 - px1
        dy = py2 - py1
        line_length = sqrt(dx * dx + dy * dy)

        ! Handle degenerate case
        if (line_length < 1e-6_wp) return

        ! Unit direction vector
        unit_x = dx / line_length
        unit_y = dy / line_length

        ! For solid lines, draw the whole line at once
        if (trim(line_style) == '-' .or. trim(line_style) == 'solid') then
            call draw_line_distance_aa(image_data, img_w, img_h, &
                                      px1, py1, px2, py2, &
                                      r, g, b, line_width)
            return
        end if

        ! Degenerate pattern -> draw solid to avoid an invisible line.
        if (pattern_size <= 0 .or. pattern_length <= 0.0_wp) then
            call draw_line_distance_aa(image_data, img_w, img_h, &
                                      px1, py1, px2, py2, &
                                      r, g, b, line_width)
            pattern_distance = pattern_distance + line_length * PATTERN_SCALE_FACTOR
            return
        end if

        ! Walk the dash pattern in exact device units (no per-pixel
        ! quantization). For each "on" interval, draw the precise sub-segment.
        ! The distance-AA primitive adds a rounded cap of ~half the line width
        ! beyond each endpoint; inset every drawn interval by that amount so
        ! short dots stay round and dashes keep matplotlib-like gaps instead of
        ! bleeding into the off intervals.
        cap_inset = line_width * 0.5_wp
        pos = 0.0_wp
        do while (pos < line_length - 1e-9_wp)
            ! Locate the current phase for the global pattern distance at pos.
            call locate_phase(pattern_distance + pos * PATTERN_SCALE_FACTOR, &
                              line_pattern, pattern_size, pattern_length, &
                              phase_index, phase)
            in_draw = (mod(phase_index, 2) == 1)

            ! Distance (device units) to the end of this phase along the line.
            seg_end = pos + (line_pattern(phase_index) - phase) / PATTERN_SCALE_FACTOR
            if (seg_end > line_length) seg_end = line_length
            if (seg_end <= pos) seg_end = pos + 1e-6_wp

            if (in_draw) then
                draw_start = pos
                draw_end = seg_end
                ! Inset for round-cap compensation, but never invert the
                ! interval; very short "on" phases collapse to a single dot.
                if (draw_end - draw_start > 2.0_wp * cap_inset) then
                    draw_start = draw_start + cap_inset
                    draw_end = draw_end - cap_inset
                else
                    draw_start = 0.5_wp * (pos + seg_end)
                    draw_end = draw_start
                end if
                sx = px1 + draw_start * unit_x
                sy = py1 + draw_start * unit_y
                ex = px1 + draw_end * unit_x
                ey = py1 + draw_end * unit_y
                call draw_line_distance_aa(image_data, img_w, img_h, &
                                          sx, sy, ex, ey, &
                                          r, g, b, line_width)
            end if

            pos = seg_end
        end do

        pattern_distance = pattern_distance + line_length * PATTERN_SCALE_FACTOR

    end subroutine draw_styled_line

    subroutine locate_phase(distance, pattern, pattern_size, pattern_length, &
                            phase_index, offset_in_phase)
        !! Map a cumulative pattern distance to its on/off phase. Returns the
        !! 1-based phase index (odd = draw, even = gap) and how far into that
        !! phase the distance falls.
        real(wp), intent(in) :: distance, pattern_length
        real(wp), contiguous, intent(in) :: pattern(:)
        integer, intent(in) :: pattern_size
        integer, intent(out) :: phase_index
        real(wp), intent(out) :: offset_in_phase

        real(wp) :: pos_in_cycle, cumulative
        integer :: i

        pos_in_cycle = mod(distance, pattern_length)
        if (pos_in_cycle < 0.0_wp) pos_in_cycle = pos_in_cycle + pattern_length

        cumulative = 0.0_wp
        do i = 1, pattern_size
            if (pos_in_cycle < cumulative + pattern(i)) then
                phase_index = i
                offset_in_phase = pos_in_cycle - cumulative
                return
            end if
            cumulative = cumulative + pattern(i)
        end do

        ! Numerical edge at the very end of the cycle: report last phase.
        phase_index = pattern_size
        offset_in_phase = pattern(pattern_size)

    end subroutine locate_phase

    subroutine set_raster_line_style(style, line_style, line_pattern, pattern_size, &
                                    pattern_length, pattern_distance)
        !! Set line style pattern for raster
        character(len=*), intent(in) :: style
        character(len=*), intent(out) :: line_style
        real(wp), intent(out) :: line_pattern(:)
        integer, intent(out) :: pattern_size
        real(wp), intent(out) :: pattern_length
        real(wp), intent(out) :: pattern_distance
        
        line_style = trim(style)
        call get_line_pattern(style, line_pattern, pattern_size)
        pattern_length = get_pattern_length(line_pattern, pattern_size)
        pattern_distance = 0.0_wp  ! Reset pattern distance
    end subroutine set_raster_line_style

    subroutine reset_pattern_distance(pattern_distance)
        !! Reset pattern distance to start of pattern
        real(wp), intent(out) :: pattern_distance
        pattern_distance = 0.0_wp
    end subroutine reset_pattern_distance

end module fortplot_raster_line_styles