48886c882e7198f01b7c6bcaffa9ef107a291538
braney
  Tue Jul 21 13:38:08 2026 -0700
hgTracks: merge pixel-adjacent blocks when drawing direction barbs

Chain tracks show direction chevrons only on their aligned blocks, not on
the connector line between blocks.  When zoomed out the blocks smash
together and individually most are too narrow to hold a chevron, so a chain
showed only a couple of chevrons and its orientation was nearly invisible.

Accumulate blocks that touch in pixel space into a single span and draw the
barbs across the whole span, so the chevrons run continuously across a run
of smashed-together blocks.  The gap under which blocks merge is the new
hg.conf setting barbMergePixels (default 3); set it to 0 to disable merging
and get the old per-block barbs.  Only pixel-adjacent blocks merge, so genes
with visible introns and everything else render unchanged, refs #37889

diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c
index 515b60ed6b0..ab5191f33b5 100644
--- src/hg/hgTracks/simpleTracks.c
+++ src/hg/hgTracks/simpleTracks.c
@@ -4404,30 +4404,41 @@
 if (!hideArrows)
     {
     if ((intronGap == 0) && (vis == tvFull || vis == tvPack))
 	{
 	if (lf->highlightColor && (lf->highlightMode == highlightOutline))
 	    clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing,
                          lf->orientation, lf->highlightColor, FALSE);
         else
             clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing,
                          lf->orientation, bColor, FALSE);
         }
     }
 
 components = (lf->codons && zoomedToCdsColorLevel) ? lf->codons : lf->components;
 
+/* For direction barbs, merge blocks that touch in pixel space into a single
+ * span (accumulated in the loop below) so the chevrons run continuously across
+ * them.  This matters for chains, whose blocks smash together when zoomed out:
+ * individually most are too narrow to hold a chevron.  barbRunX1 < 0 means no
+ * run is currently open. */
+static int barbMergePixels = -1;   // max pixel gap between blocks still merged for
+if (barbMergePixels < 0)           // barbs; hg.conf barbMergePixels, 0 disables merging
+    barbMergePixels = atoi(cfgOptionDefault("barbMergePixels", "3"));
+Color barbColor = hvGfxContrastingColor(hvg, color);
+int barbRunX1 = -1;
+int barbRunX2 = -1;
 
 for (sf = components; sf != NULL; sf = sf->next)
     {
     s = sf->start; e = sf->end;
 
     /* Draw UTR portion(s) of exon, if any: */
     if (s < tallStart)
 	{
 	e2 = e;
 	if (e2 > tallStart) e2 = tallStart;
 	if (lf->highlightColor && (lf->highlightMode == highlightOutline))
 	    {
 	    drawScaledBox(hvg, s, e2, scale, xOff, y+shortOff , shortHeight , lf->highlightColor);
 	    drawScaledBox(hvg, s, e2, scale, xOff + 1, y+shortOff + 1, shortHeight - 2, color);
 	    }
@@ -4484,46 +4495,66 @@
                     }
 
 		else
 		    drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color);
                 }
 
             /* Display barbs only if no intron is visible on the item.
                This occurs when the exon completely spans the window,
                or when it is the first or last intron in the feature and
                the following/preceding intron isn't visible */
             if (exonArrowsAlways
             || (  exonArrows
                && (sf->start <= winStart || sf->start == lf->start)
                && (sf->end   >= winEnd   || sf->end   == lf->end)))
                 {
-                Color barbColor = hvGfxContrastingColor(hvg, color);
                 // This scaling of bases to an image window occurs in several places.
                 // It should really be broken out into a function.
-                if (s < winStart)
-                    s = winStart;
-                if (e > winEnd)
-                    e = winEnd;
-                x1 = round((double)((int)s-winStart)*scale) + xOff;
-                x2 = round((double)((int)e-winStart)*scale) + xOff;
-                w = x2-x1;
-                clippedBarbs(hvg, x1+1, midY, x2-x1-2, tl.barbHeight, tl.barbSpacing,
-                             lf->orientation, barbColor, TRUE);
+                int bs = s, be = e;
+                if (bs < winStart)
+                    bs = winStart;
+                if (be > winEnd)
+                    be = winEnd;
+                x1 = round((double)((int)bs-winStart)*scale) + xOff;
+                x2 = round((double)((int)be-winStart)*scale) + xOff;
+                if (barbRunX1 < 0)
+                    {           // start a new run
+                    barbRunX1 = x1;
+                    barbRunX2 = x2;
                     }
+                else if (barbMergePixels > 0 && x1 <= barbRunX2 + barbMergePixels)
+                    {           // this block touches the run: extend it
+                    if (x2 > barbRunX2)
+                        barbRunX2 = x2;
+                    }
+                else
+                    {           // real gap: flush the run and start a new one
+                    clippedBarbs(hvg, barbRunX1+1, midY, barbRunX2-barbRunX1-2,
+                                 tl.barbHeight, tl.barbSpacing, lf->orientation,
+                                 barbColor, TRUE);
+                    barbRunX1 = x1;
+                    barbRunX2 = x2;
                     }
                 }
             }
+	}
+    }
+
+/* Flush the final merged barb run. */
+if (barbRunX1 >= 0)
+    clippedBarbs(hvg, barbRunX1+1, midY, barbRunX2-barbRunX1-2,
+                 tl.barbHeight, tl.barbSpacing, lf->orientation, barbColor, TRUE);
 
 if ((intronGap > 0) || chainLines)
     lfDrawSpecialGaps(lf, intronGap, chainLines, gapFactor,
 		      tg, hvg, xOff, y, scale, color, bColor, vis);
 
 if (vis != tvDense)
     {
     /* If highlighting differences between aligned sequence and genome when
      * zoomed way out, this must be done in a separate pass after exons are
      * drawn so that exons sharing the pixel don't overdraw differences. */
     baseColorOverdrawDiff(tg, lf, hvg, xOff, y, scale, heightPer,
 			  qSeq, qOffset, psl, winStart, drawOpt);
     /* When codons are colored, distribute strand arrows across the exons on top
      * of the boxes (coding exons when too small to label, plus the UTRs). */
     baseColorDrawCdsArrows(tg, lf, hvg, xOff, y, scale, heightPer, winStart, drawOpt, color);