Skip to content

Release: LaTeX math rendering/export, FidoCadJ menu icons & ruler, keyboard/export fixes#28

Merged
DanteCpp merged 17 commits into
mainfrom
dev
Jun 4, 2026
Merged

Release: LaTeX math rendering/export, FidoCadJ menu icons & ruler, keyboard/export fixes#28
DanteCpp merged 17 commits into
mainfrom
dev

Conversation

@DanteCpp

@DanteCpp DanteCpp commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Release PR merging the accumulated dev work into main (14 commits).

Highlights

LaTeX math

  • Vendor the MathJax SVG engine for typeset math, with a shared MathLayout core.
  • Render LaTeX on the canvas via MathJax (replacing the old KaTeX overlay) and typeset math in SVG, PDF, and bitmap exports.
  • Remove vendored KaTeX; document MathJax; add e2e coverage.

Canvas & UI parity with FidoCadJ

  • Add the FidoCadJ measuring ruler on right-button drag (decides ruler vs. context menu on right release).
  • Port the FidoCadJ dropdown menu icons.
  • Fix rotated macro/text labels: drawAdvText now replicates Graphics2DSwing.drawAdvText exactly (four orientation/mirror cases), so rendered text matches its bounding box at 90°/270° — fixes the label offsets seen on macros like arduino.hd44780 and schemimultifilari.itad.

Keyboard

  • Match FidoCadJ 0.24.9 shortcuts (language-independent); bind curve to V and correct rectangle shortcut docs.

Export fixes

  • Bitmap export no longer leaves primitive caches in export coordinate space.
  • Bitmap export no longer clips the EXPORT_BORDER margin.
  • Bitmap export no longer draws filled text/math white-on-white.

Testing

  • npx tsc --noEmit clean.
  • Full unit suite: 685 tests passing.

DanteCpp added 17 commits June 3, 2026 17:25
Add a DOM-free, self-contained MathJax SVG renderer vendored as a single
ES module (src/vendor/mathjax/mathjax.mjs, ~1.7MB), built from
scripts/mathjax-svg-entry.mjs via scripts/vendor-mathjax.sh (esbuild over
mathjax-full, a dev-only dependency so runtime deps stay at zero).

The engine exposes tex2mathgeom() — flattened glyph-path geometry in MathJax
units (1000/em, y-down, baseline at 0) ready to place with translate+scale —
and tex2svg(). A typed wrapper (src/vendor/mathjax-shim.ts) mirrors
katex-shim.ts. Malformed LaTeX surfaces an error flag for literal-text
fallback. This is the foundation for replacing KaTeX with a single engine
that renders typeset math on-screen and into SVG/PNG/JPG/PDF exports.
MathLayout splits a FidoCad text string into plain-text and LaTeX-math
segments (porting the $/$$ splitter), renders math segments to glyph
geometry via the vendored MathJax engine, and positions all segments
left-to-right with x-advances. Text advance uses a caller-supplied measure
callback so each backend (canvas, SVG, PDF) applies its own font metrics;
math advance is widthEm*emPx. Malformed LaTeX degrades to literal $...$
text. Shared by the on-screen renderer and exporters so math is consistent
everywhere. Covered by test/graphic/math-layout.test.ts (12 cases).
PrimitiveAdvText now lays out $...$ math with MathLayout and paints it
through a new GraphicsInterface.drawMathSegments: plain text via the font,
math as MathJax glyph Path2D fills + rule rects, sharing the anchor/
orientation/mirror/stretch transform with drawAdvText. Advance and height
come analytically from MathJax, so the zoom-to-fit bounding box and the
selection box (getDistanceToPoint) reflect the typeset math — replacing the
old DOM getBoundingClientRect round-trip.

The KaTeX HTML overlay (TeXOverlay) and its CircuitPanel wiring are removed,
along with TeXRenderer; the renderTeX setting now gates canvas math via the
retained TeXMode flag. Math therefore renders identically on screen and
(next phases) in raster export, which both go through GraphicsCanvas.

All 676 unit tests pass; production build verified.
All exporters now render $...$ as typeset math via the shared MathLayout
instead of emitting the literal source:

- SVG: math segments embed MathJax glyph <path>/<rect> in a scaled <g>;
  plain text stays a <text> element (no-math output byte-identical, fixtures
  unchanged).
- PDF: new SvgPathToPdf converts MathJax glyph d-strings to PDF path
  operators (quadratics→cubics) filled via emitPdfMathRun; plain text keeps
  the original text-run placement (emitPdfTextRun, dx=0 reproduces it).
- Bitmap (PNG/JPG): ExportBitmap enables TeXMode during render so the shared
  GraphicsCanvas path paints math; flag restored afterwards.

PGF/TikZ intentionally keep literal $...$ for the LaTeX compiler to typeset.
Covered by test/export/export-math.test.ts; all 680 unit tests pass.
KaTeX is fully replaced, so delete its vendored dist (src/vendor/katex/ —
mjs + css + 62 font files), the katex-shim, and the now-dead CircuitPanel CSS
import. Repurpose the TeXMode flag doc (it gates canvas math, not an overlay).

Document the new LaTeX-math behaviour in the README: one vendored MathJax SVG
engine drives on-screen, PNG/JPG, SVG, and PDF identically, while PGF/TikZ keep
literal $...$ for LaTeX. Add test/e2e/math-render.test.ts verifying canvas
typesetting, SVG path embedding, and absence of any KaTeX overlay DOM.

Verified end to end: 680 unit tests, full chromium e2e suite, production
build, and a visual canvas capture of a fraction/root/sum/Greek expression.
renderToOffscreen painted the white background by setting ctx.fillStyle
directly, which desynced GraphicsCanvas's cached colour from the real
context. GraphicPrimitive.selectLayer skips re-applying an unchanged colour,
so the first black fill (text glyphs, math paths) inherited the leftover
white fillStyle and rendered invisibly — strokes (lines, borders) were
unaffected, so only fills vanished and the drawing looked partially exported.

Paint the background through the GraphicsCanvas abstraction
(paintWhiteBackground) so the colour cache stays in sync, in both
renderToOffscreen and renderLayerToOffscreen. Pre-existing bug, surfaced
while verifying math export.

Add test/e2e/export-bitmap-render.test.ts: decodes the exported PNG with
pngjs and asserts black text/math ink is present (the visible-canvas
toBlob e2e never exercised renderToOffscreen).
renderToOffscreen offset content by EXPORT_BORDER/2 (via buildExportMapCoords)
but sized the canvas to the raw drawing extent, so the right/bottom border
margin fell outside the canvas and was clipped. Pad the dimensions by
EXPORT_BORDER before computing magnitude and pixel size, matching the vector
exporters (Export.exportHeader). Applies to both renderToOffscreen and
renderLayerToOffscreen; content is now centered with a symmetric margin.
Drawing.draw invalidates each primitive's cached screen coordinates only when
the model is marked changed or the coordinate system differs from that Drawing
instance's last render. Bitmap export renders through its own Drawing instances
into export coordinate space, then left model.changed=false. The panel's
persistent Drawing then saw no change on its next redraw (same zoom) and reused
the stale export-space cache — shrinking/displacing every non-text primitive
(text escaped because PrimitiveAdvText recomputes each draw). A click that
triggered a redraw surfaced it.

Mark the model changed at the end of renderToOffscreen and
renderLayerToOffscreen so the next on-screen render recomputes screen-space
coordinates. Add an e2e asserting the canvas render is unchanged across an
export (pixelmatch).
The Complex curve tool was bound to 'O', but every locale tooltip and
the original FidoCadJ advertise 'V'; pressing V did nothing. Rebind the
tool to V (O is now free; Ctrl+O still opens a file).

The Rectangle tool is correctly bound to 'G', but most locale tooltips
(en, de, es, cs, el, ja, nl, ru, zh) wrongly told users 'R' - which
rotates the selection instead - so the shortcut appeared broken. Fix
those tooltips to 'G' to match the binding and the it/fr locales.

Update unit/e2e tests and the README shortcut table accordingly.
Restore the canonical FidoCadJ 0.24.9 bindings: Complex curve is O (not V),
which an earlier commit had changed. Bind Space to the Selection tool (as
FidoCadJ does, alongside A and Escape) and move fit-to-view to Home.

Toolbar tool tooltips now inject the shortcut letter from a single canonical
map (TOOL_SHORTCUTS) instead of the translated string, so the advertised key
is identical in every language and always matches the real binding.
Copy FidoCadJ 0.24.9's /icons/menu_icons set into public/icons/menu_icons
and show them next to the matching File/Edit/View/Circuit/Help menu items.
Items without an icon keep a 16px spacer so labels stay aligned.
Port FidoCadJ 0.24.9's Ruler: dragging with the right mouse button draws a
green ruler with tick marks and a box showing the measured length in logical
units and millimetres (1 unit = 127 microns). The measurement persists until
the next press; a plain right-click still opens the context menu.
…xtmenu

The contextmenu event fires at right-button mousedown, before any drag is
detected, so the popup briefly appeared at the start of every ruler
measurement. Move the choice to InputHandler's right-button mouseup: show the
context menu (or cancel the active tool) only when the gesture was a plain
click, and keep the ruler when it was a drag. The contextmenu handler now just
suppresses the native browser menu.
The canvas drawAdvText rotated text by +orientation with simplified mirror
handling, so rendered text disagreed with the bounding box computed in
PrimitiveAdvText.draw (which extends text along (cos o, -sin o), i.e. a
-orientation rotation, copied verbatim from FidoCadJ). At 180 deg sin is
zero so it looked correct, but at 90/270 deg labels landed on the wrong
side and read the wrong direction -- the offset seen on rotated macros
such as arduino.hd44780 and schemimultifilari.itad.

Replicate Graphics2DSwing.drawAdvText exactly: four cases for
orientation 0/non-zero x not-mirrored/mirrored, rotating by -orientation
(non-mirror) or +orientation about (-xa, ya) (mirror), drawing at the
(xa, qq+h) anchor so the stretched baseline lands at ya + h*xyfactor.
Commit 2a57a42 rebound Space to the selection tool (FidoCadJ 0.24.9) and
moved fit-to-view to Home, but the zoom-pan e2e test still pressed Space
and asserted the zoom changed. With Space now a no-op for zoom the test
failed (fit == prior zoom).

Press Home for fit-to-view, and make the assertion viewport-robust: zoom
in past 150% (safely above the <=~40% fit for this small drawing in the
fixed 1280x900 viewport), then assert Home drops the zoom below that.
Avoids asserting an absolute fit value or comparing two fits across time,
since loadCircuit auto-fits and the canvas container keeps resizing
briefly after load.
The canvas font string was built as `${size}px ${name}` with no generic
family. Headless WebKit on Linux (the CI runner, which lacks MS core fonts
like "Courier New") renders canvas text as nothing for a missing named
font instead of falling back, so exported bitmaps silently dropped all
fillText text — darkPixelCount was 0. Chromium, Firefox, and macOS WebKit
fall back automatically and were unaffected; the math (Path2D) path was
unaffected too, which is why only plain-text export failed on CI WebKit.

Quote the family and append a monospace generic fallback (the app's
default text font is monospace) in both GraphicsCanvas.setFont (rendering)
and TextCanvas.setFont (width measurement) so the two stay consistent.
Where the named font exists this is a no-op.
The headless WebKitGTK build Playwright ships on Linux does not rasterize
canvas fillText — it yields a blank result regardless of font family or
generic fallback (the monospace fallback added in 91c999f was correct
hygiene but did not change this). Path2D fills render fine, which is why
the typeset-math export and the render-corruption tests still pass on
WebKit, and only the plain-text ink assertion failed.

macOS/iOS Safari and every other engine render fillText correctly (no
Safari exists on Linux), so no real user is affected; this is purely a
limitation of the CI WebKit build. The white-on-white colour-cache desync
regression this test guards is browser-independent and remains covered on
Chromium and Firefox, so skip only this one assertion on WebKit.
@DanteCpp
DanteCpp merged commit 322bbeb into main Jun 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant