diff --git a/doc/rst/source/plot.rst b/doc/rst/source/plot.rst index 79b2ad02fb0..0f0ce787ff3 100644 --- a/doc/rst/source/plot.rst +++ b/doc/rst/source/plot.rst @@ -20,7 +20,7 @@ Synopsis [ |-D|\ *dx*\ [/*dy*] ] [ |-E|\ [**x**\|\ **y**\|\ **X**\|\ **Y**][**+a**\|\ **A**][**+cl**\|\ **f**][**+n**][**+w**\ *width*\ [/*cap*]][**+p**\ *pen*] ] [ |-F|\ [**c**\|\ **n**\|\ **p**][**a**\|\ **s**\|\ **t**\|\ **r**\|\ *refpoint*] ] -[ |-G|\ *fill*\|\ **+z** ] +[ |-G|\ *fill*\|\ **+g**\|\ **+z** ] [ |-H|\ [*scale*] ] [ |-I|\ [*intens*] ] [ |-L|\ [**+b**\|\ **d**\|\ **D**][**+xl**\|\ **r**\|\ *x0*][**+yb**\|\ **t**\|\ *y0*][**+p**\ *pen*] ] @@ -196,7 +196,7 @@ Optional Arguments .. _-G: -**-G**\ *fill*\|\ **+z** :ref:`(more ...) <-Gfill_attrib>` +**-G**\ *fill*\|\ **+g**\|\ **+z** :ref:`(more ...) <-Gfill_attrib>` Select color or pattern for filling of symbols or polygons [Default is no fill]. Note that this module will search for |-G| and |-W| strings in all the segment headers and let any values thus found over-ride the command line settings. @@ -206,6 +206,19 @@ Optional Arguments we will cycle through the fill colors implied by :term:`COLOR_SET` and change on a per-segment or per-table basis. Any *transparency* setting is unchanged. + **+g** + Enable vertex-based color gradients for polygons using Gouraud shading. + This modifier automatically detects the input data format and supports three formats: + + 1. **RGB format**: *x y r g b* where RGB values are in the range 0-255. + 2. **Color name format**: *x y colorname* where *colorname* can be a named color (e.g., "red", "blue"), + hex color (e.g., "#FF0000"), slash-separated RGB (e.g., "255/0/0"), H-S-V or C/M/Y/K formats. + 3. **CPT format**: *x y z* where *z* values are mapped to colors via the CPT specified with |-C|. + + Polygons with any number of vertices are supported and automatically triangulated using a fan triangulation + from the first vertex. Each vertex gets its own color, and Gouraud shading creates smooth color gradients + across the polygon faces. Use |-W| to add an outline to the gradient polygons. + .. _-H: **-H**\ [*scale*] @@ -455,6 +468,35 @@ to cm given the scale 3.60:: gmt plot -R20/40/-20/0 -JM6i -Sv0.15i+e+z3.6c -Gred -W0.25p -Baf data.txt -pdf map +To plot a triangle with smooth color gradients using RGB values directly:: + + cat << EOF > triangle.txt + 0 0 255 0 0 + 3 0 0 255 0 + 1.5 2.6 0 0 255 + EOF + gmt plot triangle.txt -R-1/4/-1/3 -JX10c -G+g -W0.5p,black -Baf -png gradient + +Or using color names for a quadrilateral:: + + cat << EOF > square.txt + 0 0 red + 2 0 yellow + 2 2 green + 0 2 blue + EOF + gmt plot square.txt -R-0.5/2.5/-0.5/2.5 -JX10c -G+g -W1p -Baf -png gradient + +Or using a CPT file with z-values:: + + gmt makecpt -Chot -T0/100 > temp.cpt + cat << EOF > triangle_z.txt + 0 0 0 + 3 0 50 + 1.5 2.6 100 + EOF + gmt plot triangle_z.txt -R-1/4/-1/3 -JX10c -G+g -Ctemp.cpt -W0.5p -Baf -png gradient + .. include:: plot_notes.rst_ .. include:: auto_legend_info.rst_ diff --git a/doc/rst/source/plot3d.rst b/doc/rst/source/plot3d.rst index cb4173d133a..093d1b3a190 100644 --- a/doc/rst/source/plot3d.rst +++ b/doc/rst/source/plot3d.rst @@ -19,7 +19,7 @@ Synopsis [ |SYN_OPT-B| ] [ |-C|\ *cpt* ] [ |-D|\ *dx*/*dy*\ [/*dz*] ] -[ |-G|\ *fill*\|\ **+z** ] +[ |-G|\ *fill*\|\ **+g**\|\ **+z** ] [ |-H|\ [*scale*] ] [ |-I|\ [*intens*] ] [ |-L|\ [**+b**\|\ **d**\|\ **D**][**+xl**\|\ **r**\|\ *x0*][**+yl**\|\ **r**\|\ *y0*][**+p**\ *pen*] ] @@ -129,7 +129,7 @@ Optional Arguments .. _-G: -**-G**\ *fill* :ref:`(more ...) <-Gfill_attrib>` +**-G**\ *fill*\|\ **+g**\|\ **+z** :ref:`(more ...) <-Gfill_attrib>` Select color or pattern for filling of symbols or polygons [Default is no fill]. Note that the module will search for |-G| and |-W| strings in all the segment headers and let any values thus found over-ride the command line settings. @@ -138,6 +138,19 @@ Optional Arguments we will cycle through the fill colors implied by :term:`COLOR_SET` and change on a per-segment or per-table basis. Any *transparency* setting is unchanged. + **+g** + Enable vertex-based color gradients for polygons using Gouraud shading. + This modifier automatically detects the input data format and supports three formats: + + 1. **RGB format**: *x y z r g b* where RGB values are in the range 0-255. + 2. **Color name format**: *x y z colorname* where *colorname* can be a named color (e.g., "red", "blue"), + hex color (e.g., "#FF0000"), slash-separated RGB (e.g., "255/0/0"), H-S-V or C/M/Y/K formats. + 3. **CPT format**: *x y z value* where *value* is mapped to colors via the CPT specified with |-C|. + + Polygons with any number of vertices are supported and automatically triangulated using a fan triangulation + from the first vertex. Each vertex gets its own color, and Gouraud shading creates smooth color gradients + across the polygon faces. Use |-W| to add an outline to the gradient polygons. + .. _-H: **-H**\ [*scale*] diff --git a/src/postscriptlight.c b/src/postscriptlight.c index 57e7c814290..7bf16adaa42 100644 --- a/src/postscriptlight.c +++ b/src/postscriptlight.c @@ -4853,6 +4853,198 @@ int PSL_plotpolygon (struct PSL_CTRL *PSL, double *x, double *y, int n) { return (PSL_NO_ERROR); } +int PSL_plotgradienttriangle(struct PSL_CTRL *PSL, double *x, double *y, double *rgb, int steps) { + /* Draw triangle with smooth vertex color gradient using barycentric interpolation + * x, y: arrays of 3 coordinates (in inches, will be converted to points) + * rgb: array of 9 values [r1 g1 b1 r2 g2 b2 r3 g3 b3], each 0.0-1.0 + * steps: number of subdivisions (higher = smoother, slower) + * + * Writen by Claude.ai + */ + + int i, j; + double s, t; + double s1, t1, s2, t2, s3, t3, s4, t4; + double u1, v1, w1, u2, v2, w2, u3, v3, w3, u4, v4, w4; + double px1, py1, px2, py2, px3, py3, px4, py4; + double pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3, pr4, pg4, pb4; + double step_inv; + double x_pts[3], y_pts[3]; + + if (steps <= 0) steps = 50; /* Default quality */ + if (steps > 200) steps = 200; /* Limit maximum */ + + /* Convert inches to points */ + for (i = 0; i < 3; i++) { + x_pts[i] = x[i] * PSL->internal.dpu; + y_pts[i] = y[i] * PSL->internal.dpu; + } + + step_inv = 1.0 / steps; + + PSL_comment(PSL, "Begin gradient triangle\n"); + PSL_command(PSL, "gsave\n"); + + /* Subdivide triangle into micro-triangles using barycentric coordinates */ + for (i = 0; i < steps; i++) { + t = i * step_inv; + for (j = 0; j < steps; j++) { + s = j * step_inv; + + /* Barycentric coordinates for 4 corners of sub-quad */ + s1 = s; t1 = t; + s2 = s + step_inv; t2 = t; + s3 = s + step_inv; t3 = t + step_inv; + s4 = s; t4 = t + step_inv; + + /* Check if first corner is inside triangle */ + if (s1 + t1 <= 1.0) { + u1 = 1.0 - s1 - t1; + v1 = s1; + w1 = t1; + + /* Interpolate position */ + px1 = u1 * x_pts[0] + v1 * x_pts[1] + w1 * x_pts[2]; + py1 = u1 * y_pts[0] + v1 * y_pts[1] + w1 * y_pts[2]; + + /* Interpolate color */ + pr1 = u1 * rgb[0] + v1 * rgb[3] + w1 * rgb[6]; + pg1 = u1 * rgb[1] + v1 * rgb[4] + w1 * rgb[7]; + pb1 = u1 * rgb[2] + v1 * rgb[5] + w1 * rgb[8]; + + /* Check second corner */ + if (s2 + t2 <= 1.0) { + u2 = 1.0 - s2 - t2; + v2 = s2; + w2 = t2; + + px2 = u2 * x_pts[0] + v2 * x_pts[1] + w2 * x_pts[2]; + py2 = u2 * y_pts[0] + v2 * y_pts[1] + w2 * y_pts[2]; + pr2 = u2 * rgb[0] + v2 * rgb[3] + w2 * rgb[6]; + pg2 = u2 * rgb[1] + v2 * rgb[4] + w2 * rgb[7]; + pb2 = u2 * rgb[2] + v2 * rgb[5] + w2 * rgb[8]; + + /* Check fourth corner */ + if (s4 + t4 <= 1.0) { + u4 = 1.0 - s4 - t4; + v4 = s4; + w4 = t4; + + px4 = u4 * x_pts[0] + v4 * x_pts[1] + w4 * x_pts[2]; + py4 = u4 * y_pts[0] + v4 * y_pts[1] + w4 * y_pts[2]; + pr4 = u4 * rgb[0] + v4 * rgb[3] + w4 * rgb[6]; + pg4 = u4 * rgb[1] + v4 * rgb[4] + w4 * rgb[7]; + pb4 = u4 * rgb[2] + v4 * rgb[5] + w4 * rgb[8]; + + /* Draw first micro-triangle: p1-p2-p4 */ + PSL_command(PSL, "%.6f %.6f %.6f C\n", + (pr1 + pr2 + pr4) / 3.0, + (pg1 + pg2 + pg4) / 3.0, + (pb1 + pb2 + pb4) / 3.0); + PSL_command(PSL, "N %.4f %.4f M %.4f %.4f L %.4f %.4f L P F\n", px1, py1, px2, py2, px4, py4); + } + + /* Check third corner for second micro-triangle */ + if (s3 + t3 <= 1.0) { + u3 = 1.0 - s3 - t3; + v3 = s3; + w3 = t3; + + px3 = u3 * x_pts[0] + v3 * x_pts[1] + w3 * x_pts[2]; + py3 = u3 * y_pts[0] + v3 * y_pts[1] + w3 * y_pts[2]; + pr3 = u3 * rgb[0] + v3 * rgb[3] + w3 * rgb[6]; + pg3 = u3 * rgb[1] + v3 * rgb[4] + w3 * rgb[7]; + pb3 = u3 * rgb[2] + v3 * rgb[5] + w3 * rgb[8]; + + if (s4 + t4 <= 1.0) { /* Already computed p4 */ + /* Draw second micro-triangle: p2-p3-p4 */ + PSL_command(PSL, "%.6f %.6f %.6f C\n", + (pr2 + pr3 + pr4) / 3.0, + (pg2 + pg3 + pg4) / 3.0, + (pb2 + pb3 + pb4) / 3.0); + PSL_command(PSL, "N %.4f %.4f M %.4f %.4f L %.4f %.4f L P F\n", px2, py2, px3, py3, px4, py4); + } + } + } + } + } + } + + PSL_command(PSL, "grestore\n"); + PSL_comment(PSL, "End gradient triangle\n"); + + return (PSL_NO_ERROR); +} + +int PSL_plotgradienttriangle_gouraud(struct PSL_CTRL *PSL, double *x, double *y, double *rgb) { + /* Draw triangle with smooth Gouraud shading using PostScript Type 4 shading + * x, y: arrays of 3 coordinates (in inches, will be converted to points) + * rgb: array of 9 values [r1 g1 b1 r2 g2 b2 r3 g3 b3], each 0.0-1.0 + * This uses PostScript Level 2/3 shading for perfectly smooth gradients + * + * Writen by Claude.ai + */ + + int i; + double x_pts[3], y_pts[3]; + + /* Convert inches to points */ + for (i = 0; i < 3; i++) { + x_pts[i] = x[i] * PSL->internal.dpu; + y_pts[i] = y[i] * PSL->internal.dpu; + } + + PSL_comment(PSL, "Begin Gouraud shaded triangle\n"); + PSL_command(PSL, "gsave\n"); + /* Set clipping path to triangle to constrain shfill and establish bounding box */ + PSL_command(PSL, "N %.4f %.4f M %.4f %.4f L %.4f %.4f L closepath clip", + x_pts[0], y_pts[0], x_pts[1], y_pts[1], x_pts[2], y_pts[2]); + + + /* Create Type 4 (Free-Form Gouraud) shading dictionary */ + PSL_command(PSL, "<<\n"); + PSL_command(PSL, " /ShadingType 4\n"); + PSL_command(PSL, " /ColorSpace /DeviceRGB\n"); + PSL_command(PSL, " /BitsPerCoordinate 16\n"); + PSL_command(PSL, " /BitsPerComponent 8\n"); + PSL_command(PSL, " /BitsPerFlag 8\n"); + + /* Decode arrays map from integer coordinates to actual coordinate ranges */ + /* Find bounding box */ + double xmin = x_pts[0], xmax = x_pts[0], ymin = y_pts[0], ymax = y_pts[0]; + for (i = 1; i < 3; i++) { + if (x_pts[i] < xmin) xmin = x_pts[i]; + if (x_pts[i] > xmax) xmax = x_pts[i]; + if (y_pts[i] < ymin) ymin = y_pts[i]; + if (y_pts[i] > ymax) ymax = y_pts[i]; + } + + PSL_command(PSL, " /Decode [%.4f %.4f %.4f %.4f 0 1 0 1 0 1]\n", xmin, xmax, ymin, ymax); + PSL_command(PSL, " /DataSource <\n"); + + /* Write triangle data as hex string */ + /* Flag 0 = start new triangle, then coordinates (x,y) scaled to 0-65535, then RGB scaled to 0-255 */ + for (i = 0; i < 3; i++) { + unsigned int flag = 0; /* Start new triangle with vertex 0 */ + unsigned int x_scaled = (unsigned int)((x_pts[i] - xmin) / (xmax - xmin) * 65535.0 + 0.5); + unsigned int y_scaled = (unsigned int)((y_pts[i] - ymin) / (ymax - ymin) * 65535.0 + 0.5); + unsigned int r = (unsigned int)(rgb[i*3 + 0] * 255.0 + 0.5); + unsigned int g = (unsigned int)(rgb[i*3 + 1] * 255.0 + 0.5); + unsigned int b = (unsigned int)(rgb[i*3 + 2] * 255.0 + 0.5); + + PSL_command(PSL, " %02X %04X %04X %02X%02X%02X\n", flag, x_scaled, y_scaled, r, g, b); + } + + PSL_command(PSL, " >\n"); + PSL_command(PSL, ">>\n"); + PSL_command(PSL, "shfill\n"); + + PSL_command(PSL, "grestore\n"); + PSL_comment(PSL, "End Gouraud shaded triangle\n"); + + return (PSL_NO_ERROR); +} + int PSL_setexec (struct PSL_CTRL *PSL, int action) { /* Enables of disables the execution of a PSL_plot_completion function at start of a PSL_plotinit overlay */ PSL->current.complete = (action) ? 1 : 0; diff --git a/src/postscriptlight.h b/src/postscriptlight.h index 067e2b613c5..a07fc5227c6 100644 --- a/src/postscriptlight.h +++ b/src/postscriptlight.h @@ -477,6 +477,8 @@ EXTERN_MSC int PSL_plotparagraphbox (struct PSL_CTRL *PSL, double x, double y, d EXTERN_MSC int PSL_plotpoint (struct PSL_CTRL *PSL, double x, double y, int pen); EXTERN_MSC int PSL_plotbox (struct PSL_CTRL *PSL, double x0, double y0, double x1, double y1); EXTERN_MSC int PSL_plotpolygon (struct PSL_CTRL *PSL, double *x, double *y, int n); +EXTERN_MSC int PSL_plotgradienttriangle (struct PSL_CTRL *PSL, double *x, double *y, double *rgb, int steps); +EXTERN_MSC int PSL_plotgradienttriangle_gouraud (struct PSL_CTRL *PSL, double *x, double *y, double *rgb); EXTERN_MSC int PSL_plotsegment (struct PSL_CTRL *PSL, double x0, double y0, double x1, double y1); EXTERN_MSC int PSL_plotsymbol (struct PSL_CTRL *PSL, double x, double y, double param[], int symbol); EXTERN_MSC int PSL_plottext (struct PSL_CTRL *PSL, double x, double y, double fontsize, char *text, double angle, int justify, int mode); diff --git a/src/psxy.c b/src/psxy.c index c8c88223aab..741a6d19cf9 100644 --- a/src/psxy.c +++ b/src/psxy.c @@ -72,6 +72,9 @@ struct PSXY_CTRL { struct PSXY_G { /* -G|+z */ bool active; bool set_color; + bool gradient; + bool smooth; /* Use Gouraud shading instead of subdivision */ + bool direct; /* Direct color specification instead of CPT */ unsigned int sequential; struct GMT_FILL fill; } G; @@ -576,6 +579,10 @@ static int usage (struct GMTAPI_CTRL *API, int level) { gmt_fill_syntax (API->GMT, 'G', NULL, "Specify color or pattern [no fill]."); GMT_Usage (API, -2, "The -G option can be present in all segment headers (not with -S). " "To assign fill color via -Z, give -G+z)."); + GMT_Usage (API, 3, "+g Enable vertex-based color gradients for polygons using Gouraud shading."); + GMT_Usage (API, -3, "Input data formats: (1) x y r g b (RGB values 0-255), (2) x y colorname, or (3) x y z with -C."); + GMT_Usage (API, -3, "Requires at least 3 vertices per polygon. Use -W to add outline."); + GMT_Usage (API, 3, "+gt Same as +g but reserved for future subdivision algorithm [currently uses Gouraud]."); GMT_Usage (API, 1, "\n-H[]"); GMT_Usage (API, -2, "Scale symbol sizes (set via -S or input column) by factors read from scale column. " "The scale column follows the symbol size column. Alternatively, append a fixed ."); @@ -941,6 +948,16 @@ static int parse (struct GMT_CTRL *GMT, struct PSXY_CTRL *Ctrl, struct GMT_OPTIO n_errors += gmt_M_repeated_module_option (API, Ctrl->G.active); if (strncmp (opt->arg, "+z", 2U) == 0) Ctrl->G.set_color = true; + else if (strncmp (opt->arg, "+gt", 3U) == 0) { + Ctrl->G.gradient = true; + Ctrl->G.smooth = false; /* Subdivision/triangulation, format auto-detected */ + Ctrl->G.active = true; + } + else if (strncmp (opt->arg, "+g", 2U) == 0) { + Ctrl->G.gradient = true; + Ctrl->G.smooth = true; /* Gouraud shading (default), format auto-detected */ + Ctrl->G.active = true; + } else if (!opt->arg[0] || gmt_getfill (GMT, opt->arg, &Ctrl->G.fill)) { gmt_fill_syntax (GMT, 'G', NULL, " "); n_errors++; } @@ -1124,6 +1141,12 @@ static int parse (struct GMT_CTRL *GMT, struct PSXY_CTRL *Ctrl, struct GMT_OPTIO if (Ctrl->G.set_color && !Ctrl->L.polygon) { /* Otherwise -G+z -Z and open polylines would color only the outline */ Ctrl->L.active = Ctrl->L.polygon = true; } + if (Ctrl->G.gradient && !Ctrl->L.polygon) { /* Same for gradient triangles */ + Ctrl->L.active = Ctrl->L.polygon = true; + } + if (Ctrl->G.gradient) { /* Gradient triangles need x,y,z columns */ + gmt_set_cols (GMT, GMT_IN, 3); + } gmt_consider_current_cpt (API, &Ctrl->C.active, &(Ctrl->C.file)); @@ -2465,14 +2488,31 @@ EXTERN_MSC int GMT_psxy (void *V_API, int mode, void *args) { if (Ctrl->L.anchor == PSXY_POL_SYMM_DEV) n_cols = 3; else if (Ctrl->L.anchor == PSXY_POL_ASYMM_DEV || Ctrl->L.anchor == PSXY_POL_ASYMM_ENV) n_cols = 4; + bool need_text = false; /* Flag for color string format requiring text reading */ + bool skip_set_columns = false; + if (Ctrl->G.gradient) { + /* For gradients, don't force column count - let GMT read all available columns + * and enable text for color name support */ + need_text = true; + skip_set_columns = true; /* Skip GMT_Set_Columns to let GMT auto-detect */ + } conf_line = (Ctrl->L.anchor >= PSXY_POL_SYMM_DEV && Ctrl->L.anchor <= PSXY_POL_ASYMM_ENV); - if (GMT_Init_IO (API, GMT_IS_DATASET, geometry, GMT_IN, GMT_ADD_DEFAULT, 0, options) != GMT_NOERROR) { /* Register data input */ + if (GMT_Init_IO(API, GMT_IS_DATASET, geometry, GMT_IN, GMT_ADD_DEFAULT, 0, options) != GMT_NOERROR) { /* Register data input */ Return (API->error); } - if ((error = GMT_Set_Columns (API, GMT_IN, (unsigned int)n_cols, GMT_COL_FIX_NO_TEXT)) != GMT_NOERROR) { - /* We don't want trailing text because we may need to resample lines below */ - Return (API->error); + if (skip_set_columns && need_text) { + /* For gradients: read 5 columns with text - GMT will read what's available and set L->n_columns correctly */ + GMT->current.io.max_cols_to_read = 0; /* Do not make any assumptions about max columns */ + if ((error = GMT_Set_Columns(API, GMT_IN, 0, GMT_COL_VAR)) != GMT_NOERROR) { + Return (API->error); + } + } + else if (!skip_set_columns) { + if ((error = GMT_Set_Columns(API, GMT_IN, (unsigned int)n_cols, need_text ? GMT_COL_FIX : GMT_COL_FIX_NO_TEXT)) != GMT_NOERROR) { + /* We don't want trailing text because we may need to resample lines below, except for gradients */ + Return (API->error); + } } if ((S.symbol == GMT_SYMBOL_QUOTED_LINE || S.symbol == GMT_SYMBOL_DECORATED_LINE) && S.G.segmentize) { /* Special quoted/decorated line where each point-pair should be considered a line segment */ struct GMT_SEGMENTIZE S; @@ -2560,6 +2600,7 @@ EXTERN_MSC int GMT_psxy (void *V_API, int mode, void *args) { if (Ctrl->W.cpt_effect && Ctrl->W.pen.cptmode & 2) polygon = true; if (Ctrl->G.set_color) polygon = true; + if (Ctrl->G.gradient) polygon = true; if (Ctrl->M.active && !Ctrl->M.constant) { /* Must check input matches requirements */ if (Ctrl->M.mode == GMT_CURVES_SEPARATE) { @@ -2817,9 +2858,122 @@ EXTERN_MSC int GMT_psxy (void *V_API, int mode, void *args) { gmt_set_seg_minmax (GMT, D->geometry, 2, L); /* Update min/max of x/y only */ } - if (polygon && !no_line_clip) { /* Want a closed polygon (with or without fill and with or without outline) */ - gmt_setfill (GMT, ¤t_fill, outline_setting); - gmt_geo_polygons (GMT, L); + /* Handle gradient polygons (works for both clipped and non-clipped paths) */ + bool gradient_done = false; + if (polygon && Ctrl->G.gradient) { + /* Auto-detect gradient format based on GMT's column count */ + if (Ctrl->G.gradient) { + if (L->n_columns == 5) { + /* RGB format: x y r g b */ + Ctrl->G.direct = true; + } + else if (L->n_columns == 2 && L->text && L->text[0]) { + /* Color string format: x y colorname (L->n_columns counts text as column 3) */ + Ctrl->G.direct = true; + } + else if (L->n_columns == 3) { + /* CPT format: x y z (3 numeric columns) */ + Ctrl->G.direct = false; + } + } + + /* Error check: CPT mode requires a color palette */ + if (Ctrl->G.gradient && !Ctrl->G.direct && P == NULL) { + GMT_Report(API, GMT_MSG_ERROR, "Gradient option -G+g detected CPT format (x y z) but no color palette provided. Use -C to specify a color palette.\n"); + Return (GMT_RUNTIME_ERROR); + } + + /* For gradients: need at least 3 vertices AND appropriate data format */ + if (Ctrl->G.gradient && L->n_rows >= 3 && + ((L->n_columns == 5 && Ctrl->G.direct) || (L->n_columns == 3 && !Ctrl->G.direct && P) || (L->n_columns == 2 && L->text && Ctrl->G.direct))) { + double tri_rgb[9], rgb_tmp[4]; + double plot_x[3], plot_y[3]; + + /* Project to map coordinates (use cart_to_xy if no clipping) */ + if (no_line_clip) + GMT->current.plot.n = gmt_cart_to_xy_line (GMT, L->data[GMT_X], L->data[GMT_Y], L->n_rows); + else + GMT->current.plot.n = gmt_geo_to_xy_line (GMT, L->data[GMT_X], L->data[GMT_Y], L->n_rows); + + if (GMT->current.plot.n > 0 && GMT->current.plot.n >= 3) { + /* Check if polygon is closed (last vertex == first vertex) */ + bool closed = (L->data[GMT_X][0] == L->data[GMT_X][L->n_rows-1] && + L->data[GMT_Y][0] == L->data[GMT_Y][L->n_rows-1]); + unsigned int n_unique = closed ? L->n_rows - 1 : L->n_rows; + + /* Error check: need at least 3 unique vertices */ + if (n_unique < 3) { + GMT_Report(API, GMT_MSG_ERROR, "Gradient polygons require at least 3 vertices (found %u)\n", n_unique); + Return (GMT_RUNTIME_ERROR); + } + + /* Fan triangulation from first vertex */ + unsigned int n_triangles = n_unique - 2; + + for (unsigned int tri = 0; tri < n_triangles; tri++) { + /* Triangle i uses vertices: 0, i+1, i+2 */ + unsigned int tri_indices[3] = {0, tri + 1, tri + 2}; + /* Get RGB colors for this triangle */ + for (unsigned int k = 0; k < 3; k++) { + unsigned int idx = tri_indices[k]; + if (Ctrl->G.direct && L->n_columns == 5) { + /* RGB from columns: x y r g b */ + tri_rgb[k * 3 + 0] = L->data[2][idx] / 255.0; + tri_rgb[k * 3 + 1] = L->data[3][idx] / 255.0; + tri_rgb[k * 3 + 2] = L->data[4][idx] / 255.0; + } + else if (Ctrl->G.direct && L->n_columns == 2 && L->text && L->text[idx]) { + /* Color name from text (n_columns==2 for x,y + text field) */ + if (gmt_getrgb(GMT, L->text[idx], rgb_tmp) == 0) { + tri_rgb[k * 3 + 0] = rgb_tmp[0]; + tri_rgb[k * 3 + 1] = rgb_tmp[1]; + tri_rgb[k * 3 + 2] = rgb_tmp[2]; + } + else { + tri_rgb[k * 3 + 0] = tri_rgb[k * 3 + 1] = tri_rgb[k * 3 + 2] = 0.0; + GMT_Report(API, GMT_MSG_WARNING, "Failed to parse color '%s', using black\n", L->text[idx]); + } + } + else if (!Ctrl->G.direct && L->n_columns == 3) { + /* CPT-based: z from column 3 */ + gmt_get_rgb_from_z(GMT, P, L->data[2][idx], rgb_tmp); + tri_rgb[k * 3 + 0] = rgb_tmp[0]; + tri_rgb[k * 3 + 1] = rgb_tmp[1]; + tri_rgb[k * 3 + 2] = rgb_tmp[2]; + } + else { + /* Fallback */ + tri_rgb[k * 3 + 0] = tri_rgb[k * 3 + 1] = tri_rgb[k * 3 + 2] = 0.0; + } + } + + /* Copy coordinates (PSL expects inches, not points!) */ + for (unsigned int k = 0; k < 3; k++) { + unsigned int idx = tri_indices[k]; + plot_x[k] = GMT->current.plot.x[idx]; + plot_y[k] = GMT->current.plot.y[idx]; + } + + /* Draw gradient */ + if (Ctrl->G.smooth) + PSL_plotgradienttriangle_gouraud(PSL, plot_x, plot_y, tri_rgb); + else + PSL_plotgradienttriangle(PSL, plot_x, plot_y, tri_rgb, 50); + } + + gradient_done = true; + + /* Optionally draw outline */ + if (outline_setting) { + gmt_setpen(GMT, ¤t_pen); + PSL_plotpolygon(PSL, GMT->current.plot.x, GMT->current.plot.y, L->n_rows); + } + } + } + if (!gradient_done) { + gmt_setfill(GMT, ¤t_fill, outline_setting); + gmt_geo_polygons(GMT, L); + } } else if (S.symbol == GMT_SYMBOL_QUOTED_LINE) { /* Labeled lines are dealt with by the contour machinery */ bool closed, split = false; @@ -2973,20 +3127,59 @@ EXTERN_MSC int GMT_psxy (void *V_API, int mode, void *args) { else GMT->current.plot.n = gmt_cart_to_xy_line (GMT, GMT->hidden.mem_coord[GMT_X], GMT->hidden.mem_coord[GMT_Y], end); if (Ctrl->L.outline) gmt_setpen (GMT, &Ctrl->L.pen); /* Select separate pen for polygon outline */ - if (Ctrl->G.active) /* Specify the fill, possibly set outline */ + if (Ctrl->G.active) { /* Specify the fill, possibly set outline */ gmt_setfill (GMT, ¤t_fill, Ctrl->L.outline); - else /* No fill, just outline */ - gmt_setfill (GMT, NULL, Ctrl->L.outline); - PSL_plotpolygon (PSL, GMT->current.plot.x, GMT->current.plot.y, (int)GMT->current.plot.n); + /* Check if gradient triangle */ + if (Ctrl->G.gradient && (GMT->current.plot.n == 3 || GMT->current.plot.n == 4) && D->table[tbl]->segment[seg]->n_columns >= 3 && L->n_rows >= 3 && P) { + double tri_rgb[9], rgb_tmp[4]; + double plot_x[3], plot_y[3]; + struct GMT_DATASEGMENT *S_orig = D->table[tbl]->segment[seg]; + + /* Get RGB colors from z-values via CPT */ + for (unsigned int k = 0; k < 3; k++) { + gmt_get_rgb_from_z(GMT, P, S_orig->data[2][k], rgb_tmp); + tri_rgb[k * 3 + 0] = gmt_M_is255(rgb_tmp[0]); + tri_rgb[k * 3 + 1] = gmt_M_is255(rgb_tmp[1]); + tri_rgb[k * 3 + 2] = gmt_M_is255(rgb_tmp[2]); + } + + /* Convert to PostScript points */ + for (unsigned int k = 0; k < 3; k++) { + plot_x[k] = GMT->current.plot.x[k] * GMT->session.u2u[GMT_INCH][GMT_PT]; + plot_y[k] = GMT->current.plot.y[k] * GMT->session.u2u[GMT_INCH][GMT_PT]; + } + + /* Draw gradient */ + if (Ctrl->G.smooth) + PSL_plotgradienttriangle_gouraud(PSL, plot_x, plot_y, tri_rgb); + else + PSL_plotgradienttriangle(PSL, plot_x, plot_y, tri_rgb, 50); + + /* Optionally draw outline */ + if (Ctrl->L.outline) { + PSL_plotpolygon(PSL, GMT->current.plot.x, GMT->current.plot.y, (GMT->current.plot.n == 4) ? 3 : (int)GMT->current.plot.n); + } + } + else { + PSL_plotpolygon(PSL, GMT->current.plot.x, GMT->current.plot.y, (int)GMT->current.plot.n); + } + } + else { /* No fill, just outline */ + gmt_setfill(GMT, NULL, Ctrl->L.outline); + PSL_plotpolygon(PSL, GMT->current.plot.x, GMT->current.plot.y, (int)GMT->current.plot.n); + } if (!Ctrl->W.active) draw_line = false; /* Did not want to actually draw the main line */ - if (Ctrl->L.outline) gmt_setpen (GMT, ¤t_pen); /* Reset the pen to what -W indicates */ + if (Ctrl->L.outline) gmt_setpen(GMT, ¤t_pen); /* Reset the pen to what -W indicates */ } if (no_line_clip) { /* Draw line or polygon without border clipping at all */ - if ((GMT->current.plot.n = gmt_cart_to_xy_line (GMT, L->data[GMT_X], L->data[GMT_Y], L->n_rows)) == 0) continue; - if (outline_active) gmt_setpen (GMT, ¤t_pen); /* Select separate pen for polygon outline */ - if (Ctrl->G.active) { /* Specify the fill, possibly set outline */ + /* Skip projection if gradient already handled it */ + if (!gradient_done && (GMT->current.plot.n = gmt_cart_to_xy_line (GMT, L->data[GMT_X], L->data[GMT_Y], L->n_rows)) == 0) continue; + if (outline_active && !gradient_done) gmt_setpen (GMT, ¤t_pen); /* Select separate pen for polygon outline */ + if (Ctrl->G.active && !gradient_done) { /* Specify the fill (skip if gradient already drawn) */ gmt_setfill (GMT, ¤t_fill, outline_active); - PSL_plotpolygon (PSL, GMT->current.plot.x, GMT->current.plot.y, (int)GMT->current.plot.n); + { + PSL_plotpolygon (PSL, GMT->current.plot.x, GMT->current.plot.y, (int)GMT->current.plot.n); + } } else { /* No fill but may still be a polygon */ gmt_setfill (GMT, NULL, outline_active); diff --git a/src/psxyz.c b/src/psxyz.c index 967b1e5ee02..3e3493dc2c4 100644 --- a/src/psxyz.c +++ b/src/psxyz.c @@ -53,9 +53,12 @@ struct PSXYZ_CTRL { bool active; double dx, dy, dz; } D; - struct PSXYZ_G { /* -G|+z */ + struct PSXYZ_G { /* -G|+z|+g */ bool active; bool set_color; + bool gradient; /* Use gradient fill with vertex colors */ + bool smooth; /* Use Gouraud shading (true) vs subdivision (false) */ + bool direct; /* Direct color specification (RGB/names) vs CPT */ unsigned int sequential; struct GMT_FILL fill; } G; @@ -206,6 +209,10 @@ static int usage (struct GMTAPI_CTRL *API, int level) { gmt_fill_syntax (API->GMT, 'G', NULL, "Specify color or pattern [Default is no fill]."); GMT_Usage (API, -2, "The -G option can be present in all segment headers (not with -S). " "To assign fill color via -Z, give -G+z)."); + GMT_Usage (API, 3, "+g Enable vertex-based color gradients for polygons using Gouraud shading."); + GMT_Usage (API, -3, "Input data formats: (1) x y z r g b (RGB values 0-255), (2) x y z colorname, or (3) x y z value with -C."); + GMT_Usage (API, -3, "Requires at least 3 vertices per polygon. Use -W to add outline."); + GMT_Usage (API, 3, "+gt Same as +g but reserved for future subdivision algorithm [currently uses Gouraud]."); GMT_Usage (API, 1, "\n-H[]"); GMT_Usage (API, -2, "Scale symbol sizes (set via -S or input column) by factors read from scale column. " "The scale column follows the symbol size column. Alternatively, append a fixed ."); @@ -460,6 +467,16 @@ static int parse (struct GMT_CTRL *GMT, struct PSXYZ_CTRL *Ctrl, struct GMT_OPTI n_errors += gmt_M_repeated_module_option (API, Ctrl->G.active); if (strncmp (opt->arg, "+z", 2U) == 0) Ctrl->G.set_color = true; + else if (strncmp (opt->arg, "+gt", 3U) == 0) { + Ctrl->G.gradient = true; + Ctrl->G.smooth = false; /* Subdivision/triangulation */ + Ctrl->G.active = true; + } + else if (strncmp (opt->arg, "+g", 2U) == 0) { + Ctrl->G.gradient = true; + Ctrl->G.smooth = true; /* Gouraud shading */ + Ctrl->G.active = true; + } else if (!opt->arg[0] || gmt_getfill (GMT, opt->arg, &Ctrl->G.fill)) { gmt_fill_syntax (GMT, 'G', NULL, " "); n_errors++; @@ -2280,7 +2297,54 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { xp = gmt_M_memory (GMT, NULL, n, double); yp = gmt_M_memory (GMT, NULL, n, double); - if (polygon && !no_line_clip) { + /* Handle gradient polygons */ + bool gradient_done = false; + if (polygon && Ctrl->G.gradient && L->n_rows >= 3) { + if (L->n_columns == 6) Ctrl->G.direct = true; + else if (L->n_columns == 3 && L->text && L->text[0]) Ctrl->G.direct = true; + else if (L->n_columns == 4) Ctrl->G.direct = false; + if (!Ctrl->G.direct && P == NULL) { + GMT_Report(API, GMT_MSG_ERROR, "Gradient requires -C"); + Return (GMT_RUNTIME_ERROR); + } + if ((L->n_columns == 6 && Ctrl->G.direct) || (L->n_columns == 4 && !Ctrl->G.direct && P) || (L->n_columns == 3 && L->text && Ctrl->G.direct)) { + double tri_rgb[9], rgb_tmp[4], plot_x[3], plot_y[3]; + bool closed = (L->data[GMT_X][0] == L->data[GMT_X][L->n_rows-1] && L->data[GMT_Y][0] == L->data[GMT_Y][L->n_rows-1]); + unsigned int n_unique = closed ? L->n_rows - 1 : L->n_rows, n_triangles = n_unique - 2; + gmt_plane_perspective (GMT, -1, 0.0); + for (unsigned int tri = 0; tri < n_triangles; tri++) { + unsigned int tri_indices[3] = {0, tri + 1, tri + 2}; + for (unsigned int k = 0; k < 3; k++) { + unsigned int idx = tri_indices[k]; + if (Ctrl->G.direct && L->n_columns == 6) { + tri_rgb[k*3+0] = L->data[3][idx] / 255.0; + tri_rgb[k*3+1] = L->data[4][idx] / 255.0; + tri_rgb[k*3+2] = L->data[5][idx] / 255.0; + } else if (Ctrl->G.direct && L->n_columns == 3 && L->text && L->text[idx]) { + if (gmt_getrgb(GMT, L->text[idx], rgb_tmp) == 0) { + tri_rgb[k*3+0] = rgb_tmp[0]; tri_rgb[k*3+1] = rgb_tmp[1]; tri_rgb[k*3+2] = rgb_tmp[2]; + } else tri_rgb[k*3+0] = tri_rgb[k*3+1] = tri_rgb[k*3+2] = 0.0; + } else if (!Ctrl->G.direct && L->n_columns == 4) { + gmt_get_rgb_from_z(GMT, P, L->data[3][idx], rgb_tmp); + tri_rgb[k*3+0] = rgb_tmp[0]; tri_rgb[k*3+1] = rgb_tmp[1]; tri_rgb[k*3+2] = rgb_tmp[2]; + } else tri_rgb[k*3+0] = tri_rgb[k*3+1] = tri_rgb[k*3+2] = 0.0; + } + for (unsigned int k = 0; k < 3; k++) { + unsigned int idx = tri_indices[k]; + gmt_geoz_to_xy(GMT, L->data[GMT_X][idx], L->data[GMT_Y][idx], L->data[GMT_Z][idx], &plot_x[k], &plot_y[k]); + } + if (Ctrl->G.smooth) PSL_plotgradienttriangle_gouraud(PSL, plot_x, plot_y, tri_rgb); + else PSL_plotgradienttriangle(PSL, plot_x, plot_y, tri_rgb, 50); + } + gradient_done = true; + if (outline_setting) { + for (i = 0; i < n; i++) gmt_geoz_to_xy(GMT, L->data[GMT_X][i], L->data[GMT_Y][i], L->data[GMT_Z][i], &xp[i], &yp[i]); + gmt_setpen(GMT, ¤t_pen); PSL_plotpolygon(PSL, xp, yp, (int)L->n_rows); + } + } + } + + if (polygon && !no_line_clip && !gradient_done) { gmt_plane_perspective (GMT, -1, 0.0); for (i = 0; i < n; i++) gmt_geoz_to_xy (GMT, L->data[GMT_X][i], L->data[GMT_Y][i], L->data[GMT_Z][i], &xp[i], &yp[i]); gmt_setfill (GMT, ¤t_fill, outline_setting); @@ -2404,9 +2468,9 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { gmt_geoz_to_xy (GMT, L->data[GMT_X][i], L->data[GMT_Y][i], L->data[GMT_Z][i], &xp[i], &yp[i]); } if (no_line_clip) { /* Draw line or polygon without border clipping at all */ - if ((GMT->current.plot.n = gmt_cart_to_xy_line (GMT, xp, yp, n)) == 0) continue; - if (outline_active) gmt_setpen (GMT, ¤t_pen); /* Select separate pen for polygon outline */ - if (Ctrl->G.active) { /* Specify the fill, possibly set outline */ + if (!gradient_done && (GMT->current.plot.n = gmt_cart_to_xy_line (GMT, xp, yp, n)) == 0) continue; + if (outline_active && !gradient_done) gmt_setpen (GMT, ¤t_pen); /* Select separate pen for polygon outline */ + if (Ctrl->G.active && !gradient_done) { /* Specify the fill, possibly set outline */ gmt_setfill (GMT, ¤t_fill, outline_active); PSL_plotpolygon (PSL, xp, yp, (int)n); } diff --git a/test/psxy/gradients.sh b/test/psxy/gradients.sh new file mode 100644 index 00000000000..eb9474f7762 --- /dev/null +++ b/test/psxy/gradients.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Test gradient-filled polygons with -G+g (Gouraud shading) +# Tests RGB format (x y r g b), color names (x y colorname), +# CPT format (x y z), and arbitrary polygon vertex counts + +ps=gradients.ps + +# Create CPT for CPT-based tests +gmt makecpt -Chot -T0/100 > t.cpt + +# Row 1: RGB format - triangle and square +gmt psxy -R-1/4/-1/3 -JX4c -Bafg -BWSne+t"RGB Triangle" -G+g -P -K -Xc -Y18c << EOF > $ps +0 0 255 0 0 +3 0 0 255 0 +1.5 2.6 0 0 255 +0 0 255 0 0 +EOF + +gmt psxy -R-0.5/2.5/-0.5/2.5 -JX4c -Bafg -BWSne+t"RGB Square" -G+g -O -K -X5c << EOF >> $ps +0 0 255 0 0 +2 0 255 255 0 +2 2 0 255 0 +0 2 0 0 255 +0 0 255 0 0 +EOF + +# Row 2: Color names - triangle and square +gmt psxy -R-1/4/-1/3 -JX4c -Bafg -BWSne+t"Color Names Triangle" -G+g -O -K -X-5c -Y-5.5c << EOF >> $ps +0 0 red +3 0 green +1.5 2.6 blue +0 0 red +EOF + +gmt psxy -R-0.5/2.5/-0.5/2.5 -JX4c -Bafg -BWSne+t"Color Names Square" -G+g -O -K -X5c << EOF >> $ps +0 0 red +2 0 yellow +2 2 green +0 2 blue +0 0 red +EOF + +# Row 3: CPT format - triangle and square +gmt psxy -R-1/4/-1/3 -JX4c -Bafg -BWSne+t"CPT Triangle" -G+g -Ct.cpt -O -K -X-5c -Y-5.5c << EOF >> $ps +0 0 0 +3 0 50 +1.5 2.6 100 +0 0 0 +EOF + +gmt psxy -R-0.5/2.5/-0.5/2.5 -JX4c -Bafg -BWSne+t"CPT Square" -G+g -Ct.cpt -O -K -X5c << EOF >> $ps +0 0 0 +2 0 33 +2 2 67 +0 2 100 +0 0 0 +EOF + +# Row 4: Pentagon (arbitrary polygon) with hex colors and slash-separated RGB +gmt psxy -R-0.5/2.5/-0.5/2 -JX4c -Bafg -BWSne+t"Pentagon Hex" -G+g -O -K -X-5c -Y-5.5c << EOF >> $ps +1 0 #FF0000 +1.951 0.588 #FFA500 +1.588 1.538 #FFFF00 +0.412 1.538 #00FF00 +0.049 0.588 #0000FF +1 0 #FF0000 +EOF + +gmt psxy -R-0.5/2.5/-0.5/2 -JX4c -Bafg -BWSne+t"Pentagon Slash RGB" -G+g -O -X5c << EOF >> $ps +1 0 255/0/0 +1.951 0.588 255/165/0 +1.588 1.538 255/255/0 +0.412 1.538 0/255/0 +0.049 0.588 0/0/255 +1 0 255/0/0 +EOF + +\rm t.cpt \ No newline at end of file diff --git a/test/psxyz/gradients3d.sh b/test/psxyz/gradients3d.sh new file mode 100644 index 00000000000..c0fbd278fd5 --- /dev/null +++ b/test/psxyz/gradients3d.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Test gradient-filled 3D polygons with -G+g (Gouraud shading) +# Tests RGB format (x y z r g b), color names (x y z colorname), +# and CPT format (x y z value) + +ps=gradients3d.ps + +# Create CPT for CPT-based tests +gmt makecpt -Chot -T0/100 > t.cpt + +# Row 1: RGB format - triangle and square at z=0 +gmt psxyz -R-1/4/-1/3/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"RGB Triangle" -G+g -P -K -X1.5 -Y19c << EOF > $ps +0 0 0 255 0 0 +3 0 0 0 255 0 +1.5 2.6 0 0 0 255 +0 0 0 255 0 0 +EOF + +gmt psxyz -R-0.5/2.5/-0.5/2.5/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"RGB Square" -G+g -O -K -X9.5c << EOF >> $ps +0 0 1 255 0 0 +2 0 1 255 255 0 +2 2 1 0 255 0 +0 2 1 0 0 255 +0 0 1 255 0 0 +EOF + +# Row 2: Color names - triangle and square at different z levels +gmt psxyz -R-1/4/-1/3/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"Color Names Triangle" -G+g -O -K -X-9.5c -Y-8.5c << EOF >> $ps +0 0 0 red +3 0 0.5 green +1.5 2.6 1 blue +0 0 0 red +EOF + +gmt psxyz -R-0.5/2.5/-0.5/2.5/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"Color Names Square" -G+g -O -K -X9.5c << EOF >> $ps +0 0 0.5 red +2 0 1 yellow +2 2 1.5 green +0 2 1 blue +0 0 0.5 red +EOF + +# Row 3: CPT format - triangle and square +gmt psxyz -R-1/4/-1/3/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"CPT Triangle" -G+g -Ct.cpt -O -K -X-9.5c -Y-8.5c << EOF >> $ps +0 0 0 0 +3 0 1 50 +1.5 2.6 2 100 +0 0 0 0 +EOF + +gmt psxyz -R-0.5/2.5/-0.5/2.5/0/3 -JX6c -JZ4c -p135/30 -Bxafg -Byafg -Bzafg -BWSneZ+t"CPT Square" -G+g -Ct.cpt -O -X9.5c << EOF >> $ps +0 0 0.5 0 +2 0 1 33 +2 2 1.5 67 +0 2 1 100 +0 0 0.5 0 +EOF + +\rm t.cpt