From 7055d964962dd70434a01171caecd6f9483dadad Mon Sep 17 00:00:00 2001 From: Joaquim Date: Tue, 25 Aug 2026 19:45:01 +0100 Subject: [PATCH 1/4] Fit 3-D subplot panels, and their heading, inside the figure (#4450) A perspective panel is drawn as a projected box that is larger than the flat map rectangle subplot lays its panels out from, and nothing ever fitted that box to the panel. In the reported example both cubes burst out of the -Ff20c/10c figure area and the heading was laid out against the flat rectangle, so it ended up among them. gmt_map_setup could not have fitted the box earlier: it is only known once gmtmap_init_three_D has run, at the very end of the setup. So the fit now happens there, for a panel in 3-D: the footprint is measured from the eight corners of the x/y/z region, padded for the ticks and annotations that the frame draws outside it (a quarter of that above the box, where nothing is annotated), and if it does not fit the panel the projection is scaled down and the box centered in the panel via z_project.x_off/y_off. The scaling includes proj.zmax/zmin, the z-axis length in plot units that the PostScript perspective matrix works from; without those the numbers changed but the picture did not. The heading is now placed at subplot end rather than at subplot begin, since only then is it known how far a 3-D panel reaches: begin records the position it would have used in gmt.subplotheading. together with the canvas corner (as PSL_SUBPLOT_ox/oy in the PostScript, so no assumption about -X -Y is needed), gmt_plotinit records the highest projected y of any 3-D panel in gmt.subplottop., and end returns to that corner, raises the heading by any excess and puts the origin back for whatever follows. The panel settings are cleared around that call, or the heading is scaled into the last panel, and restored afterwards since the -R history is built from them. 2-D figures write no gmt.subplottop file and are unaffected; test/subplot is unchanged apart from the new test. test/subplot/subplot_3d_fit.sh plots the example from the issue and requires the drawn height to stay near the 10 cm of the figure; the unfitted cubes reach some 19 cm. It measures the plot instead of comparing it, so it needs no baseline PostScript. Co-Authored-By: Claude Opus 5 --- src/gmt_map.c | 50 ++++++++++++ src/gmt_plot.c | 19 +++++ src/subplot.c | 144 ++++++++++++++++++++++++++------- test/subplot/subplot_3d_fit.sh | 44 ++++++++++ 4 files changed, 226 insertions(+), 31 deletions(-) create mode 100644 test/subplot/subplot_3d_fit.sh diff --git a/src/gmt_map.c b/src/gmt_map.c index 608e5327b44..56841c24056 100644 --- a/src/gmt_map.c +++ b/src/gmt_map.c @@ -6647,6 +6647,56 @@ GMT_LOCAL int gmtmap_init_three_D (struct GMT_CTRL *GMT) { GMT->current.proj.z_project.ymin += GMT->current.proj.z_project.y_off; GMT->current.proj.z_project.ymax += GMT->current.proj.z_project.y_off; + if (GMT->current.proj.three_D && GMT->current.plot.panel.active && GMT->current.plot.panel.no_scaling == 0) { + /* We are in a subplot panel. gmtmap_setxy could only fit the nominal 2-D map rectangle into the panel, + * since the perspective box is not known until here, and that box is the larger of the two - so the + * panels stuck out of the figure [issue #4450]. Measure the box the frame will actually occupy from + * the eight corners of the region, shrink the projection until it fits the panel, and center it there. + * All projected coordinates are linear in the plot scales, so the measured box scales with them. */ + struct GMT_SUBPLOT *P = &(GMT->current.plot.panel); + double bx[2] = {DBL_MAX, -DBL_MAX}, by[2] = {DBL_MAX, -DBL_MAX}, bw, bh, f, xc, yc, pad; + unsigned int ix, iy, iz; + for (ix = 0; ix < 2; ix++) for (iy = 0; iy < 2; iy++) for (iz = 0; iz < 2; iz++) { + gmt_xyz_to_xy (GMT, gmt_x_to_xx (GMT, GMT->common.R.wesn[XLO+ix]), gmt_y_to_yy (GMT, GMT->common.R.wesn[YLO+iy]), + gmt_z_to_zz (GMT, GMT->common.R.wesn[ZLO+iz]), &xc, &yc); + bx[0] = MIN (bx[0], xc); bx[1] = MAX (bx[1], xc); + by[0] = MIN (by[0], yc); by[1] = MAX (by[1], yc); + } + /* The frame is annotated outside that box, so leave room for a tick, the annotation offset and the + * annotation itself on every side, or the annotations hang outside the panel */ + pad = GMT->current.setting.map_tick_length[GMT_PRIMARY] + GMT->current.setting.map_annot_offset[GMT_PRIMARY] + + 2.0 * GMT->current.setting.font_annot[GMT_PRIMARY].size * GMT->session.u2u[GMT_PT][GMT_INCH]; + bx[0] -= pad; bx[1] += pad; by[0] -= pad; by[1] += 0.25 * pad; /* Nothing is annotated above the box */ + bw = bx[1] - bx[0]; bh = by[1] - by[0]; + f = (bw > 0.0 && bh > 0.0) ? MIN (P->w / bw, P->h / bh) : 1.0; + if (f < 1.0) { /* Too big for the panel, so scale the projection down */ + GMT->current.proj.scale[GMT_X] *= f; GMT->current.proj.scale[GMT_Y] *= f; GMT->current.proj.scale[GMT_Z] *= f; + GMT->current.proj.i_scale[GMT_X] /= f; GMT->current.proj.i_scale[GMT_Y] /= f; GMT->current.proj.i_scale[GMT_Z] /= f; + GMT->current.proj.w_r *= f; + GMT->current.proj.rect[XHI] *= f; GMT->current.proj.rect[YHI] *= f; + GMT->current.proj.origin[GMT_X] *= f; GMT->current.proj.origin[GMT_Y] *= f; GMT->current.proj.origin[GMT_Z] *= f; + GMT->current.proj.zmax *= f; GMT->current.proj.zmin *= f; /* The z axis length is in plot units too */ + GMT->current.proj.z_project.x_off *= f; GMT->current.proj.z_project.y_off *= f; + GMT->current.proj.z_project.xmin *= f; GMT->current.proj.z_project.xmax *= f; + GMT->current.proj.z_project.ymin *= f; GMT->current.proj.z_project.ymax *= f; + GMT->current.map.width *= f; GMT->current.map.height *= f; + GMT->current.map.half_width = 0.5 * GMT->current.map.width; + GMT->current.map.half_height = 0.5 * GMT->current.map.height; + bx[0] *= f; bx[1] *= f; by[0] *= f; by[1] *= f; + bw *= f; bh *= f; + } + /* Shift the projection so the box sits centered inside the panel */ + GMT->current.proj.z_project.x_off += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.y_off += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.xmin += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.xmax += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.ymin += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.ymax += 0.5 * (P->h - bh) - by[0]; + P->dx = P->dy = 0.0; /* The centering is in the projection now */ + GMT_Report (GMT->parent, GMT_MSG_DEBUG, "Perspective panel scaled by %g to a %g x %g footprint inside its %g x %g panel\n", + f, bw, bh, P->w, P->h); + } + return (GMT_NOERROR); } diff --git a/src/gmt_plot.c b/src/gmt_plot.c index a5c7624e86e..8e7a7cbb68a 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -9277,6 +9277,25 @@ struct PSL_CTRL *gmt_plotinit (struct GMT_CTRL *GMT, struct GMT_OPTION *options) /* Consider offsets required to center the plot on the subplot panel [0/0] */ GMT->current.setting.map_origin[GMT_X] += (P->dx + P->gap[XLO]); GMT->current.setting.map_origin[GMT_Y] += (P->dy + P->gap[YLO]); + if (GMT->current.proj.three_D) { + /* A subplot heading is laid out in figure coordinates, while a perspective panel may extend above its + * nominal 2-D rectangle. Record the highest projected y reached by any 3-D panel so that subplot end + * can place the figure heading above the real perspective footprint (issue #4450). */ + int fig = gmt_get_current_figure (GMT->parent); + char file[PATH_MAX] = {""}; + double old_top = -DBL_MAX; + double top = GMT->current.setting.map_origin[GMT_Y] + GMT->current.proj.z_project.ymax; + FILE *fp_top = NULL; + snprintf (file, PATH_MAX, "%s/gmt.subplottop.%d", GMT->parent->gwf_dir, fig); + if ((fp_top = fopen (file, "r")) != NULL) { + if (fscanf (fp_top, "%lf", &old_top) != 1) old_top = -DBL_MAX; + fclose (fp_top); + } + if (top > old_top && (fp_top = fopen (file, "w")) != NULL) { + fprintf (fp_top, "%.16g\n", top); + fclose (fp_top); + } + } if (P->first && O_active) /* Run completion script, if any */ PSL_setexec (PSL, 1); } diff --git a/src/subplot.c b/src/subplot.c index 1f31eac87e1..e1c51fcf86f 100644 --- a/src/subplot.c +++ b/src/subplot.c @@ -1435,7 +1435,8 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } } - /* Start the subplot with a blank canvas and place the optional title. + /* Start the subplot with a blank canvas. The optional figure heading is deferred until subplot end + so that the perspective extent of any 3-D panel is known when it is placed [issue #4450]. The blank canvas dimensions should become the -R and -Jx1 once subplot ends */ if (Ctrl->F.fill[0] != '-' && Ctrl->F.pen[0] != '-') /* Need to fill and draw the canvas box */ @@ -1448,36 +1449,41 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { width += 2.0 * Ctrl->F.clearance[GMT_X]; height += 2.0 * Ctrl->F.clearance[GMT_Y]; - if (Ctrl->T.title) { /* Must call text to place a heading */ - uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ - struct GMT_DATASET *T = NULL; - if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); - Return (error); + /* plot is required, since nothing is plotted here (except for possibly the canvas fill/outline) */ + sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); + if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); + if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas */ + Return (API->error); + if (Ctrl->T.title) { /* Save exactly where and how the heading is to be plotted by subplot end [issue #4450]. + * We must place it there and not here since a 3-D panel may reach above the nominal figure top, which + * is only known once the panels have been drawn. The position is the one the heading would have been + * given here, i.e. in the canvas frame; subplot end returns the origin to this canvas corner, which we + * remember in the PostScript itself so that no assumption about -X -Y is needed. */ + FILE *fpt = NULL; + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fpt = fopen (file, "w")) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot create subplot heading file %s\n", file); + Return (GMT_ERROR_ON_FOPEN); } - T->table[0]->segment[0]->data[GMT_X][0] = 0.5 * width; /* Centered */ - T->table[0]->segment[0]->data[GMT_Y][0] = y_heading + Ctrl->F.clearance[GMT_Y]; /* On top */ - T->table[0]->segment[0]->text[0] = strdup (Ctrl->T.title); - T->table[0]->segment[0]->n_rows = 1; - T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; - if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { - Return (API->error); + /* Record: , all in canvas inches */ + fprintf (fpt, "%.16g %.16g %.16g %.16g %.16g %s\n", 0.5 * width, y_heading + Ctrl->F.clearance[GMT_Y], + width, height, height, Ctrl->T.title); + fclose (fpt); + /* Remember this canvas corner in the PostScript so subplot end can come back to it */ + if (gmt_set_psfilename (GMT) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, "a")) == NULL) { /* The canvas was just drawn, so append */ + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s to append\n", GMT->current.ps.filename); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "/PSL_SUBPLOT_ox PSL_xorig def /PSL_SUBPLOT_oy PSL_yorig def\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); } - sprintf (command, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s%s --GMT_HISTORY=readonly", - width, height, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile, origin_shift); - if (Bopt[0] == ' ') strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); - if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); - if (GMT_Destroy_Data (API, &T) != GMT_OK) - Return (API->error); - } - else { /* plot is required, since nothing is plotted (except for possibly the canvas fill/outline) */ - sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); - if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); - if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); } if (fabs (Ctrl->F.clearance[GMT_X]) > 0.0 || fabs (Ctrl->F.clearance[GMT_Y]) > 0.0) { /* Must reset origin */ width -= 2.0 * Ctrl->F.clearance[GMT_X]; @@ -1578,8 +1584,11 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { int k, id, row, col; char *wmode[2] = {"w","a"}, vfile[GMT_VF_LEN] = {""}, Rtxt[GMT_LEN64] = {""}, off[GMT_LEN32] = {""}; char legend_justification[4] = {""}, Jstr[3] = {"J"}, pen[GMT_LEN32] = {""}, fill[GMT_LEN32] = {""}; - double legend_width = 0.0, legend_scale = 1.0; - FILE *fp = NULL; + char line[GMT_BUFSIZ] = {""}, heading[GMT_BUFSIZ] = {""}; + double legend_width = 0.0, legend_scale = 1.0, hx = 0.0, hy = 0.0, top = -DBL_MAX, ytop = DBL_MAX, Rw = 0.0, Rh = 0.0; + int n_chars = 0; + bool have_heading = false; + FILE *fp = NULL, *fh = NULL; if ((P = gmt_subplot_info (API, fig)) == NULL) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "No subplot information file!\n"); @@ -1601,6 +1610,27 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { API->GMT->current.map.height = P->dim[GMT_Y]; P->active = 0; /* Ensure subplot mode is now terminated */ + /* Collect what is needed to place the figure heading now that every panel has had a chance to report its + * actual perspective footprint. subplot begin saved the exact position, region and text; the heading + * itself is plotted further down, once we are truly out of subplot mode. For 2-D figures no + * gmt.subplottop file exists so the placement is the same as it always was. */ + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { + if (fgets (line, GMT_BUFSIZ, fh) && sscanf (line, "%lf %lf %lf %lf %lf %n", &hx, &hy, &Rw, &Rh, &ytop, &n_chars) == 5 && n_chars > 0) { + strncpy (heading, &line[n_chars], GMT_BUFSIZ-1); + gmt_chop (heading); + have_heading = true; + } + fclose (fh); + gmt_remove_file (GMT, file); + } + sprintf (file, "%s/gmt.subplottop.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { /* At least one panel was 3-D, so the heading may need raising */ + if (fscanf (fh, "%lf", &top) == 1 && top > ytop) + hy += (top - ytop); + fclose (fh); + gmt_remove_file (GMT, file); + } if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { /* Get hidden file name for PS */ GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); Return (GMT_ERROR_ON_FOPEN); @@ -1611,6 +1641,12 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } /* Must force PSL_plot_completion procedure to run, if it was set */ PSL_command (GMT->PSL, "PSL_plot_completion /PSL_plot_completion {} def\n"); /* Run once, then make it a null function */ + if (have_heading) /* Remember where the panels left the origin, then put it back at the page corner so that + * the deferred heading below, whose position was saved in page coordinates, lands where it belongs. + * This is what -Xf does, but modern mode does not allow -Xf. The origin is restored once the heading + * has been placed, since anything following (e.g., a second subplot) shifts relative to it [#4450] */ + PSL_command (GMT->PSL, "/PSL_SUBPLOT_x PSL_xorig def /PSL_SUBPLOT_y PSL_yorig def " + "PSL_SUBPLOT_ox PSL_xorig sub PSL_SUBPLOT_oy PSL_yorig sub TM\n"); if (PSL_fclose (GMT->PSL)) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); Return (GMT_RUNTIME_ERROR); @@ -1631,6 +1667,52 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { if (!access (file, F_OK)) gmt_remove_file (GMT, file); } } + if (have_heading) { /* Must call text to place the figure heading, using the position collected above. + * Note this has to happen here, after the subplot information files are gone: a plotting module + * called while they are still around is set up as a panel plot by gmt_init_module, which is also + * why the debug lines below are drawn at this point. */ + uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ + struct GMT_DATASET *T = NULL; + struct GMT_SUBPLOT P_save; + /* The panel settings must not make the text call below fit the heading into the last panel, but they + * are still needed afterwards (the -R history is built from P->dim), so put them back when done */ + gmt_M_memcpy (&P_save, &GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + gmt_M_memset (&GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); + Return (API->error); + } + T->table[0]->segment[0]->data[GMT_X][0] = hx; /* Centered */ + T->table[0]->segment[0]->data[GMT_Y][0] = hy; /* On top */ + T->table[0]->segment[0]->text[0] = strdup (heading); + T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; + if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { + Return (API->error); + } + snprintf (command, GMT_LEN256, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s -Xa0i -Ya0i --GMT_HISTORY=readonly", + Rw, Rh, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile); + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); + if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the heading */ + Return (API->error); + if (GMT_Destroy_Data (API, &T) != GMT_OK) + Return (API->error); + /* Put the origin back where the panels had left it before we placed the heading */ + if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, wmode[k])) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s with mode %s\n", GMT->current.ps.filename, wmode[k]); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "PSL_SUBPLOT_x PSL_xorig sub PSL_SUBPLOT_y PSL_yorig sub TM\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); + } + gmt_M_memcpy (&GMT->current.plot.panel, &P_save, 1, struct GMT_SUBPLOT); /* Restore for the code below */ + } + /* Check if we should draw debug lines */ sprintf (file, "%s/gmt.subplotdebug.%d", API->gwf_dir, fig); if (!access (file, R_OK)) { /* Yes, must draw debug lines on top */ diff --git a/test/subplot/subplot_3d_fit.sh b/test/subplot/subplot_3d_fit.sh new file mode 100644 index 00000000000..bda9351e0cf --- /dev/null +++ b/test/subplot/subplot_3d_fit.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# A 3-D panel must fit inside the subplot figure area. This is the example from issue #4450. +# +# subplot sizes and places its panels from the nominal 2-D map rectangle, but a perspective panel is +# drawn as a projected box that is larger than that rectangle, and the box was never fitted to the +# panel: both cubes burst out of the -Ff20c/10c figure area, and the heading landed among them. +# gmt_map_setup now measures the projected footprint once the perspective is known and shrinks and +# centers it inside the panel. +# +# No baseline PostScript is needed: we measure the ink of the two perspective panels straight out of +# the PostScript and require it to fit the 10 cm height of the figure. + +# This test measures the plot instead of comparing it, so it has no baseline PostScript. gmtest gives +# any script that spells out the modern mode start command a $ps to compare, so we spell it via $start. +start=begin + +gmt $start fit3d ps + gmt subplot begin 1x2 -Ff20c/10c+pblack+wblue -T"3D Subplots" + gmt subplot set 0,0 + gmt basemap -BneSWZ+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + + gmt subplot set 0,1 + gmt basemap -BSEnwZ2+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + gmt subplot end +gmt end + +# Crop to the ink and read the height of what was actually drawn, in cm +gmt psconvert -A -Te fit3d.ps +height=$(awk '/^%%BoundingBox: / && $5 != "(atend)" {printf "%.2f\n", ($5 - $3) * 2.54 / 72; exit}' fit3d.eps) + +rm -f fit3d.ps fit3d.eps # Measured; there is no baseline for them + +# The figure is 10 cm tall and the heading adds about 1 cm above it. Panels that do not fit come out +# far taller than that: the unfitted cubes of the report reach some 19 cm. +echo "drawn height ${height} cm" > result.txt +awk -v h="$height" 'BEGIN { + if (h <= 0) { print "nothing was drawn"; exit 1 } + if (h > 13.0) { printf "the figure is %.2f cm tall, so the 3-D panels do not fit its 10 cm height\n", h; exit 1 } + print "fits" +}' > verdict.txt + +echo "fits" > answer.txt +diff -q --strip-trailing-cr answer.txt verdict.txt From 549ef819a0ae4bf8e4fd4725ff58ffd31017cafc Mon Sep 17 00:00:00 2001 From: Joaquim <jluis@ualg.pt> Date: Tue, 25 Aug 2026 19:45:01 +0100 Subject: [PATCH 2/4] Fit 3-D subplot panels, and their heading, inside the figure (#4450) A perspective panel is drawn as a projected box that is larger than the flat map rectangle subplot lays its panels out from, and nothing ever fitted that box to the panel. In the reported example both cubes burst out of the -Ff20c/10c figure area and the heading was laid out against the flat rectangle, so it ended up among them. gmt_map_setup could not have fitted the box earlier: it is only known once gmtmap_init_three_D has run, at the very end of the setup. So the fit now happens there. It applies only when all of the following hold, so that no plot that was already laid out sensibly is disturbed: * the projection has a real z axis (-JZ|-Jz), since a perspective view of a flat plane has always been allowed to spill outside its panel; * the box does not already fit the panel - one that fits is left alone; * the figure dimensions were given with subplot -Ff, so the contents owe it to the user to stay inside them. With -Fs the user set the panel size and we keep out of it. A new fixed_figure flag in GMT_SUBPLOT carries this, written as # FIXEDFIG: into the subplot information file. When it does apply, the footprint is measured from the eight corners of the x/y/z region, padded for the ticks and annotations that the frame draws outside it (a quarter of that above the box, where nothing is annotated), the projection is scaled down and the box centered in the panel via z_project.x_off/y_off. The scaling includes proj.zmax/zmin, the z-axis length in plot units that the PostScript perspective matrix works from; without those the numbers change but the picture does not. The heading is now placed at subplot end rather than at subplot begin, since only then is it known how far a 3-D panel reaches: begin records the position it would have used in gmt.subplotheading.<fig> together with the canvas corner (as PSL_SUBPLOT_ox/oy in the PostScript, so no assumption about -X -Y is needed), gmt_plotinit records the highest projected y of any 3-D panel in gmt.subplottop.<fig>, and end returns to that corner, raises the heading by any excess and puts the origin back for whatever follows. The panel settings are cleared around that call, or the heading is scaled into the last panel, and restored afterwards since the -R history is built from them. 2-D figures write no gmt.subplottop file and are unaffected. test/subplot/subplot_3d_fit.sh plots the example from the issue and requires the drawn height to stay near the 10 cm of the figure; the unfitted cubes reach some 19 cm. It measures the plot instead of comparing it, so it needs no baseline PostScript. test/subplot and test/psxyz pass unchanged, with no baseline regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- src/gmt_init.c | 2 + src/gmt_map.c | 55 +++++++++++ src/gmt_plot.c | 19 ++++ src/gmt_types.h | 1 + src/subplot.c | 163 +++++++++++++++++++++++++-------- test/subplot/subplot_3d_fit.sh | 44 +++++++++ 6 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 test/subplot/subplot_3d_fit.sh diff --git a/src/gmt_init.c b/src/gmt_init.c index 9c79e6f079c..203387270d0 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -14719,6 +14719,8 @@ struct GMT_SUBPLOT *gmt_subplot_info (struct GMTAPI_CTRL *API, int fig) { P->parallel = atoi (&line[12]); else if (!strncmp (line, "# INSIDE:", 9U)) P->inside = atoi (&line[10]); + else if (!strncmp (line, "# FIXEDFIG:", 11U)) + P->fixed_figure = atoi (&line[12]); else if (!strncmp (line, "# DIRECTION:", 12U)) sscanf (&line[13], "%d %d", &P->dir[GMT_X], &P->dir[GMT_Y]); else if (!strncmp (line, "# GAPS:", 7U)) diff --git a/src/gmt_map.c b/src/gmt_map.c index 608e5327b44..6145f68f8d5 100644 --- a/src/gmt_map.c +++ b/src/gmt_map.c @@ -6647,6 +6647,61 @@ GMT_LOCAL int gmtmap_init_three_D (struct GMT_CTRL *GMT) { GMT->current.proj.z_project.ymin += GMT->current.proj.z_project.y_off; GMT->current.proj.z_project.ymax += GMT->current.proj.z_project.y_off; + if (GMT->current.proj.three_D && GMT->current.proj.zmax > GMT->current.proj.zmin && + GMT->current.plot.panel.active && GMT->current.plot.panel.fixed_figure && GMT->current.plot.panel.no_scaling == 0) { + /* We are in a subplot panel. gmtmap_setxy could only fit the nominal 2-D map rectangle into the panel, + * since the perspective box is not known until here, and that box is the larger of the two - so the + * panels stuck out of the figure [issue #4450]. Measure the box the frame will actually occupy from + * the eight corners of the region, shrink the projection until it fits the panel, and center it there. + * All projected coordinates are linear in the plot scales, so the measured box scales with them. */ + struct GMT_SUBPLOT *P = &(GMT->current.plot.panel); + double bx[2] = {DBL_MAX, -DBL_MAX}, by[2] = {DBL_MAX, -DBL_MAX}, bw, bh, f, xc, yc, pad; + unsigned int ix, iy, iz; + for (ix = 0; ix < 2; ix++) for (iy = 0; iy < 2; iy++) for (iz = 0; iz < 2; iz++) { + gmt_xyz_to_xy (GMT, gmt_x_to_xx (GMT, GMT->common.R.wesn[XLO+ix]), gmt_y_to_yy (GMT, GMT->common.R.wesn[YLO+iy]), + gmt_z_to_zz (GMT, GMT->common.R.wesn[ZLO+iz]), &xc, &yc); + bx[0] = MIN (bx[0], xc); bx[1] = MAX (bx[1], xc); + by[0] = MIN (by[0], yc); by[1] = MAX (by[1], yc); + } + if ((bx[1] - bx[0]) <= P->w && (by[1] - by[0]) <= P->h) + return (GMT_NOERROR); /* The box already fits the panel, so leave this plot exactly as it was */ + + /* It does not fit. The frame is annotated outside the box, so when we shrink it we must also leave room + * for a tick, the annotation offset and the annotation itself, or the annotations hang out of the panel */ + pad = GMT->current.setting.map_tick_length[GMT_PRIMARY] + GMT->current.setting.map_annot_offset[GMT_PRIMARY] + + 2.0 * GMT->current.setting.font_annot[GMT_PRIMARY].size * GMT->session.u2u[GMT_PT][GMT_INCH]; + bx[0] -= pad; bx[1] += pad; by[0] -= pad; by[1] += 0.25 * pad; /* Nothing is annotated above the box */ + bw = bx[1] - bx[0]; bh = by[1] - by[0]; + f = (bw > 0.0 && bh > 0.0) ? MIN (P->w / bw, P->h / bh) : 1.0; + if (f < 1.0) { /* Too big for the panel, so scale the projection down and center what is left. + * A panel that already fits is left completely alone, so such plots are unchanged. */ + GMT->current.proj.scale[GMT_X] *= f; GMT->current.proj.scale[GMT_Y] *= f; GMT->current.proj.scale[GMT_Z] *= f; + GMT->current.proj.i_scale[GMT_X] /= f; GMT->current.proj.i_scale[GMT_Y] /= f; GMT->current.proj.i_scale[GMT_Z] /= f; + GMT->current.proj.w_r *= f; + GMT->current.proj.rect[XHI] *= f; GMT->current.proj.rect[YHI] *= f; + GMT->current.proj.origin[GMT_X] *= f; GMT->current.proj.origin[GMT_Y] *= f; GMT->current.proj.origin[GMT_Z] *= f; + GMT->current.proj.zmax *= f; GMT->current.proj.zmin *= f; /* The z axis length is in plot units too */ + GMT->current.proj.z_project.x_off *= f; GMT->current.proj.z_project.y_off *= f; + GMT->current.proj.z_project.xmin *= f; GMT->current.proj.z_project.xmax *= f; + GMT->current.proj.z_project.ymin *= f; GMT->current.proj.z_project.ymax *= f; + GMT->current.map.width *= f; GMT->current.map.height *= f; + GMT->current.map.half_width = 0.5 * GMT->current.map.width; + GMT->current.map.half_height = 0.5 * GMT->current.map.height; + bx[0] *= f; bx[1] *= f; by[0] *= f; by[1] *= f; + bw *= f; bh *= f; + /* Shift the projection so the box sits centered inside the panel */ + GMT->current.proj.z_project.x_off += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.y_off += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.xmin += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.xmax += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.ymin += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.ymax += 0.5 * (P->h - bh) - by[0]; + P->dx = P->dy = 0.0; /* The centering is in the projection now */ + GMT_Report (GMT->parent, GMT_MSG_DEBUG, "Perspective panel scaled by %g to a %g x %g footprint inside its %g x %g panel\n", + f, bw, bh, P->w, P->h); + } + } + return (GMT_NOERROR); } diff --git a/src/gmt_plot.c b/src/gmt_plot.c index a5c7624e86e..8e7a7cbb68a 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -9277,6 +9277,25 @@ struct PSL_CTRL *gmt_plotinit (struct GMT_CTRL *GMT, struct GMT_OPTION *options) /* Consider offsets required to center the plot on the subplot panel [0/0] */ GMT->current.setting.map_origin[GMT_X] += (P->dx + P->gap[XLO]); GMT->current.setting.map_origin[GMT_Y] += (P->dy + P->gap[YLO]); + if (GMT->current.proj.three_D) { + /* A subplot heading is laid out in figure coordinates, while a perspective panel may extend above its + * nominal 2-D rectangle. Record the highest projected y reached by any 3-D panel so that subplot end + * can place the figure heading above the real perspective footprint (issue #4450). */ + int fig = gmt_get_current_figure (GMT->parent); + char file[PATH_MAX] = {""}; + double old_top = -DBL_MAX; + double top = GMT->current.setting.map_origin[GMT_Y] + GMT->current.proj.z_project.ymax; + FILE *fp_top = NULL; + snprintf (file, PATH_MAX, "%s/gmt.subplottop.%d", GMT->parent->gwf_dir, fig); + if ((fp_top = fopen (file, "r")) != NULL) { + if (fscanf (fp_top, "%lf", &old_top) != 1) old_top = -DBL_MAX; + fclose (fp_top); + } + if (top > old_top && (fp_top = fopen (file, "w")) != NULL) { + fprintf (fp_top, "%.16g\n", top); + fclose (fp_top); + } + } if (P->first && O_active) /* Run completion script, if any */ PSL_setexec (PSL, 1); } diff --git a/src/gmt_types.h b/src/gmt_types.h index f3fffecab49..869cf07443a 100644 --- a/src/gmt_types.h +++ b/src/gmt_types.h @@ -183,6 +183,7 @@ struct GMT_SUBPLOT { unsigned int no_scaling; /* 1 when we are plotting a scale, bar, etc and not map and don't want to auto-scale plot */ unsigned int parallel; /* 1 for axis-parallel annotations [0 for standard] */ unsigned int inside; /* 1 if all annots/ticks are inside panels [0 for outside] */ + unsigned int fixed_figure; /* 1 if subplot -Ff gave the figure dimensions, so contents must fit inside them */ int row, col; /* Current panel position e.g., 0,0 */ int nrows, ncolumns; /* Panel arrangement for subplot window */ int dir[2]; /* Cartesian axis direction: +1 or -1 [1/1] */ diff --git a/src/subplot.c b/src/subplot.c index 1f31eac87e1..bdbf58e1d85 100644 --- a/src/subplot.c +++ b/src/subplot.c @@ -1338,15 +1338,16 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { GMT_Report (API, GMT_MSG_ERROR, "Cannot create file %s\n", file); Return (GMT_ERROR_ON_FOPEN); } - fprintf (fp, "# subplot information file\n"); - cmd = GMT_Create_Cmd (API, options); - fprintf (fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); - gmt_M_free (GMT, cmd); - if (Ctrl->T.active) fprintf (fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); - fprintf (fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); - fprintf (fp, "# DIMENSION: %g %g\n", width, height); - fprintf (fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); - fprintf (fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); + fprintf(fp, "# subplot information file\n"); + cmd = GMT_Create_Cmd(API, options); + fprintf(fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); + gmt_M_free(GMT, cmd); + if (Ctrl->T.active) fprintf(fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); + fprintf(fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); + fprintf(fp, "# DIMENSION: %g %g\n", width, height); + fprintf(fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); + fprintf(fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); + fprintf(fp, "# FIXEDFIG: %d\n", (Ctrl->F.mode == SUBPLOT_FIGURE) ? 1 : 0); if (Ctrl->C.active) { /* Got common gaps setting */ gmt_M_memcpy (GMT->current.plot.panel.gap, Ctrl->C.gap, 4, double); fprintf (fp, "# GAPS: %g %g %g %g\n", Ctrl->C.gap[XLO], Ctrl->C.gap[XHI], Ctrl->C.gap[YLO], Ctrl->C.gap[YHI]); @@ -1435,7 +1436,8 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } } - /* Start the subplot with a blank canvas and place the optional title. + /* Start the subplot with a blank canvas. The optional figure heading is deferred until subplot end + so that the perspective extent of any 3-D panel is known when it is placed [issue #4450]. The blank canvas dimensions should become the -R and -Jx1 once subplot ends */ if (Ctrl->F.fill[0] != '-' && Ctrl->F.pen[0] != '-') /* Need to fill and draw the canvas box */ @@ -1448,36 +1450,41 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { width += 2.0 * Ctrl->F.clearance[GMT_X]; height += 2.0 * Ctrl->F.clearance[GMT_Y]; - if (Ctrl->T.title) { /* Must call text to place a heading */ - uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ - struct GMT_DATASET *T = NULL; - if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); - Return (error); + /* plot is required, since nothing is plotted here (except for possibly the canvas fill/outline) */ + sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); + if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); + if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas */ + Return (API->error); + if (Ctrl->T.title) { /* Save exactly where and how the heading is to be plotted by subplot end [issue #4450]. + * We must place it there and not here since a 3-D panel may reach above the nominal figure top, which + * is only known once the panels have been drawn. The position is the one the heading would have been + * given here, i.e. in the canvas frame; subplot end returns the origin to this canvas corner, which we + * remember in the PostScript itself so that no assumption about -X -Y is needed. */ + FILE *fpt = NULL; + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fpt = fopen (file, "w")) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot create subplot heading file %s\n", file); + Return (GMT_ERROR_ON_FOPEN); } - T->table[0]->segment[0]->data[GMT_X][0] = 0.5 * width; /* Centered */ - T->table[0]->segment[0]->data[GMT_Y][0] = y_heading + Ctrl->F.clearance[GMT_Y]; /* On top */ - T->table[0]->segment[0]->text[0] = strdup (Ctrl->T.title); - T->table[0]->segment[0]->n_rows = 1; - T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; - if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { - Return (API->error); + /* Record: <x> <y> <width of region> <height of region> <top of figure> <title>, all in canvas inches */ + fprintf (fpt, "%.16g %.16g %.16g %.16g %.16g %s\n", 0.5 * width, y_heading + Ctrl->F.clearance[GMT_Y], + width, height, height, Ctrl->T.title); + fclose (fpt); + /* Remember this canvas corner in the PostScript so subplot end can come back to it */ + if (gmt_set_psfilename (GMT) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, "a")) == NULL) { /* The canvas was just drawn, so append */ + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s to append\n", GMT->current.ps.filename); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "/PSL_SUBPLOT_ox PSL_xorig def /PSL_SUBPLOT_oy PSL_yorig def\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); } - sprintf (command, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s%s --GMT_HISTORY=readonly", - width, height, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile, origin_shift); - if (Bopt[0] == ' ') strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); - if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); - if (GMT_Destroy_Data (API, &T) != GMT_OK) - Return (API->error); - } - else { /* plot is required, since nothing is plotted (except for possibly the canvas fill/outline) */ - sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); - if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); - if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); } if (fabs (Ctrl->F.clearance[GMT_X]) > 0.0 || fabs (Ctrl->F.clearance[GMT_Y]) > 0.0) { /* Must reset origin */ width -= 2.0 * Ctrl->F.clearance[GMT_X]; @@ -1578,8 +1585,11 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { int k, id, row, col; char *wmode[2] = {"w","a"}, vfile[GMT_VF_LEN] = {""}, Rtxt[GMT_LEN64] = {""}, off[GMT_LEN32] = {""}; char legend_justification[4] = {""}, Jstr[3] = {"J"}, pen[GMT_LEN32] = {""}, fill[GMT_LEN32] = {""}; - double legend_width = 0.0, legend_scale = 1.0; - FILE *fp = NULL; + char line[GMT_BUFSIZ] = {""}, heading[GMT_BUFSIZ] = {""}; + double legend_width = 0.0, legend_scale = 1.0, hx = 0.0, hy = 0.0, top = -DBL_MAX, ytop = DBL_MAX, Rw = 0.0, Rh = 0.0; + int n_chars = 0; + bool have_heading = false; + FILE *fp = NULL, *fh = NULL; if ((P = gmt_subplot_info (API, fig)) == NULL) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "No subplot information file!\n"); @@ -1601,6 +1611,27 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { API->GMT->current.map.height = P->dim[GMT_Y]; P->active = 0; /* Ensure subplot mode is now terminated */ + /* Collect what is needed to place the figure heading now that every panel has had a chance to report its + * actual perspective footprint. subplot begin saved the exact position, region and text; the heading + * itself is plotted further down, once we are truly out of subplot mode. For 2-D figures no + * gmt.subplottop file exists so the placement is the same as it always was. */ + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { + if (fgets (line, GMT_BUFSIZ, fh) && sscanf (line, "%lf %lf %lf %lf %lf %n", &hx, &hy, &Rw, &Rh, &ytop, &n_chars) == 5 && n_chars > 0) { + strncpy (heading, &line[n_chars], GMT_BUFSIZ-1); + gmt_chop (heading); + have_heading = true; + } + fclose (fh); + gmt_remove_file (GMT, file); + } + sprintf (file, "%s/gmt.subplottop.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { /* At least one panel was 3-D, so the heading may need raising */ + if (fscanf (fh, "%lf", &top) == 1 && top > ytop) + hy += (top - ytop); + fclose (fh); + gmt_remove_file (GMT, file); + } if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { /* Get hidden file name for PS */ GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); Return (GMT_ERROR_ON_FOPEN); @@ -1611,6 +1642,12 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } /* Must force PSL_plot_completion procedure to run, if it was set */ PSL_command (GMT->PSL, "PSL_plot_completion /PSL_plot_completion {} def\n"); /* Run once, then make it a null function */ + if (have_heading) /* Remember where the panels left the origin, then put it back at the page corner so that + * the deferred heading below, whose position was saved in page coordinates, lands where it belongs. + * This is what -Xf does, but modern mode does not allow -Xf. The origin is restored once the heading + * has been placed, since anything following (e.g., a second subplot) shifts relative to it [#4450] */ + PSL_command (GMT->PSL, "/PSL_SUBPLOT_x PSL_xorig def /PSL_SUBPLOT_y PSL_yorig def " + "PSL_SUBPLOT_ox PSL_xorig sub PSL_SUBPLOT_oy PSL_yorig sub TM\n"); if (PSL_fclose (GMT->PSL)) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); Return (GMT_RUNTIME_ERROR); @@ -1631,6 +1668,52 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { if (!access (file, F_OK)) gmt_remove_file (GMT, file); } } + if (have_heading) { /* Must call text to place the figure heading, using the position collected above. + * Note this has to happen here, after the subplot information files are gone: a plotting module + * called while they are still around is set up as a panel plot by gmt_init_module, which is also + * why the debug lines below are drawn at this point. */ + uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ + struct GMT_DATASET *T = NULL; + struct GMT_SUBPLOT P_save; + /* The panel settings must not make the text call below fit the heading into the last panel, but they + * are still needed afterwards (the -R history is built from P->dim), so put them back when done */ + gmt_M_memcpy (&P_save, &GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + gmt_M_memset (&GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); + Return (API->error); + } + T->table[0]->segment[0]->data[GMT_X][0] = hx; /* Centered */ + T->table[0]->segment[0]->data[GMT_Y][0] = hy; /* On top */ + T->table[0]->segment[0]->text[0] = strdup (heading); + T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; + if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { + Return (API->error); + } + snprintf (command, GMT_LEN256, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s -Xa0i -Ya0i --GMT_HISTORY=readonly", + Rw, Rh, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile); + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); + if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the heading */ + Return (API->error); + if (GMT_Destroy_Data (API, &T) != GMT_OK) + Return (API->error); + /* Put the origin back where the panels had left it before we placed the heading */ + if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, wmode[k])) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s with mode %s\n", GMT->current.ps.filename, wmode[k]); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "PSL_SUBPLOT_x PSL_xorig sub PSL_SUBPLOT_y PSL_yorig sub TM\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); + } + gmt_M_memcpy (&GMT->current.plot.panel, &P_save, 1, struct GMT_SUBPLOT); /* Restore for the code below */ + } + /* Check if we should draw debug lines */ sprintf (file, "%s/gmt.subplotdebug.%d", API->gwf_dir, fig); if (!access (file, R_OK)) { /* Yes, must draw debug lines on top */ diff --git a/test/subplot/subplot_3d_fit.sh b/test/subplot/subplot_3d_fit.sh new file mode 100644 index 00000000000..bda9351e0cf --- /dev/null +++ b/test/subplot/subplot_3d_fit.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# A 3-D panel must fit inside the subplot figure area. This is the example from issue #4450. +# +# subplot sizes and places its panels from the nominal 2-D map rectangle, but a perspective panel is +# drawn as a projected box that is larger than that rectangle, and the box was never fitted to the +# panel: both cubes burst out of the -Ff20c/10c figure area, and the heading landed among them. +# gmt_map_setup now measures the projected footprint once the perspective is known and shrinks and +# centers it inside the panel. +# +# No baseline PostScript is needed: we measure the ink of the two perspective panels straight out of +# the PostScript and require it to fit the 10 cm height of the figure. + +# This test measures the plot instead of comparing it, so it has no baseline PostScript. gmtest gives +# any script that spells out the modern mode start command a $ps to compare, so we spell it via $start. +start=begin + +gmt $start fit3d ps + gmt subplot begin 1x2 -Ff20c/10c+pblack+wblue -T"3D Subplots" + gmt subplot set 0,0 + gmt basemap -BneSWZ+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + + gmt subplot set 0,1 + gmt basemap -BSEnwZ2+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + gmt subplot end +gmt end + +# Crop to the ink and read the height of what was actually drawn, in cm +gmt psconvert -A -Te fit3d.ps +height=$(awk '/^%%BoundingBox: / && $5 != "(atend)" {printf "%.2f\n", ($5 - $3) * 2.54 / 72; exit}' fit3d.eps) + +rm -f fit3d.ps fit3d.eps # Measured; there is no baseline for them + +# The figure is 10 cm tall and the heading adds about 1 cm above it. Panels that do not fit come out +# far taller than that: the unfitted cubes of the report reach some 19 cm. +echo "drawn height ${height} cm" > result.txt +awk -v h="$height" 'BEGIN { + if (h <= 0) { print "nothing was drawn"; exit 1 } + if (h > 13.0) { printf "the figure is %.2f cm tall, so the 3-D panels do not fit its 10 cm height\n", h; exit 1 } + print "fits" +}' > verdict.txt + +echo "fits" > answer.txt +diff -q --strip-trailing-cr answer.txt verdict.txt From a0544c200df6356ce64b77e88b0062edde0c4c49 Mon Sep 17 00:00:00 2001 From: Joaquim <jluis@ualg.pt> Date: Thu, 27 Aug 2026 03:21:31 +0100 Subject: [PATCH 3/4] Revert "Fit 3-D subplot panels, and their heading, inside the figure (#4450)" This reverts commit 549ef819a0ae4bf8e4fd4725ff58ffd31017cafc. --- src/gmt_init.c | 2 - src/gmt_map.c | 55 ----------- src/gmt_plot.c | 19 ---- src/gmt_types.h | 1 - src/subplot.c | 163 ++++++++------------------------- test/subplot/subplot_3d_fit.sh | 44 --------- 6 files changed, 40 insertions(+), 244 deletions(-) delete mode 100644 test/subplot/subplot_3d_fit.sh diff --git a/src/gmt_init.c b/src/gmt_init.c index 203387270d0..9c79e6f079c 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -14719,8 +14719,6 @@ struct GMT_SUBPLOT *gmt_subplot_info (struct GMTAPI_CTRL *API, int fig) { P->parallel = atoi (&line[12]); else if (!strncmp (line, "# INSIDE:", 9U)) P->inside = atoi (&line[10]); - else if (!strncmp (line, "# FIXEDFIG:", 11U)) - P->fixed_figure = atoi (&line[12]); else if (!strncmp (line, "# DIRECTION:", 12U)) sscanf (&line[13], "%d %d", &P->dir[GMT_X], &P->dir[GMT_Y]); else if (!strncmp (line, "# GAPS:", 7U)) diff --git a/src/gmt_map.c b/src/gmt_map.c index 6145f68f8d5..608e5327b44 100644 --- a/src/gmt_map.c +++ b/src/gmt_map.c @@ -6647,61 +6647,6 @@ GMT_LOCAL int gmtmap_init_three_D (struct GMT_CTRL *GMT) { GMT->current.proj.z_project.ymin += GMT->current.proj.z_project.y_off; GMT->current.proj.z_project.ymax += GMT->current.proj.z_project.y_off; - if (GMT->current.proj.three_D && GMT->current.proj.zmax > GMT->current.proj.zmin && - GMT->current.plot.panel.active && GMT->current.plot.panel.fixed_figure && GMT->current.plot.panel.no_scaling == 0) { - /* We are in a subplot panel. gmtmap_setxy could only fit the nominal 2-D map rectangle into the panel, - * since the perspective box is not known until here, and that box is the larger of the two - so the - * panels stuck out of the figure [issue #4450]. Measure the box the frame will actually occupy from - * the eight corners of the region, shrink the projection until it fits the panel, and center it there. - * All projected coordinates are linear in the plot scales, so the measured box scales with them. */ - struct GMT_SUBPLOT *P = &(GMT->current.plot.panel); - double bx[2] = {DBL_MAX, -DBL_MAX}, by[2] = {DBL_MAX, -DBL_MAX}, bw, bh, f, xc, yc, pad; - unsigned int ix, iy, iz; - for (ix = 0; ix < 2; ix++) for (iy = 0; iy < 2; iy++) for (iz = 0; iz < 2; iz++) { - gmt_xyz_to_xy (GMT, gmt_x_to_xx (GMT, GMT->common.R.wesn[XLO+ix]), gmt_y_to_yy (GMT, GMT->common.R.wesn[YLO+iy]), - gmt_z_to_zz (GMT, GMT->common.R.wesn[ZLO+iz]), &xc, &yc); - bx[0] = MIN (bx[0], xc); bx[1] = MAX (bx[1], xc); - by[0] = MIN (by[0], yc); by[1] = MAX (by[1], yc); - } - if ((bx[1] - bx[0]) <= P->w && (by[1] - by[0]) <= P->h) - return (GMT_NOERROR); /* The box already fits the panel, so leave this plot exactly as it was */ - - /* It does not fit. The frame is annotated outside the box, so when we shrink it we must also leave room - * for a tick, the annotation offset and the annotation itself, or the annotations hang out of the panel */ - pad = GMT->current.setting.map_tick_length[GMT_PRIMARY] + GMT->current.setting.map_annot_offset[GMT_PRIMARY] + - 2.0 * GMT->current.setting.font_annot[GMT_PRIMARY].size * GMT->session.u2u[GMT_PT][GMT_INCH]; - bx[0] -= pad; bx[1] += pad; by[0] -= pad; by[1] += 0.25 * pad; /* Nothing is annotated above the box */ - bw = bx[1] - bx[0]; bh = by[1] - by[0]; - f = (bw > 0.0 && bh > 0.0) ? MIN (P->w / bw, P->h / bh) : 1.0; - if (f < 1.0) { /* Too big for the panel, so scale the projection down and center what is left. - * A panel that already fits is left completely alone, so such plots are unchanged. */ - GMT->current.proj.scale[GMT_X] *= f; GMT->current.proj.scale[GMT_Y] *= f; GMT->current.proj.scale[GMT_Z] *= f; - GMT->current.proj.i_scale[GMT_X] /= f; GMT->current.proj.i_scale[GMT_Y] /= f; GMT->current.proj.i_scale[GMT_Z] /= f; - GMT->current.proj.w_r *= f; - GMT->current.proj.rect[XHI] *= f; GMT->current.proj.rect[YHI] *= f; - GMT->current.proj.origin[GMT_X] *= f; GMT->current.proj.origin[GMT_Y] *= f; GMT->current.proj.origin[GMT_Z] *= f; - GMT->current.proj.zmax *= f; GMT->current.proj.zmin *= f; /* The z axis length is in plot units too */ - GMT->current.proj.z_project.x_off *= f; GMT->current.proj.z_project.y_off *= f; - GMT->current.proj.z_project.xmin *= f; GMT->current.proj.z_project.xmax *= f; - GMT->current.proj.z_project.ymin *= f; GMT->current.proj.z_project.ymax *= f; - GMT->current.map.width *= f; GMT->current.map.height *= f; - GMT->current.map.half_width = 0.5 * GMT->current.map.width; - GMT->current.map.half_height = 0.5 * GMT->current.map.height; - bx[0] *= f; bx[1] *= f; by[0] *= f; by[1] *= f; - bw *= f; bh *= f; - /* Shift the projection so the box sits centered inside the panel */ - GMT->current.proj.z_project.x_off += 0.5 * (P->w - bw) - bx[0]; - GMT->current.proj.z_project.y_off += 0.5 * (P->h - bh) - by[0]; - GMT->current.proj.z_project.xmin += 0.5 * (P->w - bw) - bx[0]; - GMT->current.proj.z_project.xmax += 0.5 * (P->w - bw) - bx[0]; - GMT->current.proj.z_project.ymin += 0.5 * (P->h - bh) - by[0]; - GMT->current.proj.z_project.ymax += 0.5 * (P->h - bh) - by[0]; - P->dx = P->dy = 0.0; /* The centering is in the projection now */ - GMT_Report (GMT->parent, GMT_MSG_DEBUG, "Perspective panel scaled by %g to a %g x %g footprint inside its %g x %g panel\n", - f, bw, bh, P->w, P->h); - } - } - return (GMT_NOERROR); } diff --git a/src/gmt_plot.c b/src/gmt_plot.c index 8e7a7cbb68a..a5c7624e86e 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -9277,25 +9277,6 @@ struct PSL_CTRL *gmt_plotinit (struct GMT_CTRL *GMT, struct GMT_OPTION *options) /* Consider offsets required to center the plot on the subplot panel [0/0] */ GMT->current.setting.map_origin[GMT_X] += (P->dx + P->gap[XLO]); GMT->current.setting.map_origin[GMT_Y] += (P->dy + P->gap[YLO]); - if (GMT->current.proj.three_D) { - /* A subplot heading is laid out in figure coordinates, while a perspective panel may extend above its - * nominal 2-D rectangle. Record the highest projected y reached by any 3-D panel so that subplot end - * can place the figure heading above the real perspective footprint (issue #4450). */ - int fig = gmt_get_current_figure (GMT->parent); - char file[PATH_MAX] = {""}; - double old_top = -DBL_MAX; - double top = GMT->current.setting.map_origin[GMT_Y] + GMT->current.proj.z_project.ymax; - FILE *fp_top = NULL; - snprintf (file, PATH_MAX, "%s/gmt.subplottop.%d", GMT->parent->gwf_dir, fig); - if ((fp_top = fopen (file, "r")) != NULL) { - if (fscanf (fp_top, "%lf", &old_top) != 1) old_top = -DBL_MAX; - fclose (fp_top); - } - if (top > old_top && (fp_top = fopen (file, "w")) != NULL) { - fprintf (fp_top, "%.16g\n", top); - fclose (fp_top); - } - } if (P->first && O_active) /* Run completion script, if any */ PSL_setexec (PSL, 1); } diff --git a/src/gmt_types.h b/src/gmt_types.h index 869cf07443a..f3fffecab49 100644 --- a/src/gmt_types.h +++ b/src/gmt_types.h @@ -183,7 +183,6 @@ struct GMT_SUBPLOT { unsigned int no_scaling; /* 1 when we are plotting a scale, bar, etc and not map and don't want to auto-scale plot */ unsigned int parallel; /* 1 for axis-parallel annotations [0 for standard] */ unsigned int inside; /* 1 if all annots/ticks are inside panels [0 for outside] */ - unsigned int fixed_figure; /* 1 if subplot -Ff gave the figure dimensions, so contents must fit inside them */ int row, col; /* Current panel position e.g., 0,0 */ int nrows, ncolumns; /* Panel arrangement for subplot window */ int dir[2]; /* Cartesian axis direction: +1 or -1 [1/1] */ diff --git a/src/subplot.c b/src/subplot.c index bdbf58e1d85..1f31eac87e1 100644 --- a/src/subplot.c +++ b/src/subplot.c @@ -1338,16 +1338,15 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { GMT_Report (API, GMT_MSG_ERROR, "Cannot create file %s\n", file); Return (GMT_ERROR_ON_FOPEN); } - fprintf(fp, "# subplot information file\n"); - cmd = GMT_Create_Cmd(API, options); - fprintf(fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); - gmt_M_free(GMT, cmd); - if (Ctrl->T.active) fprintf(fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); - fprintf(fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); - fprintf(fp, "# DIMENSION: %g %g\n", width, height); - fprintf(fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); - fprintf(fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); - fprintf(fp, "# FIXEDFIG: %d\n", (Ctrl->F.mode == SUBPLOT_FIGURE) ? 1 : 0); + fprintf (fp, "# subplot information file\n"); + cmd = GMT_Create_Cmd (API, options); + fprintf (fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); + gmt_M_free (GMT, cmd); + if (Ctrl->T.active) fprintf (fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); + fprintf (fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); + fprintf (fp, "# DIMENSION: %g %g\n", width, height); + fprintf (fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); + fprintf (fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); if (Ctrl->C.active) { /* Got common gaps setting */ gmt_M_memcpy (GMT->current.plot.panel.gap, Ctrl->C.gap, 4, double); fprintf (fp, "# GAPS: %g %g %g %g\n", Ctrl->C.gap[XLO], Ctrl->C.gap[XHI], Ctrl->C.gap[YLO], Ctrl->C.gap[YHI]); @@ -1436,8 +1435,7 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } } - /* Start the subplot with a blank canvas. The optional figure heading is deferred until subplot end - so that the perspective extent of any 3-D panel is known when it is placed [issue #4450]. + /* Start the subplot with a blank canvas and place the optional title. The blank canvas dimensions should become the -R and -Jx1 once subplot ends */ if (Ctrl->F.fill[0] != '-' && Ctrl->F.pen[0] != '-') /* Need to fill and draw the canvas box */ @@ -1450,41 +1448,36 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { width += 2.0 * Ctrl->F.clearance[GMT_X]; height += 2.0 * Ctrl->F.clearance[GMT_Y]; - /* plot is required, since nothing is plotted here (except for possibly the canvas fill/outline) */ - sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); - if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); - if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas */ - Return (API->error); - if (Ctrl->T.title) { /* Save exactly where and how the heading is to be plotted by subplot end [issue #4450]. - * We must place it there and not here since a 3-D panel may reach above the nominal figure top, which - * is only known once the panels have been drawn. The position is the one the heading would have been - * given here, i.e. in the canvas frame; subplot end returns the origin to this canvas corner, which we - * remember in the PostScript itself so that no assumption about -X -Y is needed. */ - FILE *fpt = NULL; - sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); - if ((fpt = fopen (file, "w")) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Cannot create subplot heading file %s\n", file); - Return (GMT_ERROR_ON_FOPEN); - } - /* Record: <x> <y> <width of region> <height of region> <top of figure> <title>, all in canvas inches */ - fprintf (fpt, "%.16g %.16g %.16g %.16g %.16g %s\n", 0.5 * width, y_heading + Ctrl->F.clearance[GMT_Y], - width, height, height, Ctrl->T.title); - fclose (fpt); - /* Remember this canvas corner in the PostScript so subplot end can come back to it */ - if (gmt_set_psfilename (GMT) == GMT_NOTSET) { - GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); - Return (GMT_ERROR_ON_FOPEN); - } - if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, "a")) == NULL) { /* The canvas was just drawn, so append */ - GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s to append\n", GMT->current.ps.filename); - Return (GMT_ERROR_ON_FOPEN); + if (Ctrl->T.title) { /* Must call text to place a heading */ + uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ + struct GMT_DATASET *T = NULL; + if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); + Return (error); } - PSL_command (GMT->PSL, "/PSL_SUBPLOT_ox PSL_xorig def /PSL_SUBPLOT_oy PSL_yorig def\n"); - if (PSL_fclose (GMT->PSL)) { - GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); - Return (GMT_RUNTIME_ERROR); + T->table[0]->segment[0]->data[GMT_X][0] = 0.5 * width; /* Centered */ + T->table[0]->segment[0]->data[GMT_Y][0] = y_heading + Ctrl->F.clearance[GMT_Y]; /* On top */ + T->table[0]->segment[0]->text[0] = strdup (Ctrl->T.title); + T->table[0]->segment[0]->n_rows = 1; + T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; + if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { + Return (API->error); } + sprintf (command, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s%s --GMT_HISTORY=readonly", + width, height, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile, origin_shift); + if (Bopt[0] == ' ') strcat (command, Bopt); /* The -B was set above, so include it in the command */ + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); + if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ + Return (API->error); + if (GMT_Destroy_Data (API, &T) != GMT_OK) + Return (API->error); + } + else { /* plot is required, since nothing is plotted (except for possibly the canvas fill/outline) */ + sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); + if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); + if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ + Return (API->error); } if (fabs (Ctrl->F.clearance[GMT_X]) > 0.0 || fabs (Ctrl->F.clearance[GMT_Y]) > 0.0) { /* Must reset origin */ width -= 2.0 * Ctrl->F.clearance[GMT_X]; @@ -1585,11 +1578,8 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { int k, id, row, col; char *wmode[2] = {"w","a"}, vfile[GMT_VF_LEN] = {""}, Rtxt[GMT_LEN64] = {""}, off[GMT_LEN32] = {""}; char legend_justification[4] = {""}, Jstr[3] = {"J"}, pen[GMT_LEN32] = {""}, fill[GMT_LEN32] = {""}; - char line[GMT_BUFSIZ] = {""}, heading[GMT_BUFSIZ] = {""}; - double legend_width = 0.0, legend_scale = 1.0, hx = 0.0, hy = 0.0, top = -DBL_MAX, ytop = DBL_MAX, Rw = 0.0, Rh = 0.0; - int n_chars = 0; - bool have_heading = false; - FILE *fp = NULL, *fh = NULL; + double legend_width = 0.0, legend_scale = 1.0; + FILE *fp = NULL; if ((P = gmt_subplot_info (API, fig)) == NULL) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "No subplot information file!\n"); @@ -1611,27 +1601,6 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { API->GMT->current.map.height = P->dim[GMT_Y]; P->active = 0; /* Ensure subplot mode is now terminated */ - /* Collect what is needed to place the figure heading now that every panel has had a chance to report its - * actual perspective footprint. subplot begin saved the exact position, region and text; the heading - * itself is plotted further down, once we are truly out of subplot mode. For 2-D figures no - * gmt.subplottop file exists so the placement is the same as it always was. */ - sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); - if ((fh = fopen (file, "r")) != NULL) { - if (fgets (line, GMT_BUFSIZ, fh) && sscanf (line, "%lf %lf %lf %lf %lf %n", &hx, &hy, &Rw, &Rh, &ytop, &n_chars) == 5 && n_chars > 0) { - strncpy (heading, &line[n_chars], GMT_BUFSIZ-1); - gmt_chop (heading); - have_heading = true; - } - fclose (fh); - gmt_remove_file (GMT, file); - } - sprintf (file, "%s/gmt.subplottop.%d", API->gwf_dir, fig); - if ((fh = fopen (file, "r")) != NULL) { /* At least one panel was 3-D, so the heading may need raising */ - if (fscanf (fh, "%lf", &top) == 1 && top > ytop) - hy += (top - ytop); - fclose (fh); - gmt_remove_file (GMT, file); - } if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { /* Get hidden file name for PS */ GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); Return (GMT_ERROR_ON_FOPEN); @@ -1642,12 +1611,6 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } /* Must force PSL_plot_completion procedure to run, if it was set */ PSL_command (GMT->PSL, "PSL_plot_completion /PSL_plot_completion {} def\n"); /* Run once, then make it a null function */ - if (have_heading) /* Remember where the panels left the origin, then put it back at the page corner so that - * the deferred heading below, whose position was saved in page coordinates, lands where it belongs. - * This is what -Xf does, but modern mode does not allow -Xf. The origin is restored once the heading - * has been placed, since anything following (e.g., a second subplot) shifts relative to it [#4450] */ - PSL_command (GMT->PSL, "/PSL_SUBPLOT_x PSL_xorig def /PSL_SUBPLOT_y PSL_yorig def " - "PSL_SUBPLOT_ox PSL_xorig sub PSL_SUBPLOT_oy PSL_yorig sub TM\n"); if (PSL_fclose (GMT->PSL)) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); Return (GMT_RUNTIME_ERROR); @@ -1668,52 +1631,6 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { if (!access (file, F_OK)) gmt_remove_file (GMT, file); } } - if (have_heading) { /* Must call text to place the figure heading, using the position collected above. - * Note this has to happen here, after the subplot information files are gone: a plotting module - * called while they are still around is set up as a panel plot by gmt_init_module, which is also - * why the debug lines below are drawn at this point. */ - uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ - struct GMT_DATASET *T = NULL; - struct GMT_SUBPLOT P_save; - /* The panel settings must not make the text call below fit the heading into the last panel, but they - * are still needed afterwards (the -R history is built from P->dim), so put them back when done */ - gmt_M_memcpy (&P_save, &GMT->current.plot.panel, 1, struct GMT_SUBPLOT); - gmt_M_memset (&GMT->current.plot.panel, 1, struct GMT_SUBPLOT); - if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); - Return (API->error); - } - T->table[0]->segment[0]->data[GMT_X][0] = hx; /* Centered */ - T->table[0]->segment[0]->data[GMT_Y][0] = hy; /* On top */ - T->table[0]->segment[0]->text[0] = strdup (heading); - T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; - if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { - Return (API->error); - } - snprintf (command, GMT_LEN256, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s -Xa0i -Ya0i --GMT_HISTORY=readonly", - Rw, Rh, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile); - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); - if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the heading */ - Return (API->error); - if (GMT_Destroy_Data (API, &T) != GMT_OK) - Return (API->error); - /* Put the origin back where the panels had left it before we placed the heading */ - if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { - GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); - Return (GMT_ERROR_ON_FOPEN); - } - if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, wmode[k])) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s with mode %s\n", GMT->current.ps.filename, wmode[k]); - Return (GMT_ERROR_ON_FOPEN); - } - PSL_command (GMT->PSL, "PSL_SUBPLOT_x PSL_xorig sub PSL_SUBPLOT_y PSL_yorig sub TM\n"); - if (PSL_fclose (GMT->PSL)) { - GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); - Return (GMT_RUNTIME_ERROR); - } - gmt_M_memcpy (&GMT->current.plot.panel, &P_save, 1, struct GMT_SUBPLOT); /* Restore for the code below */ - } - /* Check if we should draw debug lines */ sprintf (file, "%s/gmt.subplotdebug.%d", API->gwf_dir, fig); if (!access (file, R_OK)) { /* Yes, must draw debug lines on top */ diff --git a/test/subplot/subplot_3d_fit.sh b/test/subplot/subplot_3d_fit.sh deleted file mode 100644 index bda9351e0cf..00000000000 --- a/test/subplot/subplot_3d_fit.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# -# A 3-D panel must fit inside the subplot figure area. This is the example from issue #4450. -# -# subplot sizes and places its panels from the nominal 2-D map rectangle, but a perspective panel is -# drawn as a projected box that is larger than that rectangle, and the box was never fitted to the -# panel: both cubes burst out of the -Ff20c/10c figure area, and the heading landed among them. -# gmt_map_setup now measures the projected footprint once the perspective is known and shrinks and -# centers it inside the panel. -# -# No baseline PostScript is needed: we measure the ink of the two perspective panels straight out of -# the PostScript and require it to fit the 10 cm height of the figure. - -# This test measures the plot instead of comparing it, so it has no baseline PostScript. gmtest gives -# any script that spells out the modern mode start command a $ps to compare, so we spell it via $start. -start=begin - -gmt $start fit3d ps - gmt subplot begin 1x2 -Ff20c/10c+pblack+wblue -T"3D Subplots" - gmt subplot set 0,0 - gmt basemap -BneSWZ+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 - - gmt subplot set 0,1 - gmt basemap -BSEnwZ2+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 - gmt subplot end -gmt end - -# Crop to the ink and read the height of what was actually drawn, in cm -gmt psconvert -A -Te fit3d.ps -height=$(awk '/^%%BoundingBox: / && $5 != "(atend)" {printf "%.2f\n", ($5 - $3) * 2.54 / 72; exit}' fit3d.eps) - -rm -f fit3d.ps fit3d.eps # Measured; there is no baseline for them - -# The figure is 10 cm tall and the heading adds about 1 cm above it. Panels that do not fit come out -# far taller than that: the unfitted cubes of the report reach some 19 cm. -echo "drawn height ${height} cm" > result.txt -awk -v h="$height" 'BEGIN { - if (h <= 0) { print "nothing was drawn"; exit 1 } - if (h > 13.0) { printf "the figure is %.2f cm tall, so the 3-D panels do not fit its 10 cm height\n", h; exit 1 } - print "fits" -}' > verdict.txt - -echo "fits" > answer.txt -diff -q --strip-trailing-cr answer.txt verdict.txt From aba9b8a6ca7d8f9e2e2957dd366537851c1d5145 Mon Sep 17 00:00:00 2001 From: Joaquim <jluis@ualg.pt> Date: Thu, 27 Aug 2026 03:27:46 +0100 Subject: [PATCH 4/4] Reapply "Fit 3-D subplot panels, and their heading, inside the figure (#4450)" This reverts commit a0544c200df6356ce64b77e88b0062edde0c4c49. --- src/gmt_init.c | 2 + src/gmt_map.c | 55 +++++++++++ src/gmt_plot.c | 19 ++++ src/gmt_types.h | 1 + src/subplot.c | 163 +++++++++++++++++++++++++-------- test/subplot/subplot_3d_fit.sh | 44 +++++++++ 6 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 test/subplot/subplot_3d_fit.sh diff --git a/src/gmt_init.c b/src/gmt_init.c index 9c79e6f079c..203387270d0 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -14719,6 +14719,8 @@ struct GMT_SUBPLOT *gmt_subplot_info (struct GMTAPI_CTRL *API, int fig) { P->parallel = atoi (&line[12]); else if (!strncmp (line, "# INSIDE:", 9U)) P->inside = atoi (&line[10]); + else if (!strncmp (line, "# FIXEDFIG:", 11U)) + P->fixed_figure = atoi (&line[12]); else if (!strncmp (line, "# DIRECTION:", 12U)) sscanf (&line[13], "%d %d", &P->dir[GMT_X], &P->dir[GMT_Y]); else if (!strncmp (line, "# GAPS:", 7U)) diff --git a/src/gmt_map.c b/src/gmt_map.c index 608e5327b44..6145f68f8d5 100644 --- a/src/gmt_map.c +++ b/src/gmt_map.c @@ -6647,6 +6647,61 @@ GMT_LOCAL int gmtmap_init_three_D (struct GMT_CTRL *GMT) { GMT->current.proj.z_project.ymin += GMT->current.proj.z_project.y_off; GMT->current.proj.z_project.ymax += GMT->current.proj.z_project.y_off; + if (GMT->current.proj.three_D && GMT->current.proj.zmax > GMT->current.proj.zmin && + GMT->current.plot.panel.active && GMT->current.plot.panel.fixed_figure && GMT->current.plot.panel.no_scaling == 0) { + /* We are in a subplot panel. gmtmap_setxy could only fit the nominal 2-D map rectangle into the panel, + * since the perspective box is not known until here, and that box is the larger of the two - so the + * panels stuck out of the figure [issue #4450]. Measure the box the frame will actually occupy from + * the eight corners of the region, shrink the projection until it fits the panel, and center it there. + * All projected coordinates are linear in the plot scales, so the measured box scales with them. */ + struct GMT_SUBPLOT *P = &(GMT->current.plot.panel); + double bx[2] = {DBL_MAX, -DBL_MAX}, by[2] = {DBL_MAX, -DBL_MAX}, bw, bh, f, xc, yc, pad; + unsigned int ix, iy, iz; + for (ix = 0; ix < 2; ix++) for (iy = 0; iy < 2; iy++) for (iz = 0; iz < 2; iz++) { + gmt_xyz_to_xy (GMT, gmt_x_to_xx (GMT, GMT->common.R.wesn[XLO+ix]), gmt_y_to_yy (GMT, GMT->common.R.wesn[YLO+iy]), + gmt_z_to_zz (GMT, GMT->common.R.wesn[ZLO+iz]), &xc, &yc); + bx[0] = MIN (bx[0], xc); bx[1] = MAX (bx[1], xc); + by[0] = MIN (by[0], yc); by[1] = MAX (by[1], yc); + } + if ((bx[1] - bx[0]) <= P->w && (by[1] - by[0]) <= P->h) + return (GMT_NOERROR); /* The box already fits the panel, so leave this plot exactly as it was */ + + /* It does not fit. The frame is annotated outside the box, so when we shrink it we must also leave room + * for a tick, the annotation offset and the annotation itself, or the annotations hang out of the panel */ + pad = GMT->current.setting.map_tick_length[GMT_PRIMARY] + GMT->current.setting.map_annot_offset[GMT_PRIMARY] + + 2.0 * GMT->current.setting.font_annot[GMT_PRIMARY].size * GMT->session.u2u[GMT_PT][GMT_INCH]; + bx[0] -= pad; bx[1] += pad; by[0] -= pad; by[1] += 0.25 * pad; /* Nothing is annotated above the box */ + bw = bx[1] - bx[0]; bh = by[1] - by[0]; + f = (bw > 0.0 && bh > 0.0) ? MIN (P->w / bw, P->h / bh) : 1.0; + if (f < 1.0) { /* Too big for the panel, so scale the projection down and center what is left. + * A panel that already fits is left completely alone, so such plots are unchanged. */ + GMT->current.proj.scale[GMT_X] *= f; GMT->current.proj.scale[GMT_Y] *= f; GMT->current.proj.scale[GMT_Z] *= f; + GMT->current.proj.i_scale[GMT_X] /= f; GMT->current.proj.i_scale[GMT_Y] /= f; GMT->current.proj.i_scale[GMT_Z] /= f; + GMT->current.proj.w_r *= f; + GMT->current.proj.rect[XHI] *= f; GMT->current.proj.rect[YHI] *= f; + GMT->current.proj.origin[GMT_X] *= f; GMT->current.proj.origin[GMT_Y] *= f; GMT->current.proj.origin[GMT_Z] *= f; + GMT->current.proj.zmax *= f; GMT->current.proj.zmin *= f; /* The z axis length is in plot units too */ + GMT->current.proj.z_project.x_off *= f; GMT->current.proj.z_project.y_off *= f; + GMT->current.proj.z_project.xmin *= f; GMT->current.proj.z_project.xmax *= f; + GMT->current.proj.z_project.ymin *= f; GMT->current.proj.z_project.ymax *= f; + GMT->current.map.width *= f; GMT->current.map.height *= f; + GMT->current.map.half_width = 0.5 * GMT->current.map.width; + GMT->current.map.half_height = 0.5 * GMT->current.map.height; + bx[0] *= f; bx[1] *= f; by[0] *= f; by[1] *= f; + bw *= f; bh *= f; + /* Shift the projection so the box sits centered inside the panel */ + GMT->current.proj.z_project.x_off += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.y_off += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.xmin += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.xmax += 0.5 * (P->w - bw) - bx[0]; + GMT->current.proj.z_project.ymin += 0.5 * (P->h - bh) - by[0]; + GMT->current.proj.z_project.ymax += 0.5 * (P->h - bh) - by[0]; + P->dx = P->dy = 0.0; /* The centering is in the projection now */ + GMT_Report (GMT->parent, GMT_MSG_DEBUG, "Perspective panel scaled by %g to a %g x %g footprint inside its %g x %g panel\n", + f, bw, bh, P->w, P->h); + } + } + return (GMT_NOERROR); } diff --git a/src/gmt_plot.c b/src/gmt_plot.c index a5c7624e86e..8e7a7cbb68a 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -9277,6 +9277,25 @@ struct PSL_CTRL *gmt_plotinit (struct GMT_CTRL *GMT, struct GMT_OPTION *options) /* Consider offsets required to center the plot on the subplot panel [0/0] */ GMT->current.setting.map_origin[GMT_X] += (P->dx + P->gap[XLO]); GMT->current.setting.map_origin[GMT_Y] += (P->dy + P->gap[YLO]); + if (GMT->current.proj.three_D) { + /* A subplot heading is laid out in figure coordinates, while a perspective panel may extend above its + * nominal 2-D rectangle. Record the highest projected y reached by any 3-D panel so that subplot end + * can place the figure heading above the real perspective footprint (issue #4450). */ + int fig = gmt_get_current_figure (GMT->parent); + char file[PATH_MAX] = {""}; + double old_top = -DBL_MAX; + double top = GMT->current.setting.map_origin[GMT_Y] + GMT->current.proj.z_project.ymax; + FILE *fp_top = NULL; + snprintf (file, PATH_MAX, "%s/gmt.subplottop.%d", GMT->parent->gwf_dir, fig); + if ((fp_top = fopen (file, "r")) != NULL) { + if (fscanf (fp_top, "%lf", &old_top) != 1) old_top = -DBL_MAX; + fclose (fp_top); + } + if (top > old_top && (fp_top = fopen (file, "w")) != NULL) { + fprintf (fp_top, "%.16g\n", top); + fclose (fp_top); + } + } if (P->first && O_active) /* Run completion script, if any */ PSL_setexec (PSL, 1); } diff --git a/src/gmt_types.h b/src/gmt_types.h index f3fffecab49..869cf07443a 100644 --- a/src/gmt_types.h +++ b/src/gmt_types.h @@ -183,6 +183,7 @@ struct GMT_SUBPLOT { unsigned int no_scaling; /* 1 when we are plotting a scale, bar, etc and not map and don't want to auto-scale plot */ unsigned int parallel; /* 1 for axis-parallel annotations [0 for standard] */ unsigned int inside; /* 1 if all annots/ticks are inside panels [0 for outside] */ + unsigned int fixed_figure; /* 1 if subplot -Ff gave the figure dimensions, so contents must fit inside them */ int row, col; /* Current panel position e.g., 0,0 */ int nrows, ncolumns; /* Panel arrangement for subplot window */ int dir[2]; /* Cartesian axis direction: +1 or -1 [1/1] */ diff --git a/src/subplot.c b/src/subplot.c index 1f31eac87e1..bdbf58e1d85 100644 --- a/src/subplot.c +++ b/src/subplot.c @@ -1338,15 +1338,16 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { GMT_Report (API, GMT_MSG_ERROR, "Cannot create file %s\n", file); Return (GMT_ERROR_ON_FOPEN); } - fprintf (fp, "# subplot information file\n"); - cmd = GMT_Create_Cmd (API, options); - fprintf (fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); - gmt_M_free (GMT, cmd); - if (Ctrl->T.active) fprintf (fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); - fprintf (fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); - fprintf (fp, "# DIMENSION: %g %g\n", width, height); - fprintf (fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); - fprintf (fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); + fprintf(fp, "# subplot information file\n"); + cmd = GMT_Create_Cmd(API, options); + fprintf(fp, "# Command: %s %s\n", THIS_MODULE_CLASSIC_NAME, cmd); + gmt_M_free(GMT, cmd); + if (Ctrl->T.active) fprintf(fp, "# HEADING: %g %g %s\n", 0.5 * width, y_heading, Ctrl->T.title); + fprintf(fp, "# ORIGIN: %g %g\n", off[GMT_X], off[GMT_Y]); + fprintf(fp, "# DIMENSION: %g %g\n", width, height); + fprintf(fp, "# PARALLEL: %d\n", Ctrl->S[GMT_Y].parallel); + fprintf(fp, "# INSIDE: %d\n", (GMT->current.setting.map_frame_type == GMT_IS_INSIDE) ? 1 : 0); + fprintf(fp, "# FIXEDFIG: %d\n", (Ctrl->F.mode == SUBPLOT_FIGURE) ? 1 : 0); if (Ctrl->C.active) { /* Got common gaps setting */ gmt_M_memcpy (GMT->current.plot.panel.gap, Ctrl->C.gap, 4, double); fprintf (fp, "# GAPS: %g %g %g %g\n", Ctrl->C.gap[XLO], Ctrl->C.gap[XHI], Ctrl->C.gap[YLO], Ctrl->C.gap[YHI]); @@ -1435,7 +1436,8 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } } - /* Start the subplot with a blank canvas and place the optional title. + /* Start the subplot with a blank canvas. The optional figure heading is deferred until subplot end + so that the perspective extent of any 3-D panel is known when it is placed [issue #4450]. The blank canvas dimensions should become the -R and -Jx1 once subplot ends */ if (Ctrl->F.fill[0] != '-' && Ctrl->F.pen[0] != '-') /* Need to fill and draw the canvas box */ @@ -1448,36 +1450,41 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { width += 2.0 * Ctrl->F.clearance[GMT_X]; height += 2.0 * Ctrl->F.clearance[GMT_Y]; - if (Ctrl->T.title) { /* Must call text to place a heading */ - uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ - struct GMT_DATASET *T = NULL; - if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { - GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); - Return (error); + /* plot is required, since nothing is plotted here (except for possibly the canvas fill/outline) */ + sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); + if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); + if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas */ + Return (API->error); + if (Ctrl->T.title) { /* Save exactly where and how the heading is to be plotted by subplot end [issue #4450]. + * We must place it there and not here since a 3-D panel may reach above the nominal figure top, which + * is only known once the panels have been drawn. The position is the one the heading would have been + * given here, i.e. in the canvas frame; subplot end returns the origin to this canvas corner, which we + * remember in the PostScript itself so that no assumption about -X -Y is needed. */ + FILE *fpt = NULL; + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fpt = fopen (file, "w")) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot create subplot heading file %s\n", file); + Return (GMT_ERROR_ON_FOPEN); } - T->table[0]->segment[0]->data[GMT_X][0] = 0.5 * width; /* Centered */ - T->table[0]->segment[0]->data[GMT_Y][0] = y_heading + Ctrl->F.clearance[GMT_Y]; /* On top */ - T->table[0]->segment[0]->text[0] = strdup (Ctrl->T.title); - T->table[0]->segment[0]->n_rows = 1; - T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; - if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { - Return (API->error); + /* Record: <x> <y> <width of region> <height of region> <top of figure> <title>, all in canvas inches */ + fprintf (fpt, "%.16g %.16g %.16g %.16g %.16g %s\n", 0.5 * width, y_heading + Ctrl->F.clearance[GMT_Y], + width, height, height, Ctrl->T.title); + fclose (fpt); + /* Remember this canvas corner in the PostScript so subplot end can come back to it */ + if (gmt_set_psfilename (GMT) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, "a")) == NULL) { /* The canvas was just drawn, so append */ + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s to append\n", GMT->current.ps.filename); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "/PSL_SUBPLOT_ox PSL_xorig def /PSL_SUBPLOT_oy PSL_yorig def\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); } - sprintf (command, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s%s --GMT_HISTORY=readonly", - width, height, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile, origin_shift); - if (Bopt[0] == ' ') strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); - if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); - if (GMT_Destroy_Data (API, &T) != GMT_OK) - Return (API->error); - } - else { /* plot is required, since nothing is plotted (except for possibly the canvas fill/outline) */ - sprintf (command, "-R0/%g/0/%g -Jx1i -T%s --GMT_HISTORY=readonly", width, height, origin_shift); - if (Bopt[0]) strcat (command, Bopt); /* The -B was set above, so include it in the command */ - GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for plot: %s\n", command); - if (GMT_Call_Module (API, "plot", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the canvas with heading */ - Return (API->error); } if (fabs (Ctrl->F.clearance[GMT_X]) > 0.0 || fabs (Ctrl->F.clearance[GMT_Y]) > 0.0) { /* Must reset origin */ width -= 2.0 * Ctrl->F.clearance[GMT_X]; @@ -1578,8 +1585,11 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { int k, id, row, col; char *wmode[2] = {"w","a"}, vfile[GMT_VF_LEN] = {""}, Rtxt[GMT_LEN64] = {""}, off[GMT_LEN32] = {""}; char legend_justification[4] = {""}, Jstr[3] = {"J"}, pen[GMT_LEN32] = {""}, fill[GMT_LEN32] = {""}; - double legend_width = 0.0, legend_scale = 1.0; - FILE *fp = NULL; + char line[GMT_BUFSIZ] = {""}, heading[GMT_BUFSIZ] = {""}; + double legend_width = 0.0, legend_scale = 1.0, hx = 0.0, hy = 0.0, top = -DBL_MAX, ytop = DBL_MAX, Rw = 0.0, Rh = 0.0; + int n_chars = 0; + bool have_heading = false; + FILE *fp = NULL, *fh = NULL; if ((P = gmt_subplot_info (API, fig)) == NULL) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "No subplot information file!\n"); @@ -1601,6 +1611,27 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { API->GMT->current.map.height = P->dim[GMT_Y]; P->active = 0; /* Ensure subplot mode is now terminated */ + /* Collect what is needed to place the figure heading now that every panel has had a chance to report its + * actual perspective footprint. subplot begin saved the exact position, region and text; the heading + * itself is plotted further down, once we are truly out of subplot mode. For 2-D figures no + * gmt.subplottop file exists so the placement is the same as it always was. */ + sprintf (file, "%s/gmt.subplotheading.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { + if (fgets (line, GMT_BUFSIZ, fh) && sscanf (line, "%lf %lf %lf %lf %lf %n", &hx, &hy, &Rw, &Rh, &ytop, &n_chars) == 5 && n_chars > 0) { + strncpy (heading, &line[n_chars], GMT_BUFSIZ-1); + gmt_chop (heading); + have_heading = true; + } + fclose (fh); + gmt_remove_file (GMT, file); + } + sprintf (file, "%s/gmt.subplottop.%d", API->gwf_dir, fig); + if ((fh = fopen (file, "r")) != NULL) { /* At least one panel was 3-D, so the heading may need raising */ + if (fscanf (fh, "%lf", &top) == 1 && top > ytop) + hy += (top - ytop); + fclose (fh); + gmt_remove_file (GMT, file); + } if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { /* Get hidden file name for PS */ GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); Return (GMT_ERROR_ON_FOPEN); @@ -1611,6 +1642,12 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { } /* Must force PSL_plot_completion procedure to run, if it was set */ PSL_command (GMT->PSL, "PSL_plot_completion /PSL_plot_completion {} def\n"); /* Run once, then make it a null function */ + if (have_heading) /* Remember where the panels left the origin, then put it back at the page corner so that + * the deferred heading below, whose position was saved in page coordinates, lands where it belongs. + * This is what -Xf does, but modern mode does not allow -Xf. The origin is restored once the heading + * has been placed, since anything following (e.g., a second subplot) shifts relative to it [#4450] */ + PSL_command (GMT->PSL, "/PSL_SUBPLOT_x PSL_xorig def /PSL_SUBPLOT_y PSL_yorig def " + "PSL_SUBPLOT_ox PSL_xorig sub PSL_SUBPLOT_oy PSL_yorig sub TM\n"); if (PSL_fclose (GMT->PSL)) { GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); Return (GMT_RUNTIME_ERROR); @@ -1631,6 +1668,52 @@ EXTERN_MSC int GMT_subplot (void *V_API, int mode, void *args) { if (!access (file, F_OK)) gmt_remove_file (GMT, file); } } + if (have_heading) { /* Must call text to place the figure heading, using the position collected above. + * Note this has to happen here, after the subplot information files are gone: a plotting module + * called while they are still around is set up as a panel plot by gmt_init_module, which is also + * why the debug lines below are drawn at this point. */ + uint64_t dim[4] = {1, 1, 1, 2}; /* A single record */ + struct GMT_DATASET *T = NULL; + struct GMT_SUBPLOT P_save; + /* The panel settings must not make the text call below fit the heading into the last panel, but they + * are still needed afterwards (the -R history is built from P->dim), so put them back when done */ + gmt_M_memcpy (&P_save, &GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + gmt_M_memset (&GMT->current.plot.panel, 1, struct GMT_SUBPLOT); + if ((T = GMT_Create_Data (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_WITH_STRINGS, dim, NULL, NULL, 0, 0, NULL)) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Subplot: Unable to allocate a dataset\n"); + Return (API->error); + } + T->table[0]->segment[0]->data[GMT_X][0] = hx; /* Centered */ + T->table[0]->segment[0]->data[GMT_Y][0] = hy; /* On top */ + T->table[0]->segment[0]->text[0] = strdup (heading); + T->n_records = T->table[0]->n_records = T->table[0]->segment[0]->n_rows = 1; + if (GMT_Open_VirtualFile (API, GMT_IS_DATASET, GMT_IS_NONE, GMT_IN|GMT_IS_REFERENCE, T, vfile) != GMT_NOERROR) { + Return (API->error); + } + snprintf (command, GMT_LEN256, "-R0/%g/0/%g -Jx1i -N -F+jBC+f%s %s -Xa0i -Ya0i --GMT_HISTORY=readonly", + Rw, Rh, gmt_putfont (GMT, &GMT->current.setting.font_heading), vfile); + GMT_Report (API, GMT_MSG_DEBUG, "Subplot command for text: %s\n", command); + if (GMT_Call_Module (API, "text", GMT_MODULE_CMD, command) != GMT_OK) /* Plot the heading */ + Return (API->error); + if (GMT_Destroy_Data (API, &T) != GMT_OK) + Return (API->error); + /* Put the origin back where the panels had left it before we placed the heading */ + if ((k = gmt_set_psfilename (GMT)) == GMT_NOTSET) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "No workflow directory\n"); + Return (GMT_ERROR_ON_FOPEN); + } + if ((fp = PSL_fopen (GMT->PSL, GMT->current.ps.filename, wmode[k])) == NULL) { + GMT_Report (API, GMT_MSG_ERROR, "Cannot open %s with mode %s\n", GMT->current.ps.filename, wmode[k]); + Return (GMT_ERROR_ON_FOPEN); + } + PSL_command (GMT->PSL, "PSL_SUBPLOT_x PSL_xorig sub PSL_SUBPLOT_y PSL_yorig sub TM\n"); + if (PSL_fclose (GMT->PSL)) { + GMT_Report (GMT->parent, GMT_MSG_ERROR, "Unable to close hidden PS file %s!\n", GMT->current.ps.filename); + Return (GMT_RUNTIME_ERROR); + } + gmt_M_memcpy (&GMT->current.plot.panel, &P_save, 1, struct GMT_SUBPLOT); /* Restore for the code below */ + } + /* Check if we should draw debug lines */ sprintf (file, "%s/gmt.subplotdebug.%d", API->gwf_dir, fig); if (!access (file, R_OK)) { /* Yes, must draw debug lines on top */ diff --git a/test/subplot/subplot_3d_fit.sh b/test/subplot/subplot_3d_fit.sh new file mode 100644 index 00000000000..bda9351e0cf --- /dev/null +++ b/test/subplot/subplot_3d_fit.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# A 3-D panel must fit inside the subplot figure area. This is the example from issue #4450. +# +# subplot sizes and places its panels from the nominal 2-D map rectangle, but a perspective panel is +# drawn as a projected box that is larger than that rectangle, and the box was never fitted to the +# panel: both cubes burst out of the -Ff20c/10c figure area, and the heading landed among them. +# gmt_map_setup now measures the projected footprint once the perspective is known and shrinks and +# centers it inside the panel. +# +# No baseline PostScript is needed: we measure the ink of the two perspective panels straight out of +# the PostScript and require it to fit the 10 cm height of the figure. + +# This test measures the plot instead of comparing it, so it has no baseline PostScript. gmtest gives +# any script that spells out the modern mode start command a $ps to compare, so we spell it via $start. +start=begin + +gmt $start fit3d ps + gmt subplot begin 1x2 -Ff20c/10c+pblack+wblue -T"3D Subplots" + gmt subplot set 0,0 + gmt basemap -BneSWZ+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + + gmt subplot set 0,1 + gmt basemap -BSEnwZ2+b -R0/10/20/30/40/50 -Jz1 -p157.7/45 + gmt subplot end +gmt end + +# Crop to the ink and read the height of what was actually drawn, in cm +gmt psconvert -A -Te fit3d.ps +height=$(awk '/^%%BoundingBox: / && $5 != "(atend)" {printf "%.2f\n", ($5 - $3) * 2.54 / 72; exit}' fit3d.eps) + +rm -f fit3d.ps fit3d.eps # Measured; there is no baseline for them + +# The figure is 10 cm tall and the heading adds about 1 cm above it. Panels that do not fit come out +# far taller than that: the unfitted cubes of the report reach some 19 cm. +echo "drawn height ${height} cm" > result.txt +awk -v h="$height" 'BEGIN { + if (h <= 0) { print "nothing was drawn"; exit 1 } + if (h > 13.0) { printf "the figure is %.2f cm tall, so the 3-D panels do not fit its 10 cm height\n", h; exit 1 } + print "fits" +}' > verdict.txt + +echo "fits" > answer.txt +diff -q --strip-trailing-cr answer.txt verdict.txt