# HG changeset patch # User Markus Mützel # Date 1674993307 -3600 # Sun Jan 29 12:55:07 2023 +0100 # Node ID 8ab6f6dbd7c9cdeff704f26f4ecafab2c5c4d9b2 # Parent 5e9bfd2c11b50642ede3e99827f5544ddb812734 print: Add option to merge only subsequent triangles with SVG toolchain (bug #63646). Trying to merge all triangles that are sharing an edge might take a long time. But that might be necessary to avoid hairlines in figures containing patch or surface graphics objects. Allow selecting if no, all, or only subsequent triangles sharing an edge should be merged into a polygon. * src/octave-svgconvert.cc (main): Distinguish between merging no(0), all (2), or only subsequent (1) triangles into polygons. (octave_polygon::reconstruct): Skip numerically expensive part of trying to merge all polygons that are sharing an edge unless the user selected it. * scripts/plot/util/print.m: Document new options for polygon merging with the SVG toolchain. (svgconvert): Call "octave-svgconvert" with the selected polygon merge mode. * scripts/plot/private/__print_parge_opts__.m: Select default value and parse input for new options. diff -r 5e9bfd2c11b5 -r 8ab6f6dbd7c9 scripts/plot/util/print.m --- a/scripts/plot/util/print.m Fri Jan 27 14:27:30 2023 -0800 +++ b/scripts/plot/util/print.m Sun Jan 29 12:55:07 2023 +0100 @@ -132,6 +132,20 @@ ## When using the @option{-painters} renderer, this enables or disables the ## SVG based backend toolchain with enhanced characteristics: ## +## @item -polymerge +## @item -nopolymerge +## @item -polymerge-all +## When using the SVG based backend toolchain @option{-svgconvert}, faces are +## rendered as triangles. In some cases, some viewers might display fine lines +## where those triangles share an edge. These options control whether all +## triangles that share edges are merged into polygons (@options{-polymerge-all} +## which take some time for graphics constisting of many triangles -- including +## line markers), only merge subsequent polygons (@options{-polymerge}), or no +## triangles are merged at all (@options{-no-polymerge}). By default, only +## subsequent triangles sharing an edge are merged, unless the printed figure +## contains patch or surface graphics objects in which case all triangles that +## are sharing an edge are merged. +## ## @table @asis ## @item Font handling: ## For interpreters "none" and "tex", the actual font is embedded in the output @@ -1160,7 +1174,8 @@ cmd = sprintf ('%s - %%s %3.2f "%s" %d "%%s"', ... undo_string_escapes (opts.svgconvert_binary), ... get (0, "screenpixelsperinch"), ... - undo_string_escapes (fullfile (fontdir, "FreeSans.otf")), 1); + undo_string_escapes (fullfile (fontdir, "FreeSans.otf")), + opts.polymerge); if (opts.debug) fprintf ("svgconvert command: '%s'\n", cmd); diff -r 5e9bfd2c11b5 -r 8ab6f6dbd7c9 scripts/plot/util/private/__print_parse_opts__.m --- a/scripts/plot/util/private/__print_parse_opts__.m Fri Jan 27 14:27:30 2023 -0800 +++ b/scripts/plot/util/private/__print_parse_opts__.m Sun Jan 29 12:55:07 2023 +0100 @@ -59,6 +59,7 @@ arg_st.ghostscript.antialiasing_textalphabits = 4; arg_st.ghostscript.antialiasing_graphicsalphabits = 1; arg_st.lpr_binary = __quote_path__ (__find_binary__ ("lpr")); + arg_st.polymerge = 1; arg_st.name = ""; arg_st.orientation = ""; arg_st.pstoedit_binary = __quote_path__ (__find_binary__ ("pstoedit")); @@ -88,6 +89,11 @@ varargin(1) = []; endif + if (! isempty (findall (arg_st.figure, "type", "patch", ... + "-or", "type", "surface"))) + arg_st.polymerge = 2; + endif + for i = 1:numel (varargin) if (! ischar (varargin{i}) && ! iscellstr (varargin{i})) error ("print: input arguments must be a graphics handle or strings."); @@ -127,6 +133,12 @@ arg_st.svgconvert = true; elseif (strcmp (arg, "-nosvgconvert")) arg_st.svgconvert = false; + elseif (strcmp (arg, "-polymerge")) + arg_st.polymerge = 1; + elseif (strcmp (arg, "-nopolymerge")) + arg_st.polymerge = 0; + elseif (strcmp (arg, "-polymerge-all")) + arg_st.polymerge = 2; elseif (strcmp (arg, "-textspecial")) arg_st.special_flag = "textspecial"; elseif (strcmp (arg, "-fillpage")) diff -r 5e9bfd2c11b5 -r 8ab6f6dbd7c9 src/octave-svgconvert.cc --- a/src/octave-svgconvert.cc Fri Jan 27 14:27:30 2023 -0800 +++ b/src/octave-svgconvert.cc Sun Jan 29 12:55:07 2023 +0100 @@ -161,7 +161,7 @@ void reset () { m_polygons.clear (); } - QList reconstruct () + QList reconstruct (int reconstruct_level = 2) { if (m_polygons.isEmpty ()) return QList (); @@ -171,74 +171,77 @@ for (auto it = m_polygons.begin (); it != m_polygons.end (); it++) unused.push_back (false); - bool tryagain = (m_polygons.count () > 1); - - while (tryagain) + if (reconstruct_level > 1) { - tryagain = false; - for (auto ii = 0; ii < m_polygons.count (); ii++) + bool tryagain = (m_polygons.count () > 1); + + while (tryagain) { - if (! unused[ii]) + tryagain = false; + for (auto ii = 0; ii < m_polygons.count (); ii++) { - QPolygonF polygon = m_polygons[ii]; - for (auto jj = ii+1; jj < m_polygons.count (); jj++) + if (! unused[ii]) { - if (! unused[jj]) + QPolygonF polygon = m_polygons[ii]; + for (auto jj = ii+1; jj < m_polygons.count (); jj++) { - QPolygonF newpoly = mergepoly (polygon, m_polygons[jj]); - if (newpoly.count ()) + if (! unused[jj]) { - polygon = newpoly; - m_polygons[ii] = newpoly; - unused[jj] = true; - tryagain = true; + QPolygonF newpoly = mergepoly (polygon, m_polygons[jj]); + if (newpoly.count ()) + { + polygon = newpoly; + m_polygons[ii] = newpoly; + unused[jj] = true; + tryagain = true; + } } } } } } - } - // Try to remove cracks in polygons - for (auto ii = 0; ii < m_polygons.count (); ii++) - { - QPolygonF polygon = m_polygons[ii]; - tryagain = ! unused[ii]; + // Try to remove cracks in polygons + for (auto ii = 0; ii < m_polygons.count (); ii++) + { + QPolygonF polygon = m_polygons[ii]; + tryagain = ! unused[ii]; - while (tryagain && polygon.count () > 4) - { - tryagain = false; - QVector del; + while (tryagain && polygon.count () > 4) + { + tryagain = false; + QVector del; - for (auto jj = 1; jj < (polygon.count () - 1); jj++) - if (polygon[jj-1] == polygon[jj+1]) - { - if (! del.contains (jj)) - del.push_front (jj); + for (auto jj = 1; jj < (polygon.count () - 1); jj++) + if (polygon[jj-1] == polygon[jj+1]) + { + if (! del.contains (jj)) + del.push_front (jj); + + del.push_front (jj+1); + } - del.push_front (jj+1); - } - - for (auto idx : del) - polygon.remove (idx); + for (auto idx : del) + polygon.remove (idx); - if (del.count ()) - tryagain = true; + if (del.count ()) + tryagain = true; + } + m_polygons[ii] = polygon; } - m_polygons[ii] = polygon; - } - // FIXME: There may still be residual cracks, we should do something like - // resetloop = 2; - // while (resetloop) - // currface = shift (currface, 1); - // if (currface(1) == currface(3)) - // currface([2 3]) = []; - // resetloop = 2; - // else - // resetloop--; - // endif - // endwhile + // FIXME: There may still be residual cracks, we should do something like + // resetloop = 2; + // while (resetloop) + // currface = shift (currface, 1); + // if (currface(1) == currface(3)) + // currface([2 3]) = []; + // resetloop = 2; + // else + // resetloop--; + // endif + // endwhile + } QList retval; for (int ii = 0; ii < m_polygons.count (); ii++) @@ -749,7 +752,7 @@ parent_elt.removeChild (orig.at (ii)); } -void reconstruct_polygons (QDomElement& parent_elt) +void reconstruct_polygons (QDomElement& parent_elt, int reconstruct_level) { QDomNodeList nodes = parent_elt.childNodes (); QColor current_color; @@ -787,7 +790,8 @@ if (color != current_color) { // Reconstruct the previous series of triangles - QList polygons = current_polygon.reconstruct (); + QList polygons + = current_polygon.reconstruct (reconstruct_level); collection.push_back (QPair,QList > (replaced_nodes, polygons)); @@ -806,19 +810,20 @@ { if (current_polygon.count ()) { - QList polygons = current_polygon.reconstruct (); + QList polygons = current_polygon.reconstruct (reconstruct_level); collection.push_back (QPair,QList > (replaced_nodes, polygons)); replaced_nodes.clear (); current_polygon.reset (); } - reconstruct_polygons (elt); + reconstruct_polygons (elt, reconstruct_level); } } // Finish collection.push_back (QPair,QList > - (replaced_nodes, current_polygon.reconstruct ())); + (replaced_nodes, + current_polygon.reconstruct (reconstruct_level))); for (int ii = 0; ii < collection.count (); ii++) replace_polygons (parent_elt, collection[ii].first, collection[ii].second); @@ -876,7 +881,10 @@ * fmt: format of the output file. May be one of pdf or svg\n\ * dpi: device dependent resolution in screen pixel per inch\n\ * font: specify a file name for the default FreeSans font\n\ -* reconstruct: specify whether to reconstruct triangle to polygons (0 or 1)\n\ +* reconstruct: specify whether to reconstruct triangle to polygons\n\ + 0: no reconstruction (merging) of polygons\n\ + 1: merge consecutive triangles if they share an edge\n\ + 2: merge all triangles that share edges (might take a long time)\n\ * outfile: output file name\n"; if (strcmp (argv[1], "-h") == 0) @@ -977,8 +985,9 @@ } // Do basic polygons reconstruction - if (QString (argv[5]).toInt ()) - reconstruct_polygons (root); + int reconstruct_level = QString (argv[5]).toInt (); + if (reconstruct_level) + reconstruct_polygons (root, reconstruct_level); // Add custom properties to SVG add_custom_properties (root);