module fortplot_raster_rendering !! Specialized rendering functionality for raster backend !! Extracted from fortplot_raster.f90 for size reduction (SRP compliance) use fortplot_constants, only: EPSILON_COMPARE use fortplot_raster_core, only: raster_image_t use fortplot_margins, only: plot_area_t use fortplot_colormap, only: colormap_value_to_color use fortplot_interpolation, only: interpolate_z_bilinear use fortplot_raster_primitives, only: color_to_byte, draw_filled_quad_raster use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none private public :: raster_fill_heatmap, raster_fill_quad, fill_triangle, fill_horizontal_line public :: raster_render_legend_specialized, raster_calculate_legend_dimensions public :: raster_set_legend_border_width, raster_calculate_legend_position public :: raster_extract_rgb_data, raster_get_png_data, raster_prepare_3d_data contains subroutine raster_fill_heatmap(raster, width, height, plot_area, x_min, x_max, y_min, y_max, & x_grid, y_grid, z_grid, z_min, z_max) !! Fill contour plot using scanline method for pixel-by-pixel rendering type(raster_image_t), intent(inout) :: raster integer, intent(in) :: width, height type(plot_area_t), intent(in) :: plot_area real(wp), intent(in) :: x_min, x_max, y_min, y_max real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:) real(wp), intent(in) :: z_min, z_max integer :: nx, ny real(wp) :: x_min_grid, x_max_grid, y_min_grid, y_max_grid nx = size(x_grid) ny = size(y_grid) ! Validate input dimensions and data bounds if (size(z_grid, 1) /= ny .or. size(z_grid, 2) /= nx) return if (abs(z_max - z_min) < EPSILON_COMPARE) return ! Get data bounds x_min_grid = minval(x_grid) x_max_grid = maxval(x_grid) y_min_grid = minval(y_grid) y_max_grid = maxval(y_grid) ! Render pixels using scanline method call raster_render_heatmap_pixels(raster, width, height, plot_area, & x_min, x_max, y_min, y_max, & x_grid, y_grid, z_grid, & x_min_grid, x_max_grid, y_min_grid, y_max_grid, z_min, z_max) end subroutine raster_fill_heatmap subroutine raster_render_heatmap_pixels(raster, width, height, plot_area, & x_min, x_max, y_min, y_max, & x_grid, y_grid, z_grid, & x_min_grid, x_max_grid, y_min_grid, y_max_grid, z_min, z_max) !! Render heatmap pixels using pixel-by-pixel scanline approach type(raster_image_t), intent(inout) :: raster integer, intent(in) :: width, height type(plot_area_t), intent(in) :: plot_area real(wp), intent(in) :: x_min, x_max, y_min, y_max real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:,:) real(wp), intent(in) :: x_min_grid, x_max_grid, y_min_grid, y_max_grid, z_min, z_max integer :: px, py real(wp) :: world_x, world_y, z_value real(wp) :: color_rgb(3) integer(1) :: r_byte, g_byte, b_byte integer :: offset associate(dxming=>x_min_grid, dxmaxg=>x_max_grid, dyming=>y_min_grid, dymaxg=>y_max_grid); end associate ! Scanline rendering: iterate over all pixels in plot area do py = plot_area%bottom, plot_area%bottom + plot_area%height - 1 do px = plot_area%left, plot_area%left + plot_area%width - 1 ! Map pixel to world coordinates world_x = x_min + (real(px - plot_area%left, wp) / & real(plot_area%width - 1, wp)) * (x_max - x_min) world_y = y_max - (real(py - plot_area%bottom, wp) / & real(plot_area%height - 1, wp)) * (y_max - y_min) ! Interpolate Z value and convert to color call interpolate_z_bilinear(x_grid, y_grid, z_grid, world_x, world_y, z_value) call colormap_value_to_color(z_value, z_min, z_max, 'viridis', color_rgb) ! Convert to bytes and set pixel r_byte = color_to_byte(color_rgb(1)) g_byte = color_to_byte(color_rgb(2)) b_byte = color_to_byte(color_rgb(3)) ! Set pixel directly in image data (RGB format) if (px >= 1 .and. px <= width .and. py >= 1 .and. py <= height) then offset = 3 * ((py - 1) * width + (px - 1)) + 1 if (offset >= 1 .and. offset + 2 <= size(raster%image_data)) then raster%image_data(offset) = r_byte raster%image_data(offset + 1) = g_byte raster%image_data(offset + 2) = b_byte end if end if end do end do end subroutine raster_render_heatmap_pixels subroutine raster_fill_quad(raster, width, height, plot_area, x_min, x_max, y_min, y_max, & x_quad, y_quad) !! Fill quadrilateral with current color type(raster_image_t), intent(inout) :: raster integer, intent(in) :: width, height type(plot_area_t), intent(in) :: plot_area real(wp), intent(in) :: x_min, x_max, y_min, y_max real(wp), intent(in) :: x_quad(4), y_quad(4) real(wp) :: px_quad(4), py_quad(4) integer :: i ! Transform data coordinates to pixel coordinates (same as line drawing) ! This ensures the quad respects plot area margins do i = 1, 4 px_quad(i) = (x_quad(i) - x_min) / (x_max - x_min) * real(plot_area%width, wp) + & real(plot_area%left, wp) py_quad(i) = real(plot_area%bottom + plot_area%height, wp) - & (y_quad(i) - y_min) / (y_max - y_min) * real(plot_area%height, wp) end do call draw_filled_quad_raster(raster%image_data, width, height, & px_quad, py_quad, & raster%current_r, raster%current_g, raster%current_b) end subroutine raster_fill_quad subroutine fill_triangle(image_data, img_w, img_h, x1, y1, x2, y2, x3, y3, r, g, b) !! Fill triangle using barycentric coordinates integer(1), intent(inout) :: image_data(*) integer, intent(in) :: img_w, img_h real(wp), intent(in) :: x1, y1, x2, y2, x3, y3 integer(1), intent(in) :: r, g, b integer :: x, y, x_min, x_max, y_min, y_max real(wp) :: denom, a, b_coord, c integer :: pixel_index ! Find bounding box x_min = max(1, int(min(min(x1, x2), x3))) x_max = min(img_w, int(max(max(x1, x2), x3)) + 1) y_min = max(1, int(min(min(y1, y2), y3))) y_max = min(img_h, int(max(max(y1, y2), y3)) + 1) ! Precompute denominator for barycentric coordinates denom = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) if (abs(denom) < EPSILON_COMPARE) return ! Degenerate triangle ! Check each pixel in bounding box do y = y_min, y_max do x = x_min, x_max ! Compute barycentric coordinates a = ((y2 - y3) * (real(x, wp) - x3) + (x3 - x2) * (real(y, wp) - y3)) / denom b_coord = ((y3 - y1) * (real(x, wp) - x3) + (x1 - x3) * (real(y, wp) - y3)) / denom c = 1.0_wp - a - b_coord ! Check if point is inside triangle if (a >= 0.0_wp .and. b_coord >= 0.0_wp .and. c >= 0.0_wp) then pixel_index = 3 * ((y - 1) * img_w + (x - 1)) + 1 image_data(pixel_index) = r ! Red image_data(pixel_index + 1) = g ! Green image_data(pixel_index + 2) = b ! Blue end if end do end do end subroutine fill_triangle subroutine fill_horizontal_line(image_data, img_w, img_h, x1, x2, y, r, g, b) !! Fill horizontal line segment integer(1), intent(inout) :: image_data(*) integer, intent(in) :: img_w, img_h, x1, x2, y integer(1), intent(in) :: r, g, b integer :: x, x_start, x_end, pixel_index x_start = max(1, min(x1, x2)) x_end = min(img_w, max(x1, x2)) if (y >= 1 .and. y <= img_h) then do x = x_start, x_end pixel_index = 3 * ((y - 1) * img_w + (x - 1)) + 1 image_data(pixel_index) = r ! Red image_data(pixel_index + 1) = g ! Green image_data(pixel_index + 2) = b ! Blue end do end if end subroutine fill_horizontal_line subroutine raster_render_legend_specialized(legend, legend_x, legend_y) !! Render legend using standard algorithm for PNG use fortplot_legend, only: legend_t type(legend_t), intent(in) :: legend real(wp), intent(in) :: legend_x, legend_y associate(dlg=>legend%num_entries, dx=>legend_x, dy=>legend_y); end associate ! No-op: legend rendering handled by fortplot_legend module ! This method exists only for polymorphic compatibility end subroutine raster_render_legend_specialized subroutine raster_calculate_legend_dimensions(legend, legend_width, legend_height) !! Calculate legend dimensions for PNG using real text metrics (pixels) use fortplot_legend, only: legend_t use fortplot_text, only: calculate_text_width, calculate_text_height type(legend_t), intent(in) :: legend real(wp), intent(out) :: legend_width, legend_height integer :: i, max_label_w, label_h, padding_x, line_len, text_gap, pad_y, entry_gap if (legend%num_entries <= 0) then legend_width = 0.0_wp legend_height = 0.0_wp return end if max_label_w = 0 label_h = 0 do i = 1, legend%num_entries max_label_w = max(max_label_w, calculate_text_width(legend%entries(i)%label)) label_h = max(label_h, calculate_text_height(legend%entries(i)%label)) end do ! Match layout spacing used in legend layout (in pixels here) line_len = 20 text_gap = 6 padding_x = 4 pad_y = 4 entry_gap = 5 legend_width = real(2*padding_x + line_len + text_gap + max_label_w + 1, wp) ! +1px AA/border safety legend_height = real(2*pad_y + legend%num_entries*label_h + max(legend%num_entries-1,0)*entry_gap, wp) end subroutine raster_calculate_legend_dimensions subroutine raster_set_legend_border_width() !! Set thin border width for PNG legend ! No-op: border width handled by context ! This method exists only for polymorphic compatibility end subroutine raster_set_legend_border_width subroutine raster_calculate_legend_position(legend, x, y) !! Calculate standard legend position for PNG using plot coordinates use fortplot_legend, only: legend_t type(legend_t), intent(in) :: legend real(wp), intent(out) :: x, y associate(dn=>legend%num_entries); end associate ! No-op: position calculation handled by fortplot_legend module ! This method exists only for polymorphic compatibility x = 0.0_wp y = 0.0_wp end subroutine raster_calculate_legend_position subroutine raster_extract_rgb_data(raster, width, height, rgb_data) !! Extract RGB data from PNG backend type(raster_image_t), intent(in) :: raster integer, intent(in) :: width, height real(wp), intent(out) :: rgb_data(width, height, 3) integer :: x, y, idx_base do y = 1, height do x = 1, width ! Calculate 1D index for packed RGB data (width * height * 3 array) ! Format: [R1, G1, B1, R2, G2, B2, ...] idx_base = ((y-1) * width + (x-1)) * 3 ! Extract RGB values (normalized to 0-1) rgb_data(x, y, 1) = real(raster%image_data(idx_base + 1), wp) / 255.0_wp rgb_data(x, y, 2) = real(raster%image_data(idx_base + 2), wp) / 255.0_wp rgb_data(x, y, 3) = real(raster%image_data(idx_base + 3), wp) / 255.0_wp end do end do end subroutine raster_extract_rgb_data subroutine raster_get_png_data(width, height, png_data, status) !! Raster context doesn't generate PNG data - only PNG context does integer, intent(in) :: width, height integer(1), allocatable, intent(out) :: png_data(:) integer, intent(out) :: status associate(dw=>width, dh=>height); end associate ! Raster context doesn't generate PNG data ! This should be overridden by PNG context allocate(png_data(0)) status = -1 end subroutine raster_get_png_data subroutine raster_prepare_3d_data(plots) !! Prepare 3D data for PNG backend (no-op - PNG doesn't use 3D data) use fortplot_plot_data, only: plot_data_t type(plot_data_t), intent(in) :: plots(:) associate(dn=>size(plots)); end associate ! PNG backend doesn't need 3D data preparation - no-op end subroutine raster_prepare_3d_data end module fortplot_raster_rendering