From 75ebd98e37ccc0559853c23ea851426a8d880dba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:15:11 +0000 Subject: [PATCH 1/4] Initial plan From 816f9b66fd7897b4b80e57370a60420ff7ca1489 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:39:59 +0000 Subject: [PATCH 2/4] Planning enhancement to glyph debugger Co-authored-by: bschlenk <1390303+bschlenk@users.noreply.github.com> --- .../src/components/font-view/glyf-view.tsx | 65 +++- .../components/font-view/glyph-debugger.tsx | 218 ++++++++++++++ .../font-view/glyph-editor-enhanced.tsx | 282 ++++++++++++++++++ .../components/icon-button/icon-button.tsx | 1 + 4 files changed, 560 insertions(+), 6 deletions(-) create mode 100644 packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx create mode 100644 packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx diff --git a/packages/otfjs-ui/src/components/font-view/glyf-view.tsx b/packages/otfjs-ui/src/components/font-view/glyf-view.tsx index 67da3ab..b232303 100644 --- a/packages/otfjs-ui/src/components/font-view/glyf-view.tsx +++ b/packages/otfjs-ui/src/components/font-view/glyf-view.tsx @@ -14,23 +14,76 @@ import { } from 'otfjs' import { rgbaToCss } from '../../utils/color' +import { GlyphDebugger } from './glyph-debugger' import { GlyphEditor } from './glyph-editor' +import { GlyphEditorEnhanced } from './glyph-editor-enhanced' export function GlyfView({ font }: { font: Font }) { const [glyf, setGlyf] = useState(null) + const [view, setView] = useState<'simple' | 'enhanced' | 'debugger'>('simple') const head = font.getTable('head') if (!glyf) { return setGlyf(i)} /> } + const glyph = font.getGlyph(glyf) + return ( - <> - - - +
+
+ +
+
+ + + +
+
+
+ {view === 'simple' && ( + + )} + {view === 'enhanced' && ( + + )} + {view === 'debugger' && ( + + )} +
+
) } diff --git a/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx new file mode 100644 index 0000000..fcb151a --- /dev/null +++ b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx @@ -0,0 +1,218 @@ +import { useMemo, useState } from 'react' +import type { Font } from 'otfjs' +import { GlyphEnriched, glyphToSvgPath, VirtualMachine } from 'otfjs' + +export interface GlyphDebuggerProps { + glyph: GlyphEnriched + font: Font + upem: number +} + +export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) { + const [fontSize, setFontSize] = useState(16) + const [showHinted, setShowHinted] = useState(true) + const [showOriginal, setShowOriginal] = useState(true) + + const vm = useMemo(() => { + const vm = new VirtualMachine(font) + vm.setFontSize(fontSize) + vm.runFpgm() + vm.runPrep() + vm.setGlyph(glyph) + vm.runGlyph() + return vm + }, [font, glyph, fontSize]) + + const hintedGlyph = useMemo(() => vm.getGlyph(), [vm]) + + const width = Math.max(glyph.advanceWidth || glyph.xMax - glyph.xMin, upem) + const height = upem + + return ( +
+
+ + + +
+ +
+
+

Glyph Comparison

+
+ {showOriginal && ( +
+

Original

+ + + + {glyph.points.map((p, i) => ( + + ))} + + +
+ )} + {showHinted && ( +
+

+ Hinted ({fontSize}px) +

+ + + + {hintedGlyph.points.map((p, i) => ( + + ))} + + +
+ )} +
+
+ +
+

Virtual Machine State

+
+
+

Graphics State

+
+
+ Projection Vector: ({vm.gs.projectionVector.x.toFixed(3)},{' '} + {vm.gs.projectionVector.y.toFixed(3)}) +
+
+ Freedom Vector: ({vm.gs.freedomVector.x.toFixed(3)},{' '} + {vm.gs.freedomVector.y.toFixed(3)}) +
+
Round State: {vm.gs.roundState}
+
Auto Flip: {vm.gs.autoFlip ? 'Yes' : 'No'}
+
Loop: {vm.gs.loop}
+
Min Distance: {vm.gs.minimumDistance}
+
+
+ +
+

Reference Points

+
+
rp0: {vm.gs.rp0}
+
rp1: {vm.gs.rp1}
+
rp2: {vm.gs.rp2}
+
+
+ +
+

Zone Pointers

+
+
zp0: {vm.gs.zp0}
+
zp1: {vm.gs.zp1}
+
zp2: {vm.gs.zp2}
+
+
+ +
+

Stack

+
+ {vm.stack.depth() === 0 ? ( +
Empty
+ ) : ( +
+ {Array.from({ length: vm.stack.depth() }, (_, i) => { + const idx = vm.stack.depth() - 1 - i + return ( +
+ [{idx}]: {vm.stack.at(idx)} +
+ ) + })} +
+ )} +
+
+ +
+

CVT Table

+
+
+ {vm.cvt.length === 0 ? ( +
Empty
+ ) : ( + vm.cvt.slice(0, 20).map((value, i) => ( +
+ [{i}]: {value} +
+ )) + )} + {vm.cvt.length > 20 && ( +
+ ... and {vm.cvt.length - 20} more +
+ )} +
+
+
+
+
+
+
+ ) +} diff --git a/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx b/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx new file mode 100644 index 0000000..47d8edc --- /dev/null +++ b/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx @@ -0,0 +1,282 @@ +import { + RefObject, + useCallback, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' +import * as vec from '@bschlenk/vec' +import { GlyphEnriched, glyphToSvgPath, renderGlyphToCanvas } from 'otfjs' + +import { useOriginScale } from '../../hooks/use-origin-scale' + +import styles from './glyph-editor.module.css' + +export interface GlyphEditorEnhancedProps { + glyph: GlyphEnriched + upem: number +} + +export function GlyphEditorEnhanced({ glyph, upem }: GlyphEditorEnhancedProps) { + const ref = useRef(null) + const size = useSize(ref) + const [pixelOffsetX, setPixelOffsetX] = useState(0) + const [pixelOffsetY, setPixelOffsetY] = useState(0) + const [showPixelGrid, setShowPixelGrid] = useState(true) + const [fontSize, setFontSize] = useState(16) + + const center = useMemo( + () => centeredGlyph(glyph, size, upem), + [glyph, size, upem], + ) + const { origin, scale: s } = useOriginScale( + ref as unknown as React.RefObject, + center, + ) + const gScale = fontSize / upem + + const x = origin.x + pixelOffsetX * s + const y = origin.y + pixelOffsetY * s + + const d = glyphToSvgPath(glyph) + + const [gCanvas, gOffset] = useMemo( + () => renderGlyph(glyph, { scale: gScale, antialias: true }), + [glyph, gScale], + ) + + const canvas = useCallback( + (canvas: HTMLCanvasElement | null) => { + if (!canvas) return + + const dpi = window.devicePixelRatio + + canvas.width = size.x * dpi + canvas.height = size.y * dpi + + const ox = gOffset.x + const oy = gOffset.y + + const ctx = canvas.getContext('2d')! + ctx.imageSmoothingEnabled = false + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.scale(dpi, dpi) + ctx.transform(s / gScale, 0, 0, -s / gScale, x, y + glyph.yMax * s) + + ctx.globalAlpha = 0.75 + ctx.drawImage(gCanvas, ox, oy) + + if (!showPixelGrid || s < 0.075) return + + // for each px, clear a 1px line so we can see the individual pixels + for (let i = 1; i < gCanvas.width; ++i) { + ctx.clearRect(i + ox, 0, gScale / s, gCanvas.height + oy) + } + + for (let i = 1; i < gCanvas.height; ++i) { + ctx.clearRect(0, i + oy, gCanvas.width + ox, gScale / s) + } + }, + [size, gOffset, s, gScale, x, y, glyph.yMax, gCanvas, showPixelGrid], + ) + + return ( +
+
+
Rendering Controls
+ + + + + + + + + + +
+ + + + + + + + {s >= 0.075 && } + {s >= 0.075 && ( + + )} + {s >= 0.075 && + glyph.points.map((p, i) => ( + + ))} + + + +
+ ) +} + +function useSize(ref: RefObject) { + const [size, setSize] = useState({ x: 0, y: 0 }) + + useLayoutEffect(() => { + const observer = new ResizeObserver((entries) => { + const { width, height } = entries[0].contentRect + setSize({ x: width, y: height }) + }) + + observer.observe(ref.current!) + + return () => observer.disconnect() + }, [ref]) + + return size +} + +const MARGIN = 32 + +function centeredGlyph(glyph: GlyphEnriched, size: vec.Vector, upem: number) { + const left = Math.min(0, glyph.xMin) + const right = Math.max(upem, glyph.xMax) + const top = Math.max(upem, glyph.yMax) + const bottom = Math.min(0, glyph.yMin) + + const width = right - left + const height = top - bottom + + const sw = size.x - MARGIN * 2 + const sh = size.y - MARGIN * 2 + + const sx = sw / width + const sy = sh / height + const s = Math.min(sx, sy) + + let x = 0 + let y = 0 + + if (sy > sx) { + x = MARGIN + y = (upem - glyph.yMax) * s + (size.y - height * s) / 2 + } else { + x = (size.x - width * s) / 2 - left * s + y = MARGIN + (upem - glyph.yMax) * s + } + + return { origin: { x, y }, scale: s } +} + +const FILTER_ANTI_ALIAS = `url('data:image/svg+xml,\ +\ +\ +\ +\ +\ +\ +#f')` + +interface RenderGlyphOptions { + scale?: number + antialias?: boolean +} + +function renderGlyph( + glyph: GlyphEnriched, + { scale = 1, antialias = false }: RenderGlyphOptions, +) { + const canvas = document.createElement('canvas') + + const gWidth = glyph.xMax - glyph.xMin + const gHeight = glyph.yMax - glyph.yMin + + canvas.width = Math.ceil(gWidth * scale) + 2 + canvas.height = Math.ceil(gHeight * scale) + 2 + + const ctx = canvas.getContext('2d')! + + if (!antialias) { + ctx.filter = FILTER_ANTI_ALIAS + } + + const ox = -Math.floor(glyph.xMin * scale) + 1 + const oy = -Math.floor(glyph.yMin * scale) + 1 + + ctx.transform(scale, 0, 0, scale, ox, oy) + + renderGlyphToCanvas(glyph, ctx) + ctx.fill() + + return [canvas, { x: -ox, y: -oy }] as const +} diff --git a/packages/otfjs-ui/src/components/icon-button/icon-button.tsx b/packages/otfjs-ui/src/components/icon-button/icon-button.tsx index 96c2dd5..848f752 100644 --- a/packages/otfjs-ui/src/components/icon-button/icon-button.tsx +++ b/packages/otfjs-ui/src/components/icon-button/icon-button.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' + import styles from './icon-button.module.css' export function IconButton({ From 49cdf2c97258f065284afa99bc3c4499e48357b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:42:24 +0000 Subject: [PATCH 3/4] Add bytecode debugger with step-through controls to glyph view Co-authored-by: bschlenk <1390303+bschlenk@users.noreply.github.com> --- .../components/font-view/glyph-debugger.tsx | 135 +++++++++++++++++- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx index fcb151a..9dc2129 100644 --- a/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx +++ b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import type { Font } from 'otfjs' -import { GlyphEnriched, glyphToSvgPath, VirtualMachine } from 'otfjs' +import { disassemble, GlyphEnriched, glyphToSvgPath, VirtualMachine } from 'otfjs' export interface GlyphDebuggerProps { glyph: GlyphEnriched @@ -12,22 +12,67 @@ export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) { const [fontSize, setFontSize] = useState(16) const [showHinted, setShowHinted] = useState(true) const [showOriginal, setShowOriginal] = useState(true) + const [debugMode, setDebugMode] = useState(false) + const [debugStep, setDebugStep] = useState(0) + const vmRef = useRef(null) const vm = useMemo(() => { + if (debugMode && vmRef.current) { + return vmRef.current + } const vm = new VirtualMachine(font) vm.setFontSize(fontSize) vm.runFpgm() vm.runPrep() vm.setGlyph(glyph) - vm.runGlyph() + if (!debugMode) { + vm.runGlyph() + } + vmRef.current = vm return vm - }, [font, glyph, fontSize]) + }, [font, glyph, fontSize, debugMode, debugStep]) + + const instructions = useMemo(() => { + if (!glyph.instructions || glyph.instructions.length === 0) { + return [] + } + return disassemble(glyph.instructions) + }, [glyph]) + + const stepInstruction = useCallback(() => { + if (!vm || !glyph.instructions || vm.pc >= glyph.instructions.length) { + return + } + vm.step(glyph.instructions) + setDebugStep((prev) => prev + 1) + }, [vm, glyph]) + + const continueExecution = useCallback(() => { + if (!vm || !glyph.instructions) { + return + } + vm.runGlyph() + setDebugStep((prev) => prev + 1) + setDebugMode(false) + }, [vm, glyph]) + + const resetDebugger = useCallback(() => { + const newVm = new VirtualMachine(font) + newVm.setFontSize(fontSize) + newVm.runFpgm() + newVm.runPrep() + newVm.setGlyph(glyph) + vmRef.current = newVm + setDebugStep((prev) => prev + 1) + }, [font, fontSize, glyph]) - const hintedGlyph = useMemo(() => vm.getGlyph(), [vm]) + const hintedGlyph = useMemo(() => vm.getGlyph(), [vm, debugStep]) const width = Math.max(glyph.advanceWidth || glyph.xMax - glyph.xMin, upem) const height = upem + const hasInstructions = glyph.instructions && glyph.instructions.length > 0 + return (
@@ -38,7 +83,10 @@ export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) { min="8" max="72" value={fontSize} - onChange={(e) => setFontSize(+e.target.value)} + onChange={(e) => { + setFontSize(+e.target.value) + if (debugMode) resetDebugger() + }} className="w-32" /> {fontSize}px @@ -59,6 +107,48 @@ export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) { /> Show Hinted + {hasInstructions && ( + <> +
+ + {debugMode && ( + <> + + + + + )} + + )}
@@ -99,6 +189,7 @@ export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) {

Hinted ({fontSize}px) + {debugMode && ` - PC: ${vm.pc}/${glyph.instructions?.length ?? 0}`}

)}
+ + {hasInstructions && debugMode && ( +
+

Bytecode Instructions

+
+ {instructions.map((inst, idx) => ( +
+ + {inst.pc.toString().padStart(4, '0')} + + + {inst.name} + + {inst.args && ( + + {inst.args.join(' ')} + + )} +
+ ))} +
+
+ )}
From ce3fc06bfd8784c1c1c19d08cd36b7f620f354bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 05:06:50 +0000 Subject: [PATCH 4/4] Fix code review issues - add missing y2 attribute and extract isExecutionComplete variable Co-authored-by: bschlenk <1390303+bschlenk@users.noreply.github.com> --- .../src/components/font-view/glyph-debugger.tsx | 10 ++++++---- .../src/components/font-view/glyph-editor-enhanced.tsx | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx index 9dc2129..b2d003a 100644 --- a/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx +++ b/packages/otfjs-ui/src/components/font-view/glyph-debugger.tsx @@ -123,18 +123,20 @@ export function GlyphDebugger({ glyph, font, upem }: GlyphDebuggerProps) { /> Debug Mode - {debugMode && ( + {debugMode && (() => { + const isExecutionComplete = vm.pc >= (glyph.instructions?.length ?? 0); + return ( <> - )} + )})()} )}
diff --git a/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx b/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx index 47d8edc..e0a723c 100644 --- a/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx +++ b/packages/otfjs-ui/src/components/font-view/glyph-editor-enhanced.tsx @@ -165,7 +165,7 @@ export function GlyphEditorEnhanced({ glyph, upem }: GlyphEditorEnhancedProps) { stroke="red" strokeWidth={0.5 / s} /> - + {s >= 0.075 && } {s >= 0.075 && (