From 7c1d2e80abfb5565428494a630b308d5ad2bd5e6 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Sun, 21 Dec 2025 23:07:07 +0000 Subject: [PATCH 1/6] Add the so wished option to plot spheres in psxyz. They aren't really spheres but circles in 3D plots. This is mostly a Claude.ai creation but a one that needed a lot of baby-sitting development. --- doc/rst/source/explain_3D_symbols.rst_ | 14 +++++ src/PSL_prologue.ps | Bin 5832 -> 7162 bytes src/PSL_strings.h | 29 +++++++++ src/gmt_init.c | 78 ++++++++++++++++++++++++- src/gmt_plot.h | 7 +++ src/postscriptlight.c | 1 + src/postscriptlight.h | 1 + src/psxy.c | 2 +- src/psxyz.c | 30 +++++++++- 9 files changed, 159 insertions(+), 3 deletions(-) diff --git a/doc/rst/source/explain_3D_symbols.rst_ b/doc/rst/source/explain_3D_symbols.rst_ index 5c064a3521a..5c16c4b3c28 100644 --- a/doc/rst/source/explain_3D_symbols.rst_ +++ b/doc/rst/source/explain_3D_symbols.rst_ @@ -31,3 +31,17 @@ **q** if *size* is a quantity in x-units [Default is plot-distance units]. The facet colors will be modified to simulate shading. Use **-SU** to disable 3-D illumination. + + **-SQ**\ *size*\ [**c**\|\ **i**\|\ **p**][**+a**\ *azimuth*][**+e**\ *elevation*][**+f**][**+n**] + Sphere (3-D) with diameter *size* [Default unit is **c** (cm)]. + The sphere is rendered with a radial gradient from white at the light source + to the fill color (**-G**) at the opposite side, creating a 3-D appearance. + Modifiers: + + - **+a**\ *azimuth* - Set light source azimuth [default: 0, from the right]. + - **+e**\ *elevation* - Set light source elevation [default: 90, perpendicular to viewing plane]. + - **+f** - Use flat/constant fill color (no gradient shading). + - **+n** - Draw outline only (no fill). + + The outline pen is controlled by **-W**. If no fill color is specified via **-G**, + the sphere defaults to black. diff --git a/src/PSL_prologue.ps b/src/PSL_prologue.ps index 1b5eac1aa6612a265f92925330898ae12e8e65b5..cdf21ab4de59125b31bf195784407caaccccd038 100644 GIT binary patch delta 1301 zcmZux!H&}~5ET-qiX#G!!(pXGf_2JDAkb6gFo3u2k2mn7{B{?;)ak;mpEO&?x-h}3w1E$$Qwn#W`I*gF3 zB+K*vVIeR?24Je`gH7NWIgY=cJbm%WC@OR!RRNy(s?uBy+4l}a_dP_2e%bcuxGJHj za^N*vL)g$b2K`eRAba3JI@@br4;pcrCbECnBQI$E>oy^WV;TlfS%5M4yp?Nf9g zcRo}w=x{{D)$n-B{zSCPgwdmpUG1jtCve{jh37zH;@TvC%|OP-yjla#o-g?7R>90$vbV@TbwPzq|k;6 zgRopGI2RI9+a(vqCb5qj3EzjC3vVu91`)o8lCdc_GYno|> >>\n" +" >> matrix makepattern setcolor fill\n" +" } ifelse\n" +" } ifelse\n" +" grestore}!\n" "% Rectangle: height width xc yc\n" "/Sr {M dup -2 div 2 index -2 div G dup 0 D exch 0 exch D neg 0 D FO}!\n" "% Rounded rectangle: height width radius xc yc\n" diff --git a/src/gmt_init.c b/src/gmt_init.c index a6d7a576cc2..364f757f31f 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -17118,7 +17118,7 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL unsigned int ju, col; char symbol_type, txt_a[GMT_LEN256] = {""}, txt_b[GMT_LEN256] = {""}, txt_c[GMT_LEN256] = {""}, txt_d[GMT_LEN256] = {""}; char text_cp[GMT_LEN256] = {""}, diameter[GMT_LEN32] = {""}, *c = NULL; - static char *allowed_symbols[2] = {"~=-+AaBbCcDdEefGgHhIiJjMmNnpqRrSsTtVvWwxy", "=-+AabCcDdEefGgHhIiJjMmNnOopqRrSsTtUuVvWwxy"}; + static char *allowed_symbols[2] = {"~=-+AaBbCcDdEefGgHhIiJjMmNnpQqRrSsTtVvWwxy", "=-+AabCcDdEefGgHhIiJjMmNnOopQqRrSsTtUuVvWwxy"}; static char *bar_symbols[2] = {"Bb", "-BbOoUu"}; if (cmd) { p->base = GMT->session.d_NaN; @@ -17213,6 +17213,28 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL } } } + else if (text[0] == 'Q') { /* Sphere symbol with optional modifiers for light position, flat color, or no fill */ + char arg[GMT_LEN64] = {""}; + n = sscanf (text, "%c%[^+]", &symbol_type, arg); /* arg should be symbol size with no + at the end */ + if (n == 1) { /* No modifiers or no size given */ + if (text[1] && text[1] != '+') { + /* Gave size without modifiers */ + strncpy (arg, &text[1], GMT_LEN64-1); + } + } + if (arg[0] && arg[0] != '+') { /* Need to get size */ + if (cmd) p->read_size_cmd = false; + p->size_x = p->given_size_x = gmt_M_to_inch (GMT, arg); + check = false; + } + else if (!text[1] || text[1] == '+') { /* No size given */ + if (p->size_x == 0.0) p->size_x = p->given_size_x; + if (p->size_y == 0.0) p->size_y = p->given_size_y; + if (p->size_x == 0.0) + col_off++; + if (cmd) p->read_size_cmd = true; + } + } else if (strchr (GMT_VECTOR_CODES, text[0])) { /* Vectors gets separate treatment because of optional modifiers [+j+b+e+s+l+r+a+n] */ int one; @@ -17831,6 +17853,60 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL check = false; } break; + case 'Q': /* Sphere symbol: -SQ[+a][+e][+f][+n] */ + p->symbol = PSL_SPHERE; + /* Set default light position: center (perpendicular to viewing plane) */ + p->SQ_lx = 0.0; + p->SQ_ly = 0.0; + p->SQ_light_set = false; + p->SQ_flat = false; + p->SQ_no_fill = false; + /* Process +a, +e, +f, and +n modifiers */ + { + double azimuth = 0.0, elevation = 90.0; /* Default values: centered light */ + bool got_azim = false, got_elev = false; + char mod[GMT_LEN64] = {""}; + unsigned int pos = 0, error = 0; + if ((c = strchr(text, '+'))) { /* Got modifiers */ + while (gmt_getmodopt(GMT, 'S', c, "aefn", &pos, mod, &error) && error == 0) { + switch (mod[0]) { + case 'a': /* Azimuth */ + if (mod[1]) { + azimuth = atof(&mod[1]); + got_azim = true; + } + else { + GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SQ: +a modifier requires azimuth value\n"); + decode_error++; + } + break; + case 'e': /* Elevation */ + if (mod[1]) { + elevation = atof(&mod[1]); + got_elev = true; + } + else { + GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SQ: +e modifier requires elevation value\n"); + decode_error++; + } + break; + case 'f': /* Flat/constant color (no gradient) */ + p->SQ_flat = true; + break; + case 'n': /* No fill (outline only) */ + p->SQ_no_fill = true; + break; + } + } + if (got_azim || got_elev) { /* Convert azimuth/elevation to x,y light position */ + double radius = cosd(elevation); + p->SQ_lx = radius * sind(azimuth); + p->SQ_ly = radius * cosd(azimuth); + p->SQ_light_set = true; + } + } + } + break; case 'q': /* Quoted lines: -Sq[d|n|l|s|x][:] */ p->symbol = GMT_SYMBOL_QUOTED_LINE; check = false; diff --git a/src/gmt_plot.h b/src/gmt_plot.h index 67b87cdc90b..1505fb96dfc 100644 --- a/src/gmt_plot.h +++ b/src/gmt_plot.h @@ -190,6 +190,13 @@ struct GMT_SYMBOL { struct GMT_FRONTLINE f; /* parameters needed for a front */ struct GMT_CUSTOM_SYMBOL *custom; /* pointer to a custom symbol */ + /* These apply to sphere symbols */ + double SQ_lx; /* Light source x position for sphere symbol [0.0] */ + double SQ_ly; /* Light source y position for sphere symbol [0.0] */ + bool SQ_light_set; /* true if +a or +e modifier was used to set light position */ + bool SQ_flat; /* true if +f modifier was used to disable gradient (flat/solid color) */ + bool SQ_no_fill; /* true if +n modifier was used to draw outline only (no fill) */ + struct GMT_CONTOUR G; /* For quoted lines */ struct GMT_DECORATE D; /* For decorated lines */ }; diff --git a/src/postscriptlight.c b/src/postscriptlight.c index 8c5eb8c5b20..62266c37c9b 100644 --- a/src/postscriptlight.c +++ b/src/postscriptlight.c @@ -3990,6 +3990,7 @@ int PSL_plotsymbol (struct PSL_CTRL *PSL, double x, double y, double size[], int case PSL_INVTRIANGLE: /* Inverted triangle */ case PSL_OCTAGON: /* Octagon */ case PSL_PENTAGON: /* Pentagon */ + case PSL_SPHERE: /* Sphere */ case PSL_SQUARE: /* Square */ case PSL_TRIANGLE: /* Triangle */ PSL_command (PSL, "%d %d %d S%c\n", psl_iz (PSL, 0.5 * size[0]), psl_ix (PSL, x), psl_iy (PSL, y), (char)symbol); diff --git a/src/postscriptlight.h b/src/postscriptlight.h index 1b5132e6f26..94c2ea34abe 100644 --- a/src/postscriptlight.h +++ b/src/postscriptlight.h @@ -69,6 +69,7 @@ extern "C" { #define PSL_MARC ((int)'m') #define PSL_PENTAGON ((int)'n') #define PSL_DOT ((int)'p') +#define PSL_SPHERE ((int)'Q') #define PSL_RECT ((int)'r') #define PSL_RNDRECT ((int)'R') #define PSL_SQUARE ((int)'s') diff --git a/src/psxy.c b/src/psxy.c index d15274efb01..c8c88223aab 100644 --- a/src/psxy.c +++ b/src/psxy.c @@ -616,7 +616,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { GMT_Usage (API, 2, "\n%s Basic geometric symbol. Append one:", GMT_LINE_BULLET); GMT_Usage (API, -3, "-(xdash), +(plus), st(a)r, (b|B)ar, (c)ircle, (d)iamond, (e)llipse, " "(f)ront, octa(g)on, (h)exagon, (i)nvtriangle, (j)rotated rectangle, " - "(k)ustom, (l)etter, (m)athangle, pe(n)tagon, (p)oint, (q)uoted line, (r)ectangle, " + "(k)ustom, (l)etter, (m)athangle, pe(n)tagon, (p)oint, s(Q)here, (q)uoted line, (r)ectangle, " "(R)ounded rectangle, (s)quare, (t)riangle, (v)ector, (w)edge, (x)cross, (y)dash, or " "=(geovector, i.e., great or small circle vectors) or ~(decorated line)."); GMT_Usage (API, -3, "If no size is specified, then the 3rd column must have sizes. " diff --git a/src/psxyz.c b/src/psxyz.c index 5424afeb9a7..075491324d1 100644 --- a/src/psxyz.c +++ b/src/psxyz.c @@ -238,7 +238,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { GMT_Usage (API, -3, "-(xdash), +(plus), st(a)r, (b|B)ar, (c)ircle, (d)iamond, (e)llipse, " "(f)ront, octa(g)on, (h)exagon (i)nvtriangle, (j)rotated rectangle, " "(k)ustom, (l)etter, (m)athangle, pe(n)tagon, c(o)lumn, (p)oint, " - "(q)uoted line, (r)ectangle, (R)ounded rectangle, (s)quare, (t)riangle, " + "s(Q)here, (q)uoted line, (r)ectangle, (R)ounded rectangle, (s)quare, (t)riangle, " "c(u)be, (v)ector, (w)edge, (x)cross, (y)dash, (z)dash, or " "=(geovector, i.e., great or small circle vectors)."); @@ -276,6 +276,13 @@ static int usage (struct GMTAPI_CTRL *API, int level) { GMT_Usage (API, 2, "\n%s 3-D Cube: Give as the length of all sides; append q if " "is a quantity in x-units.", GMT_LINE_BULLET); + GMT_Usage (API, 2, "\n%s 3-D Sphere: Give as sphere diameter [Default unit is cm]. " + "Sphere is rendered with radial gradient shading from white at light source to fill color. Modifiers:", GMT_LINE_BULLET); + GMT_Usage (API, 3, "+a Set light source azimuth [0, from the right]."); + GMT_Usage (API, 3, "+e Set light source elevation [90, perpendicular to viewing plane]."); + GMT_Usage (API, 3, "+f Use flat/constant fill color (no gradient shading)."); + GMT_Usage (API, 3, "+n Draw outline only (no fill). Outline color from -W."); + GMT_Usage (API, 2, "\n%s Ellipse: If not given, we read direction, major, and minor axis from columns 4-6. " "If -SE rather than -Se is selected, %s will expect azimuth, and " "axes [in km], and convert azimuths based on map projection. " @@ -1048,6 +1055,10 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { fill_active = Ctrl->G.active; /* Make copies because we will change the values */ outline_active = Ctrl->W.active; if (not_line && !outline_active && S.symbol != PSL_WEDGE && !fill_active && !get_rgb && !QR_symbol) outline_active = true; /* If no fill nor outline for symbols then turn outline on */ + if (S.symbol == PSL_SPHERE && !fill_active && !S.SQ_no_fill) { /* Sphere needs a default fill for 3D shading */ + current_fill.rgb[0] = current_fill.rgb[1] = current_fill.rgb[2] = 0.5; /* Black */ + fill_active = true; + } if (Ctrl->D.active) { /* Shift the plot a bit. This is a bit frustrating, since the only way to do this @@ -1892,6 +1903,23 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { gmt_plane_perspective (GMT, GMT_Z, data[i].z); PSL_plotsymbol (PSL, xpos[item], data[i].y, data[i].dim, data[i].symbol); break; + case PSL_SPHERE: /* Case created by Claude.ai */ + gmt_plane_perspective(GMT, GMT_Z, data[i].z); + if (S.SQ_light_set) /* Output custom light position for sphere */ + PSL_command(PSL, "/SQ_lx %.12g def /SQ_ly %.12g def\n", S.SQ_lx, S.SQ_ly); + if (S.SQ_flat) /* Output flat/constant color flag for sphere */ + PSL_command(PSL, "/SQ_flat true def\n"); + if (S.SQ_no_fill) /* Output no-fill flag for sphere */ + PSL_command(PSL, "/SQ_no_fill true def\n"); + PSL_plotsymbol(PSL, xpos[item], data[i].y, data[i].dim, data[i].symbol); + /* Draw outline circle in user space using pen color */ + if (data[i].outline) { + /* The 1/4 factor was obtained by trial-and-error. Couldn't find the true logic. (JL) */ + PSL_command(PSL, "V /DeviceRGB setcolorspace matrix setmatrix %s " + "xc_SQ_user yc_SQ_user T N 0 0 radius_SQ_user 0 360 arc S U\n", + PSL_makepen(PSL, (1.0/4.0) * data[i].p.width, data[i].p.rgb, data[i].p.style, data[i].p.offset)); + } + break; case PSL_ELLIPSE: gmt_plane_perspective (GMT, GMT_Z, data[i].z); if (data[i].flag & 2) From ed7409f98d56a1bf57ef14b8db25f8a9efedab41 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Tue, 23 Dec 2025 00:10:10 +0000 Subject: [PATCH 2/6] Change -SQ to -SP for sphere plots. Add a sphere.sh test --- doc/rst/source/explain_3D_symbols.rst_ | 2 +- src/PSL_prologue.ps | Bin 7162 -> 7166 bytes src/PSL_strings.h | 28 ++++++++++++------------ src/gmt_init.c | 29 ++++++++++++------------- src/gmt_plot.h | 10 ++++----- src/postscriptlight.c | 5 ++++- src/postscriptlight.h | 2 +- src/psxyz.c | 16 +++++++------- test/psxyz/sphere.sh | 6 +++++ 9 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 test/psxyz/sphere.sh diff --git a/doc/rst/source/explain_3D_symbols.rst_ b/doc/rst/source/explain_3D_symbols.rst_ index 5c16c4b3c28..a441f2ec3b4 100644 --- a/doc/rst/source/explain_3D_symbols.rst_ +++ b/doc/rst/source/explain_3D_symbols.rst_ @@ -32,7 +32,7 @@ The facet colors will be modified to simulate shading. Use **-SU** to disable 3-D illumination. - **-SQ**\ *size*\ [**c**\|\ **i**\|\ **p**][**+a**\ *azimuth*][**+e**\ *elevation*][**+f**][**+n**] + **-SP**\ *size*\ [**c**\|\ **i**\|\ **p**][**+a**\ *azimuth*][**+e**\ *elevation*][**+f**][**+n**] Sphere (3-D) with diameter *size* [Default unit is **c** (cm)]. The sphere is rendered with a radial gradient from white at the light source to the fill color (**-G**) at the opposite side, creating a 3-D appearance. diff --git a/src/PSL_prologue.ps b/src/PSL_prologue.ps index cdf21ab4de59125b31bf195784407caaccccd038..1a05ccec8a7a5315a9ab08dd43a3c2bccd46f555 100644 GIT binary patch delta 507 zcmexm{?B}aHJ4XFMru*2LbZNna(r-rLTXBCnu2}>61yldC9|{`CdZ|qpdTC%pO%wY zqEHUeS&&~)3)CMRFu9RShB09BJT3_!xt&XsJwO3$=wwC~vB`hA__YG!ON&#BknKPS z01ZaiWB}A6J~@DgYjQZZdOeahsP#D&NS1>glv5F(UzC}y0CXEr66^^Ovl1a%2@-`k z6sQCuZlGs?p%yAutB{$Nnp2$0r7-y~Hzz054M0!w^N1qxbb0iJ!1f^d45%oNM+F!L zlX=2`S5T+mq`8yP}*MSP+#cWSO^yEbl71oH?CD>gJY@4!au%YDa0|fCk3LbqR zLBt2}AWn9hX1$n&Fnl}n{p|ci-_h#WArzKjdB4megpDmd1rg-tUiQYqo_fh!KnSvIRs1!v!!gKY|0uI_E^9?exU%|)gr zU_7uQ=gKzH0Z851Avis#sXOBIPX}r+xv+E_$B3{@rT1Vib;_nJJG3+*8jqW9a!49f zXMT5w`)ehuV^YY@lktHWM_$<1>{b?pmABWU^^13l|1a-kxTe!@VP> >>\n" +" /Coords [SP_lx_user SP_ly_user 0 0 0 radius_SP_user] /Function <<\n" +" /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [R_SP G_SP B_SP] /N 1 >> >>\n" " >> matrix makepattern setcolor fill\n" " } ifelse\n" " } ifelse\n" diff --git a/src/gmt_init.c b/src/gmt_init.c index 364f757f31f..0606438e7ef 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -17213,7 +17213,7 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL } } } - else if (text[0] == 'Q') { /* Sphere symbol with optional modifiers for light position, flat color, or no fill */ + else if (text[0] == 'P') { /* Sphere symbol with optional modifiers for light position, flat color, or no fill */ char arg[GMT_LEN64] = {""}; n = sscanf (text, "%c%[^+]", &symbol_type, arg); /* arg should be symbol size with no + at the end */ if (n == 1) { /* No modifiers or no size given */ @@ -17845,7 +17845,6 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL GMT_Report (GMT->parent, GMT_MSG_ERROR, "Option -S: Symbol type %c is 3-D only\n", symbol_type); } break; - case 'P': case 'p': p->symbol = PSL_DOT; if (p->size_x == 0.0 && !p->read_size) { /* User forgot to set size */ @@ -17853,14 +17852,14 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL check = false; } break; - case 'Q': /* Sphere symbol: -SQ[+a][+e][+f][+n] */ + case 'P': /* Sphere symbol: -SP[+a][+e][+f][+n] */ p->symbol = PSL_SPHERE; /* Set default light position: center (perpendicular to viewing plane) */ - p->SQ_lx = 0.0; - p->SQ_ly = 0.0; - p->SQ_light_set = false; - p->SQ_flat = false; - p->SQ_no_fill = false; + p->SP_lx = 0.0; + p->SP_ly = 0.0; + p->SP_light_set = false; + p->SP_flat = false; + p->SP_no_fill = false; /* Process +a, +e, +f, and +n modifiers */ { double azimuth = 0.0, elevation = 90.0; /* Default values: centered light */ @@ -17876,7 +17875,7 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL got_azim = true; } else { - GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SQ: +a modifier requires azimuth value\n"); + GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SP: +a modifier requires azimuth value\n"); decode_error++; } break; @@ -17886,23 +17885,23 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL got_elev = true; } else { - GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SQ: +e modifier requires elevation value\n"); + GMT_Report(GMT->parent, GMT_MSG_ERROR, "Option -SP: +e modifier requires elevation value\n"); decode_error++; } break; case 'f': /* Flat/constant color (no gradient) */ - p->SQ_flat = true; + p->SP_flat = true; break; case 'n': /* No fill (outline only) */ - p->SQ_no_fill = true; + p->SP_no_fill = true; break; } } if (got_azim || got_elev) { /* Convert azimuth/elevation to x,y light position */ double radius = cosd(elevation); - p->SQ_lx = radius * sind(azimuth); - p->SQ_ly = radius * cosd(azimuth); - p->SQ_light_set = true; + p->SP_lx = radius * sind(azimuth); + p->SP_ly = radius * cosd(azimuth); + p->SP_light_set = true; } } } diff --git a/src/gmt_plot.h b/src/gmt_plot.h index 1505fb96dfc..74415a54735 100644 --- a/src/gmt_plot.h +++ b/src/gmt_plot.h @@ -191,11 +191,11 @@ struct GMT_SYMBOL { struct GMT_CUSTOM_SYMBOL *custom; /* pointer to a custom symbol */ /* These apply to sphere symbols */ - double SQ_lx; /* Light source x position for sphere symbol [0.0] */ - double SQ_ly; /* Light source y position for sphere symbol [0.0] */ - bool SQ_light_set; /* true if +a or +e modifier was used to set light position */ - bool SQ_flat; /* true if +f modifier was used to disable gradient (flat/solid color) */ - bool SQ_no_fill; /* true if +n modifier was used to draw outline only (no fill) */ + double SP_lx; /* Light source x position for sphere symbol [0.0] */ + double SP_ly; /* Light source y position for sphere symbol [0.0] */ + bool SP_light_set; /* true if +a or +e modifier was used to set light position */ + bool SP_flat; /* true if +f modifier was used to disable gradient (flat/solid color) */ + bool SP_no_fill; /* true if +n modifier was used to draw outline only (no fill) */ struct GMT_CONTOUR G; /* For quoted lines */ struct GMT_DECORATE D; /* For decorated lines */ diff --git a/src/postscriptlight.c b/src/postscriptlight.c index 62266c37c9b..57e7c814290 100644 --- a/src/postscriptlight.c +++ b/src/postscriptlight.c @@ -3990,12 +3990,15 @@ int PSL_plotsymbol (struct PSL_CTRL *PSL, double x, double y, double size[], int case PSL_INVTRIANGLE: /* Inverted triangle */ case PSL_OCTAGON: /* Octagon */ case PSL_PENTAGON: /* Pentagon */ - case PSL_SPHERE: /* Sphere */ case PSL_SQUARE: /* Square */ case PSL_TRIANGLE: /* Triangle */ PSL_command (PSL, "%d %d %d S%c\n", psl_iz (PSL, 0.5 * size[0]), psl_ix (PSL, x), psl_iy (PSL, y), (char)symbol); break; + case PSL_SPHERE: /* Sphere */ + PSL_command (PSL, "%d %d %d SPhere\n", psl_iz (PSL, 0.5 * size[0]), psl_ix (PSL, x), psl_iy (PSL, y)); + break; + /* Multi-parameter fillable symbols */ case PSL_WEDGE: /* A wedge or pie-slice. size[0] = radius, size[1..2] = azimuth range of arc */ diff --git a/src/postscriptlight.h b/src/postscriptlight.h index 94c2ea34abe..067e2b613c5 100644 --- a/src/postscriptlight.h +++ b/src/postscriptlight.h @@ -69,7 +69,7 @@ extern "C" { #define PSL_MARC ((int)'m') #define PSL_PENTAGON ((int)'n') #define PSL_DOT ((int)'p') -#define PSL_SPHERE ((int)'Q') +#define PSL_SPHERE ((int)'P') #define PSL_RECT ((int)'r') #define PSL_RNDRECT ((int)'R') #define PSL_SQUARE ((int)'s') diff --git a/src/psxyz.c b/src/psxyz.c index 075491324d1..f1f4183a616 100644 --- a/src/psxyz.c +++ b/src/psxyz.c @@ -1055,7 +1055,7 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { fill_active = Ctrl->G.active; /* Make copies because we will change the values */ outline_active = Ctrl->W.active; if (not_line && !outline_active && S.symbol != PSL_WEDGE && !fill_active && !get_rgb && !QR_symbol) outline_active = true; /* If no fill nor outline for symbols then turn outline on */ - if (S.symbol == PSL_SPHERE && !fill_active && !S.SQ_no_fill) { /* Sphere needs a default fill for 3D shading */ + if (S.symbol == PSL_SPHERE && !fill_active && !S.SP_no_fill) { /* Sphere needs a default fill for 3D shading */ current_fill.rgb[0] = current_fill.rgb[1] = current_fill.rgb[2] = 0.5; /* Black */ fill_active = true; } @@ -1905,18 +1905,18 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { break; case PSL_SPHERE: /* Case created by Claude.ai */ gmt_plane_perspective(GMT, GMT_Z, data[i].z); - if (S.SQ_light_set) /* Output custom light position for sphere */ - PSL_command(PSL, "/SQ_lx %.12g def /SQ_ly %.12g def\n", S.SQ_lx, S.SQ_ly); - if (S.SQ_flat) /* Output flat/constant color flag for sphere */ - PSL_command(PSL, "/SQ_flat true def\n"); - if (S.SQ_no_fill) /* Output no-fill flag for sphere */ - PSL_command(PSL, "/SQ_no_fill true def\n"); + if (S.SP_light_set) /* Output custom light position for sphere */ + PSL_command(PSL, "/SP_lx %.12g def /SP_ly %.12g def\n", S.SP_lx, S.SP_ly); + if (S.SP_flat) /* Output flat/constant color flag for sphere */ + PSL_command(PSL, "/SP_flat true def\n"); + if (S.SP_no_fill) /* Output no-fill flag for sphere */ + PSL_command(PSL, "/SP_no_fill true def\n"); PSL_plotsymbol(PSL, xpos[item], data[i].y, data[i].dim, data[i].symbol); /* Draw outline circle in user space using pen color */ if (data[i].outline) { /* The 1/4 factor was obtained by trial-and-error. Couldn't find the true logic. (JL) */ PSL_command(PSL, "V /DeviceRGB setcolorspace matrix setmatrix %s " - "xc_SQ_user yc_SQ_user T N 0 0 radius_SQ_user 0 360 arc S U\n", + "xc_SP_user yc_SP_user T N 0 0 radius_SP_user 0 360 arc S U\n", PSL_makepen(PSL, (1.0/4.0) * data[i].p.width, data[i].p.rgb, data[i].p.style, data[i].p.offset)); } break; diff --git a/test/psxyz/sphere.sh b/test/psxyz/sphere.sh new file mode 100644 index 00000000000..e7ab9ec3090 --- /dev/null +++ b/test/psxyz/sphere.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Test -SP sphere symbol +ps=sphere.ps +echo 3 3 3 | gmt psxyz -R0/6/0/6/0/6 -JX10c -JZ4c -p135/45 -SP2c -Gblue -W0.2p,red -Ba -Bz -P -K > $ps +echo 5 2 4 | gmt psxyz -R -J -JZ -p -SP1.5c -Ggreen -W0.5p -O -K >> $ps +echo 2 5 2 | gmt psxyz -R -J -JZ -p -SP1c -Gred -W0.3p,black -O >> $ps From d64744ebb80b4c8aed804463cac207396b91bb9d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 25 Dec 2025 17:02:47 +0800 Subject: [PATCH 3/6] Add the baseline image sphere.ps to dvc --- test/baseline/psxyz.dvc | 5 +++-- test/psxyz/sphere.sh | 0 2 files changed, 3 insertions(+), 2 deletions(-) mode change 100644 => 100755 test/psxyz/sphere.sh diff --git a/test/baseline/psxyz.dvc b/test/baseline/psxyz.dvc index c5c7f7df0d0..b6d797b5191 100644 --- a/test/baseline/psxyz.dvc +++ b/test/baseline/psxyz.dvc @@ -1,5 +1,6 @@ outs: -- md5: e8300f3b8e45690deb50c78d03d3e548.dir - nfiles: 28 +- md5: d294c350ed35533a2b8baf4729953c9d.dir + nfiles: 29 path: psxyz hash: md5 + size: 1205108 diff --git a/test/psxyz/sphere.sh b/test/psxyz/sphere.sh old mode 100644 new mode 100755 From ed1f289ead998d3ed709a628ad7cf6305e257abf Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 25 Dec 2025 11:47:27 +0000 Subject: [PATCH 4/6] Update src/psxyz.c Co-authored-by: Dongdong Tian --- src/psxyz.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psxyz.c b/src/psxyz.c index f1f4183a616..f0b0ab378b1 100644 --- a/src/psxyz.c +++ b/src/psxyz.c @@ -238,7 +238,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { GMT_Usage (API, -3, "-(xdash), +(plus), st(a)r, (b|B)ar, (c)ircle, (d)iamond, (e)llipse, " "(f)ront, octa(g)on, (h)exagon (i)nvtriangle, (j)rotated rectangle, " "(k)ustom, (l)etter, (m)athangle, pe(n)tagon, c(o)lumn, (p)oint, " - "s(Q)here, (q)uoted line, (r)ectangle, (R)ounded rectangle, (s)quare, (t)riangle, " + "s(P)here, (q)uoted line, (r)ectangle, (R)ounded rectangle, (s)quare, (t)riangle, " "c(u)be, (v)ector, (w)edge, (x)cross, (y)dash, (z)dash, or " "=(geovector, i.e., great or small circle vectors)."); From 3cc5a9fda9b1e011e037bd1230b6e5772e5342d2 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 25 Dec 2025 16:12:07 +0000 Subject: [PATCH 5/6] Fixes and updates to this test. --- test/psxyz/sphere.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/psxyz/sphere.sh b/test/psxyz/sphere.sh index e7ab9ec3090..71685783d5f 100755 --- a/test/psxyz/sphere.sh +++ b/test/psxyz/sphere.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Test -SP sphere symbol ps=sphere.ps -echo 3 3 3 | gmt psxyz -R0/6/0/6/0/6 -JX10c -JZ4c -p135/45 -SP2c -Gblue -W0.2p,red -Ba -Bz -P -K > $ps -echo 5 2 4 | gmt psxyz -R -J -JZ -p -SP1.5c -Ggreen -W0.5p -O -K >> $ps -echo 2 5 2 | gmt psxyz -R -J -JZ -p -SP1c -Gred -W0.3p,black -O >> $ps +echo 3 3 3 | gmt psxyz -R0/6/0/6/0/6 -JX10c -JZ4c -p135/45 -SP2c -Gblue -W0.2p,red -Ba -Baz -P -K > $ps +echo 5 2 4 | gmt psxyz -R -J -JZ -p -SP1.5c+f -Ggreen -W0.5p -O -K >> $ps +echo 2 5 2 | gmt psxyz -R -J -JZ -p -SP1c+n -Gred -W0.3p,black -O >> $ps From e544ec10bc2f6273d89081cb4abd73bfcbb0d163 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Thu, 25 Dec 2025 20:34:54 +0000 Subject: [PATCH 6/6] Make the -SP+e & +a work better. Oddly the PS seem right but conversion to raster flips spheres upside-down. --- src/PSL_prologue.ps | Bin 7166 -> 7136 bytes src/PSL_strings.h | 2 +- src/gmt_init.c | 8 ++++---- src/gmt_plot.h | 2 ++ src/psxyz.c | 12 ++++++++++-- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/PSL_prologue.ps b/src/PSL_prologue.ps index 1a05ccec8a7a5315a9ab08dd43a3c2bccd46f555..5aa1634cd93dab00890299c2e4103a3dee930558 100644 GIT binary patch delta 51 zcmexo{=j^L0*`=xa6o)cMSN*-YLP-}N^08V|J-7NmGSvSndu6-r8x><`O3|uJe~Xi D`h*hQ delta 65 zcmaE0{?B}a0?%Y79$~J^`23>GbcNi~oXLzV;)*3jiFw6o`9--3`oRJ5IhFCH#i>OK QsVS*x5P^!#;XIxE0P9Q^bpQYW diff --git a/src/PSL_strings.h b/src/PSL_strings.h index 374652f5353..e00bae28cfd 100644 --- a/src/PSL_strings.h +++ b/src/PSL_strings.h @@ -1046,7 +1046,7 @@ static char *PSL_prologue_str = " xc_SP yc_SP matrix currentmatrix transform /yc_SP_user edef /xc_SP_user edef\n" " radius_SP 0 matrix currentmatrix dtransform dup mul exch dup mul add sqrt /radius_SP_user edef\n" " /SP_lx where {pop /SP_lx_orig SP_lx def /SP_ly_orig SP_ly def} {/SP_lx_orig 0.0 def /SP_ly_orig 0.0 def} ifelse\n" -" radius_SP_user SP_lx_orig mul radius_SP_user SP_ly_orig mul matrix currentmatrix dtransform /SP_ly_user edef /SP_lx_user edef\n" +" radius_SP_user SP_lx_orig mul /SP_lx_user edef radius_SP_user SP_ly_orig mul /SP_ly_user edef\n" " matrix setmatrix\n" " SP_no_fill {\n" " % No fill (outline only, drawn separately in C code)\n" diff --git a/src/gmt_init.c b/src/gmt_init.c index 0606438e7ef..5232d166c17 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -17897,10 +17897,10 @@ int gmt_parse_symbol_option (struct GMT_CTRL *GMT, char *text, struct GMT_SYMBOL break; } } - if (got_azim || got_elev) { /* Convert azimuth/elevation to x,y light position */ - double radius = cosd(elevation); - p->SP_lx = radius * sind(azimuth); - p->SP_ly = radius * cosd(azimuth); + if (got_azim || got_elev) { /* Store light azimuth/elevation for later projection */ + /* Store the light direction - will be projected to 2D at drawing time */ + p->SP_light_az = azimuth; + p->SP_light_el = elevation; p->SP_light_set = true; } } diff --git a/src/gmt_plot.h b/src/gmt_plot.h index 74415a54735..0ca10ff1020 100644 --- a/src/gmt_plot.h +++ b/src/gmt_plot.h @@ -193,6 +193,8 @@ struct GMT_SYMBOL { /* These apply to sphere symbols */ double SP_lx; /* Light source x position for sphere symbol [0.0] */ double SP_ly; /* Light source y position for sphere symbol [0.0] */ + double SP_light_az; /* Light source azimuth for sphere symbol [0.0] */ + double SP_light_el; /* Light source elevation for sphere symbol [0.0] */ bool SP_light_set; /* true if +a or +e modifier was used to set light position */ bool SP_flat; /* true if +f modifier was used to disable gradient (flat/solid color) */ bool SP_no_fill; /* true if +n modifier was used to draw outline only (no fill) */ diff --git a/src/psxyz.c b/src/psxyz.c index f0b0ab378b1..967b1e5ee02 100644 --- a/src/psxyz.c +++ b/src/psxyz.c @@ -1905,8 +1905,16 @@ EXTERN_MSC int GMT_psxyz (void *V_API, int mode, void *args) { break; case PSL_SPHERE: /* Case created by Claude.ai */ gmt_plane_perspective(GMT, GMT_Z, data[i].z); - if (S.SP_light_set) /* Output custom light position for sphere */ - PSL_command(PSL, "/SP_lx %.12g def /SP_ly %.12g def\n", S.SP_lx, S.SP_ly); + if (S.SP_light_set) { /* Calculate and output custom light position for sphere */ + /* Simple model: azimuth controls horizontal, elevation controls vertical */ + /* Relative to viewing direction */ + double dazim = GMT->current.proj.z_project.view_azimuth - S.SP_light_az; + if (dazim > 90) dazim = 90.0; /* Do let illum from the hidden hemisphere */ + if (dazim < -90) dazim = -90.0; + double SP_lx_proj = sind(dazim); + double SP_ly_proj = sind(S.SP_light_el - GMT->current.proj.z_project.view_elevation); + PSL_command(PSL, "/SP_lx %.12g def /SP_ly %.12g def\n", SP_lx_proj, SP_ly_proj); + } if (S.SP_flat) /* Output flat/constant color flag for sphere */ PSL_command(PSL, "/SP_flat true def\n"); if (S.SP_no_fill) /* Output no-fill flag for sphere */