74dc80fd3bd2a70371fa75218347ddc45a556dde
braney
  Wed Aug 5 07:28:59 2026 -0700
Docent: render a tour at print resolution, and make zoom wait for its redraw

refs #37892

scale: k (DOCENT_SCALE=k, make hires [SCALE=3]) renders the same tour with k
times the pixels, for figures that have to print -- a screen still is about
120 dpi across a journal column. Nothing is upscaled; each layer that draws is
asked for more:

* deviceScaleFactor: k with the viewport left at its 1x CSS size, so the page
lays out exactly as at 1x -- same line breaks, same jQuery-dialog width, same
tooltip placement -- and rasterizes with k times the pixels,
* pix x k so the server draws a wider browser image, with textSize stepped up
its ladder to match (3x lands on 24) so hgTracks makes the same layout
decisions in it: same tick spacing, same room for labels, same packing of
features into rows,
* zoom: 1/k on the image table, handing that wider image the 1x amount of
layout space, so one image pixel falls on one device pixel.

A scaled run is stills-only (no mp4), and DOCENT_STILLS names a different parent
so a print render lands beside the screen stills instead of over them.

Everything hgTracks reports about the image -- map-box coords, mouseOver spans,
insideX, a px: drag endpoint -- is in the pixels the SERVER drew, which is not
the displayed size once the image is scaled. Those now go through the image's
natural-to-displayed ratio (1 at 1x). Without it a named mouseover: lands k
times off and pins a different feature.

zoom: in|out waited only for #imgTbl, which the buttons never remove (they
redraw in place via ajax), so the following step could read the previous
window's map boxes and report an item "not found" that was simply not in view
yet. It now waits for the window itself to change. Pre-existing, hidden by the
dwell that FAST removes -- and every hires run is FAST. An item-not-found error
also names the current window and the items that ARE in that row, which is what
told the two cases apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/utils/docent/docent.mk src/hg/utils/docent/docent.mk
index dfd6ef68d86..7925bf6c2d7 100644
--- src/hg/utils/docent/docent.mk
+++ src/hg/utils/docent/docent.mk
@@ -1,56 +1,74 @@
 # Shared make rules for Docent tour scripts (see README.md in this directory).
 #
 # A project that keeps a set of *.docent.yaml scripts includes this file and gets
 # incremental rebuilds: each ../<base>.mp4 is regenerated when its own script — or
 # docent.js itself — is newer. docent.js writes the mp4 and the named stills in one
 # run, so the mp4 stands in for both as the make target.
 #
 # In the project's Makefile:
 #
 #     DOCENT ?= $(HOME)/kent/src/hg/utils/docent/docent.js
 #     include $(dir $(DOCENT))docent.mk
 #
 # then:
 #
 #     make            # build every mp4 whose script (or docent.js) changed
 #     make AP1        # build just ../AP1.mp4 (if stale)
 #     make -B AP2     # force a rebuild
 #     make FAST=1 BP1 # figures only, no video -- roughly a third of the wall clock
 #     make -j6        # scenarios in parallel (each run gets its own browser + cart)
 #     make list       # list the base names discovered
 #     make clean      # remove generated mp4s and stills/
 #
 # Override before the include: FIGDIR (where mp4s land, default ..), PW_ENV (the
 # Playwright runtime), SCRIPTS/BASES (to build an explicit subset).
 
 DOCENT  ?= $(HOME)/kent/src/hg/utils/docent/docent.js
 SCRIPTS ?= $(wildcard *.docent.yaml)
 BASES   ?= $(SCRIPTS:.docent.yaml=)
 FIGDIR  ?= ..
 MP4S    := $(addprefix $(FIGDIR)/,$(addsuffix .mp4,$(BASES)))
 
 # Shared Playwright/Chromium install. Anywhere you have playwright + js-yaml works;
 # at UCSC this is the ~/pwrec tree.
 PW_ENV  ?= PLAYWRIGHT_BROWSERS_PATH=$(HOME)/pwrec/browsers NODE_PATH=$(HOME)/pwrec/node_modules
 
 # FAST=1 -> figures only: no dwells, no cursor animation, no screen recording, no mp4.
 # Same stills, about a third of the wall clock. Use it while iterating on figure content;
 # drop it for the final build that has to produce the videos.
 FAST_ENV = $(if $(FAST),DOCENT_FAST=1 ,)
 
-.PHONY: all list clean $(BASES)
+.PHONY: all list clean hires $(BASES)
 
 all: $(MP4S)
 
 $(FIGDIR)/%.mp4: %.docent.yaml $(DOCENT)
 	$(FAST_ENV)$(PW_ENV) node $(DOCENT) $<
 
+# hires: the same tours rendered for print -- SCALE times the pixels (a wider server image
+# drawn with a bigger track font, the HTML zoomed to match), stills only, written to their
+# own tree so the screen stills and the videos are left alone. Always a full rebuild: a
+# print run is rare and cheap to ask for exactly when it is wanted.
+#
+#   make hires                     # every scenario at 3x -> stills.hires/<base>/
+#   make hires SCALE=2             # 2x
+#   make hires BASES=BP1           # one scenario
+#
+SCALE ?= 3
+HIRES ?= stills.hires
+hires:
+	@for b in $(BASES); do \
+	  echo "=== $$b at $(SCALE)x"; \
+	  DOCENT_SCALE=$(SCALE) DOCENT_STILLS=$(HIRES) DOCENT_FAST=1 $(PW_ENV) \
+	    node $(DOCENT) $$b.docent.yaml || exit 1; \
+	done
+
 # Convenience: `make AP1` -> build ../AP1.mp4
 $(BASES): %: $(FIGDIR)/%.mp4
 
 list:
 	@echo $(BASES)
 
 clean:
 	rm -f $(MP4S)
-	rm -rf stills
+	rm -rf stills $(HIRES)