diff --git a/doc/rst/source/changes.rst b/doc/rst/source/changes.rst index 8163c775742..e2f8a502ab4 100644 --- a/doc/rst/source/changes.rst +++ b/doc/rst/source/changes.rst @@ -6,6 +6,15 @@ Changelog ========= +New Features in GMT 6.7 +======================= + +* Add quadratic (**Q**) and cubic (**B**) Bezier curve commands to the custom symbol + macro language, enabling smooth curves in ``.def`` symbol files. Both commands + extend the path from the previous **M** or **D** point; **Q** takes one control point + plus endpoint, **B** takes two control points plus endpoint. See + :ref:`Custom Symbols ` for details. + New Features in GMT 6.6 ======================= diff --git a/doc/rst/source/reference/custom-symbols.rst b/doc/rst/source/reference/custom-symbols.rst index 1e79b13c248..d1b1944669c 100644 --- a/doc/rst/source/reference/custom-symbols.rst +++ b/doc/rst/source/reference/custom-symbols.rst @@ -145,6 +145,8 @@ are constants. +===============+============+========================================+============================================+ | arc | **A** | Append circular arc to existing path | :math:`x_c, y_c, d, \alpha_1, \alpha_2` | +---------------+------------+----------------------------------------+--------------------------------------------+ +| cubicbezier | **B** | Cubic Bezier from last point | :math:`cx_1, cy_1, cx_2, cy_2, ex, ey` | ++---------------+------------+----------------------------------------+--------------------------------------------+ | drawto | **D** | Draw line from previous point | :math:`x, y` | +---------------+------------+----------------------------------------+--------------------------------------------+ | moveto | **M** | Set a new anchor point | :math:`x_0, y_0` | @@ -179,6 +181,8 @@ are constants. +---------------+------------+----------------------------------------+--------------------------------------------+ | pentagon | **n** | Plot a pentagon | :math:`x, y, size` | +---------------+------------+----------------------------------------+--------------------------------------------+ +| quadbezier | **Q** | Quadratic Bezier from last point | :math:`cx, cy, ex, ey` | ++---------------+------------+----------------------------------------+--------------------------------------------+ | rect | **r** | Plot a rectangle | :math:`x, y, width, height` | +---------------+------------+----------------------------------------+--------------------------------------------+ | roundrect | **R** | Plot a rounded rectangle | :math:`x, y, width, height, radius` | @@ -200,6 +204,16 @@ are constants. | plus | **+** | Plot a plus sign | :math:`x, y, size` | +---------------+------------+----------------------------------------+--------------------------------------------+ +Note for **B** and **Q**\: Both Bezier commands extend the current path from the +*last point set by* **M** *or* **D** (the implicit start point :math:`P_0`); you do not +supply :math:`P_0` explicitly. **B** takes two control points followed by the endpoint +(:math:`cx_1, cy_1, cx_2, cy_2, ex, ey`) and draws a cubic Bezier curve. +**Q** takes one control point followed by the endpoint (:math:`cx, cy, ex, ey`) and +draws a quadratic Bezier curve. Both commands are tessellated into line segments +at render time and integrate with the normal fill/stroke pipeline, so **-G** and **-W** +apply as usual. Use **B** when converting SVG paths (SVG ``C``/``c`` maps directly); +use **Q** for simpler arcs where one control point is sufficient. + Note for **O**\: if an **a** is appended to the angle then :math:`\alpha` is considered to be a map azimuth; otherwise it is a Cartesian map angle. The **a** modifier does not apply if the angle is given via a variable, in which case the type of angle diff --git a/share/custom/heart.def b/share/custom/heart.def new file mode 100644 index 00000000000..512ff2bd091 --- /dev/null +++ b/share/custom/heart.def @@ -0,0 +1,10 @@ +# Heart shape using cubic Bezier curves (B command) +# Classic heart: two cubic bezier lobes meeting at bottom tip (0,-0.5) +# and top center notch (0, 0.15) + +# Right lobe: from notch (0,0.15) → top-right → right → bottom tip +0.0 0.15 M + 0.5 0.5 0.5 -0.1 0.0 -0.5 B + +# Left lobe: from bottom tip (0,-0.5) → left → top-left → back to notch +-0.5 -0.1 -0.5 0.5 0.0 0.15 B diff --git a/share/custom/leaf.def b/share/custom/leaf.def new file mode 100644 index 00000000000..eee32b9abee --- /dev/null +++ b/share/custom/leaf.def @@ -0,0 +1,8 @@ +# Leaf shape using quadratic Bezier curves +# Tip at bottom (0,-0.5), apex at top (0,0.5) +# Right lobe: cp=(0.5,-0.2), right side curves out +# Left lobe: cp=(-0.5,-0.2), symmetric + +0.0 -0.5 M + 0.5 0.3 0.0 0.5 Q +-0.5 0.3 0.0 -0.5 Q diff --git a/src/gmt_plot.c b/src/gmt_plot.c index e08b80a8cf7..c82c9d1a5a2 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -7727,6 +7727,22 @@ int gmt_draw_custom_symbol (struct GMT_CTRL *GMT, double x0, double y0, double s gmt_M_free (GMT, yp); break; + case GMT_SYMBOL_QUAD_BEZIER: { /* Quadratic Bezier: cp=(x,y) endpoint=(dim[0],dim[1]) */ + /* P0 = last path point, P1 = (x,y) [control], P2 = (dim[0],dim[1]) [endpoint] */ + double t, mt, x0b = 0.0, y0b = 0.0; + flush = true; + if (n > 0) { x0b = xx[n-1]; y0b = yy[n-1]; } + for (i = 1; i <= GMT_BEZIER_NPTS; i++) { + t = (double)i / GMT_BEZIER_NPTS; + mt = 1.0 - t; + if (n >= n_alloc) gmt_M_malloc2 (GMT, xx, yy, n, &n_alloc, double); + xx[n] = mt*mt*x0b + 2.0*mt*t*x + t*t*dim[0]; + yy[n] = mt*mt*y0b + 2.0*mt*t*y + t*t*dim[1]; + n++; + } + break; + } + case GMT_SYMBOL_ROTATE: /* Rotate the symbol coordinate system by a fixed amount */ if (flush) gmtplot_flush_symbol_piece (GMT, PSL, xx, yy, &n, &p, &f, this_outline, &flush); PSL_setorigin (PSL, 0.0, 0.0, s->p[0], PSL_FWD); @@ -7756,6 +7772,23 @@ int gmt_draw_custom_symbol (struct GMT_CTRL *GMT, double x0, double y0, double s if (s->pen) current_pen = s->pen; break; + case GMT_SYMBOL_CUBIC_BEZIER: { /* Cubic Bezier (B): cp1=(x,y) cp2=(dim[0],dim[1]) endpoint=(dim[2], p[3]*size) */ + /* B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3 */ + double t, mt, mt2, t2, x0b = 0.0, y0b = 0.0; + double cx2 = dim[0], cy2 = dim[1], ex = dim[2], ey = s->p[3] * size[0]; + flush = true; + if (n > 0) { x0b = xx[n-1]; y0b = yy[n-1]; } + for (i = 1; i <= GMT_BEZIER_NPTS; i++) { + t = (double)i / GMT_BEZIER_NPTS; + mt = 1.0 - t; mt2 = mt * mt; t2 = t * t; + if (n >= n_alloc) gmt_M_malloc2(GMT, xx, yy, n, &n_alloc, double); + xx[n] = mt2*mt*x0b + 3.0*mt2*t*x + 3.0*mt*t2*cx2 + t2*t*ex; + yy[n] = mt2*mt*y0b + 3.0*mt2*t*y + 3.0*mt*t2*cy2 + t2*t*ey; + n++; + } + break; + } + case (int)'C': if (gmt_M_compat_check (GMT, 4)) { /* Warn and purposefully fall through to assign the rest of the statements */ GMT_Report (GMT->parent, GMT_MSG_COMPAT, "Circle macro symbol C is deprecated; use c instead\n"); diff --git a/src/gmt_plot.h b/src/gmt_plot.h index eacf9adec99..33ea45fd5e4 100644 --- a/src/gmt_plot.h +++ b/src/gmt_plot.h @@ -54,6 +54,8 @@ #define GMT_SYMBOL_GEOVECTOR ((int)'=') #define GMT_SYMBOL_VARTEXT ((int)'L') #define GMT_SYMBOL_EPS ((int)'P') +#define GMT_SYMBOL_QUAD_BEZIER ((int)'Q') /* Quadratic Bezier: cp=(x,y) endpoint=(p[0],p[1]) */ +#define GMT_SYMBOL_CUBIC_BEZIER ((int)'B') /* Cubic Bezier: cp1=(x,y) cp2=(p[0],p[1]) endpoint=(p[2],p[3]) */ #define GMT_SYMBOL_LINE 0 #define GMT_SYMBOL_NONE ((int)' ') @@ -61,6 +63,8 @@ #define GMT_DOT_SIZE 0.005 /* Size of a "dot" on a GMT PS map [in inches] */ +#define GMT_BEZIER_NPTS 20 /* Tessellation segments for Bezier curve macro commands */ + /*! FRONT symbols */ enum GMT_enum_front {GMT_FRONT_FAULT = 0, diff --git a/src/gmt_support.c b/src/gmt_support.c index 620dabdc134..b065e9937f2 100644 --- a/src/gmt_support.c +++ b/src/gmt_support.c @@ -5784,6 +5784,20 @@ GMT_LOCAL int gmtsupport_init_custom_symbol (struct GMT_CTRL *GMT, char *in_name if ((s->eps = gmtsupport_load_eps_symbol (GMT, name, path)) == NULL) return GMT_RUNTIME_ERROR; break; + case GMT_SYMBOL_QUAD_BEZIER: /* Quadratic Bezier: cx cy ex ey Q */ + if (last != 4) error++; /* Need cx cy ex ey + action = 5 cols */ + s->p[0] = atof (col[2]); /* endpoint x */ + s->p[1] = atof (col[3]); /* endpoint y */ + break; + + case GMT_SYMBOL_CUBIC_BEZIER: /* Cubic Bezier: cx1 cy1 cx2 cy2 ex ey C */ + if (last != 6) error++; /* Need cx1 cy1 cx2 cy2 ex ey + action = 7 cols */ + s->p[0] = atof (col[2]); /* second control point x */ + s->p[1] = atof (col[3]); /* second control point y */ + s->p[2] = atof (col[4]); /* endpoint x */ + s->p[3] = atof (col[5]); /* endpoint y */ + break; + default: error++; break; diff --git a/src/gmt_symbol.h b/src/gmt_symbol.h index ce50e4966e0..3e2885a8609 100644 --- a/src/gmt_symbol.h +++ b/src/gmt_symbol.h @@ -82,7 +82,7 @@ struct GMT_REFPOINT { /* Used to hold items relevant for a reference point */ char *args; /* Text representation of any additional arguments */ }; -#define CUSTOM_SYMBOL_MAXVAR 3 /* So we can check in the code if we exceed this */ +#define CUSTOM_SYMBOL_MAXVAR 4 /* So we can check in the code if we exceed this */ enum gmt_enum_custsymb { GMT_CONST_STRING = -5, /* We have a constant string in a conditional test */ diff --git a/test/baseline/grdmix/grdmix.ps b/test/baseline/grdmix/grdmix.ps new file mode 100644 index 00000000000..4f2b7cb37bd Binary files /dev/null and b/test/baseline/grdmix/grdmix.ps differ diff --git a/test/baseline/psxy/bezier_demo_cubic.ps b/test/baseline/psxy/bezier_demo_cubic.ps new file mode 100644 index 00000000000..18e0cb77114 Binary files /dev/null and b/test/baseline/psxy/bezier_demo_cubic.ps differ diff --git a/test/baseline/psxy/bezier_demo_quadratic.ps b/test/baseline/psxy/bezier_demo_quadratic.ps new file mode 100644 index 00000000000..ec215166503 Binary files /dev/null and b/test/baseline/psxy/bezier_demo_quadratic.ps differ diff --git a/test/psxy/bezier_demo_cubic.sh b/test/psxy/bezier_demo_cubic.sh new file mode 100644 index 00000000000..be98bb835c3 --- /dev/null +++ b/test/psxy/bezier_demo_cubic.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Test C (cubic Bezier) command in custom symbol macro language. +# Uses share/custom/heart.def (closed heart shape via two C curves) +# alongside share/custom/leaf.def (Q-based) for visual comparison. + +ps=bezier_demo_cubic.ps + +gmt psbasemap -R-5/5/-4/4 -JX14c/10c -Ba2f1 -BWeSn+t"Cubic Bezier (C) custom symbol demo" -P -K > $ps + +# Hearts in a row, different sizes and colors +for lon in -4 -2 0 2 4; do + echo "$lon 2" | gmt psxy -R -J -Skheart/1.5c -Gred -W0.5p,darkred -O -K >> $ps + echo "$lon 0" | gmt psxy -R -J -Skheart/2.0c -Gtomato -W0.7p,red -O -K >> $ps + echo "$lon -2" | gmt psxy -R -J -Skheart/2.5c -Gdarkred -W1p,black -O -K >> $ps +done + +# Compare: leaves (Q) vs hearts (C) side by side +for lon in -3.5 -1.5 0.5 2.5; do + echo "$lon 3.3" | gmt psxy -R -J -Skleaf/1.2c -Ggreen -W0.5p,darkgreen -O -K >> $ps +done +for lon in -2.5 -0.5 1.5 3.5; do + echo "$lon 3.3" | gmt psxy -R -J -Skheart/1.2c -Gred -W0.5p,darkred -O -K >> $ps +done + +echo "0 3.8 Q (leaf) vs C (heart)" | gmt pstext -R -J -F+f10p,Helvetica-Bold+jCM -O >> $ps diff --git a/test/psxy/bezier_demo_quadratic.sh b/test/psxy/bezier_demo_quadratic.sh new file mode 100644 index 00000000000..667a0ab06c1 --- /dev/null +++ b/test/psxy/bezier_demo_quadratic.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# Test Q (quadratic Bezier) command in custom symbol macro language. +# Uses share/custom/leaf.def (closed leaf shape from two Q curves) +# and share/custom/qbezier_test.def (open parabolic arc). + +ps=bezier_demo_quadratic.ps + +gmt psbasemap -R-5/5/-4/4 -JX14c/10c -Ba2f1 -BWeSn+t"Quadratic Bezier (Q) custom symbol demo" -P -K > $ps + +# Grid of leaves at different sizes and colors +for lon in -4 -2 0 2 4; do + echo "$lon 2" | gmt psxy -R -J -Skleaf/1.5c -Glightgreen -W0.5p,darkgreen -O -K >> $ps + echo "$lon 0" | gmt psxy -R -J -Skleaf/2.0c -Ggreen -W0.7p,darkgreen -O -K >> $ps + echo "$lon -2" | gmt psxy -R -J -Skleaf/2.5c -Gdarkgreen -W1p,black -O -K >> $ps +done + +# Parabolic arc symbol (open path, just stroked) +for lon in -3 -1 1 3; do + echo "$lon 3.2" | gmt psxy -R -J -Skqbezier_test/1.5c -W1.5p,red -O -K >> $ps +done + +# Labels +gmt pstext -R -J -F+f11p,Helvetica-Bold,red+jCM -O -K >> $ps << EOF +0 3.7 Parabolic arcs +EOF +gmt pstext -R -J -F+f9p+jCM -O >> $ps << EOF +0 2.7 1.5c leaves +0 0.7 2.0c leaves +0 -1.3 2.5c leaves +EOF