Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
61c3359
fix(ci): simplify package size reporting
thejustinwalsh Aug 10, 2026
44fdd1a
feat(text): bake and load multi-technique fonts
thejustinwalsh Aug 10, 2026
4672396
refactor(r3f): demonstrate explicit text layers
thejustinwalsh Aug 10, 2026
2703806
feat(cli): add text command hierarchy
thejustinwalsh Aug 10, 2026
465bce3
refactor(text): absorb font baker package
thejustinwalsh Aug 10, 2026
63e0da2
feat(bake): prepare font subsets in wasm
thejustinwalsh Aug 10, 2026
6fc4d16
fix(bake): remove HarfBuzz product dependency
thejustinwalsh Aug 10, 2026
c77f7e9
refactor(bake): share Unicode range normalization
thejustinwalsh Aug 10, 2026
3ad0222
docs(agents): require gh stack workflow
thejustinwalsh Aug 10, 2026
6b1d8a5
fix(publish): restore baker release gates
thejustinwalsh Aug 10, 2026
ea049d0
feat(bake): unify runtime font baking
thejustinwalsh Aug 10, 2026
5acc12d
fix(publish): align cache and size policies
thejustinwalsh Aug 10, 2026
87b9506
fix(ci): retain package-owned wasm evidence
thejustinwalsh Aug 10, 2026
4afa148
refactor(r3f): simplify hello world example
thejustinwalsh Aug 10, 2026
6110151
fix(text): retain ligature continuation ownership
thejustinwalsh Aug 10, 2026
5d3bf32
fix(text): batch ellipsized lines without clips
thejustinwalsh Aug 10, 2026
400871e
fix(text): accept reordered glyph cluster offsets
thejustinwalsh Aug 10, 2026
afcc1c9
fix(text): batch bitmap atlas layers
thejustinwalsh Aug 10, 2026
3c113a3
fix(text): refresh retained layout metrics
thejustinwalsh Aug 10, 2026
622e4cc
docs(text): record paragraph runtime corrections
thejustinwalsh Aug 10, 2026
6db3820
fix(text): stabilize retained animated batches
thejustinwalsh Aug 10, 2026
367546f
fix(benchmarks): keep icon grid animation retained
thejustinwalsh Aug 10, 2026
28af3fd
perf(three): patch only changed text transforms
thejustinwalsh Aug 10, 2026
4b93981
fix(r3f): preload text through host commits
thejustinwalsh Aug 10, 2026
0df3b32
feat(react): finalize public entry and example controls
thejustinwalsh Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,23 @@ jobs:
- name: Install locked dependencies
run: pnpm install --frozen-lockfile

- name: Stage package-size presentation
run: |
cp apps/benchmarks/src/benchmark/package-size-summary.ts "$RUNNER_TEMP/package-size-summary.ts"
cp apps/benchmarks/src/benchmark/size-limit-report.mts "$RUNNER_TEMP/size-limit-report.mts"

- name: Compare package delivery sizes with the PR base
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
build_script: build
skip_step: install
package_manager: pnpm
# The action runs this command in both revisions. The inline adapter
# accepts the base branch's older report, derives the same core total,
# and emits Size Limit's stable [{ name, size }] protocol.
# The action checks out both revisions, so stage the head revision's
# presentation adapter outside the worktree before it switches bases.
script: >-
sh -c "node apps/benchmarks/scripts/measure-package-sizes.mts |
node -e \"let input='';process.stdin.on('data',chunk=>input+=chunk).on('end',()=>{const report=JSON.parse(input);const entries=report.entries.filter(entry=>entry.status==='measured');const byId=new Map(entries.map(entry=>[entry.id,entry]));if(!byId.has('renderer-neutral-core-total')){const js=byId.get('browser-core');const wasm=byId.get('text-shaper-wasm');if(js&&wasm)byId.set('renderer-neutral-core-total',{id:'renderer-neutral-core-total',format:'aggregate',gzipBytes:js.gzipBytes+wasm.gzipBytes,brotliBytes:js.brotliBytes+wasm.brotliBytes})}const tracked=/^(browser-core|text-shaper-wasm|renderer-neutral-core-total|three-runtime-js|three-renderer-total|font-(inter|icons)-|delivery-three-)/;const rows=[...byId.values()].filter(entry=>tracked.test(entry.id)).flatMap(entry=>entry.format==='javascript'?[{name:entry.id+' (brotli)',size:entry.brotliBytes}]:entry.format==='aggregate'?[{name:entry.id+' (gzip)',size:entry.gzipBytes},{name:entry.id+' (brotli)',size:entry.brotliBytes}]:[{name:entry.id+' (raw)',size:entry.rawBytes},{name:entry.id+' (gzip)',size:entry.gzipBytes},{name:entry.id+' (brotli)',size:entry.brotliBytes}]);process.stdout.write(JSON.stringify(rows))})\""
node \"$RUNNER_TEMP/size-limit-report.mts\""

check:
name: Check
Expand Down Expand Up @@ -115,8 +119,10 @@ jobs:
with:
name: wasm-failure-evidence
path: |
packages/font-baker/dist/font_baker.wasm
packages/text/dist/font_baker.wasm
packages/text/dist/bitmap_baker.wasm
packages/text/dist/mtsdf_baker.wasm
packages/text/dist/slug_baker.wasm
packages/text/dist/text_shaper.wasm
if-no-files-found: error
retention-days: 7
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Use the repository-local `tsl` skill before implementing or reviewing Three.js S

Use the repository-local `claude-review` skill when invoking Claude Code for an adversarial or external-model review. Keep reviews read-only, stream visible progress, and retain the complete trace in the ignored repository cache instead of launching an opaque buffered subprocess.

Use the repository-local `gh-stack` skill for every dependent branch or pull-request workflow. Create, adopt, navigate,
rebase, push, submit, sync, link, and merge stacks through non-interactive `gh stack` commands; ordinary `git push`,
`gh pr create`, and `gh pr merge` are not substitutes for GitHub Stack state. Always use `gh stack submit --auto` and
`gh stack view --json`, and provide every branch or checkout argument explicitly as required by the skill.

Consult the repository-local `evidence-first` skill as the default style guidance for human-facing engineering communication, including chat updates and final answers, reports, reviews, handoffs, PR and issue prose, READMEs, and technical documentation. It offers situational cues rather than a fixed template. Domain skills still determine the work and valid evidence, `open-knowledge-format` governs bundle structure and provenance, and `diataxis-docs` governs the purpose and top-level structure of reader-facing documentation.

Use these canonical sources instead of creating shadow plans or duplicate status prose:
Expand All @@ -19,7 +24,7 @@ Use these canonical sources instead of creating shadow plans or duplicate status

Update affected canonical documentation in the same change as source. Package source or configuration changes require reviewing the matching package concept, regenerating its `source_digest`, and running `pnpm docs:check`.

Use the exact root toolchain pins through mise. Agent commands must enter that environment explicitly with `mise exec -- pnpm ...` or `mise exec -- <tool> ...`; do not depend on `mise activate` surviving across non-interactive commands. Mise owns tool selection, while pnpm remains the only repository workflow surface. Install workload-scoped mise tools only when their documented pnpm workflow requires them. The dated nightly under `packages/font-baker/fuzz` is isolated to cargo-fuzz. Verify narrowly first, then run the relevant package and repository checks. Keep tests deterministic; do not use sleeps, timer cushions, arbitrary retries, or regenerated goldens as correctness mechanisms.
Use the exact root toolchain pins through mise. Agent commands must enter that environment explicitly with `mise exec -- pnpm ...` or `mise exec -- <tool> ...`; do not depend on `mise activate` surviving across non-interactive commands. Mise owns tool selection, while pnpm remains the only repository workflow surface. Install workload-scoped mise tools only when their documented pnpm workflow requires them. The dated nightly under `packages/text/rust/font-baker-fuzz` is isolated to cargo-fuzz. Verify narrowly first, then run the relevant package and repository checks. Keep tests deterministic; do not use sleeps, timer cushions, arbitrary retries, or regenerated goldens as correctness mechanisms.

Exercise repository workflows through named `pnpm` scripts from the workspace root. Prefer a short root alias for a maintainer-facing application workflow. When a repeatable build, test, profile, capture, generation, or development command is missing, add the package-owned script and root alias before running it; do not leave the working procedure as an agent-only shell recipe or temporary probe.

Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ engine integrations below are implemented and pass their portability gates.
## Render text with React Three Fiber

```tsx
import { Text, TextGroup, useFont } from '@pmndrs/text/r3f';
import { Text, TextGroup, useFont } from '@pmndrs/text/react';
import { mtsdf } from '@pmndrs/text/raster/mtsdf';

function Labels() {
Expand Down Expand Up @@ -253,11 +253,31 @@ await bakeFont({

Baking creates font metrics, glyph records, and technique resources before the application runs. Development fallback can perform the same work in a Worker. Loading remains explicit either way.

For a known local font, the CLI exposes the same path without a project-discovery module:

```sh
pnpm exec text bake --input Inter-Regular.ttf --output Inter.font.glb --bitmap 32 --msdf --slug
```

Add `--unicodes U+0020-007E` to subset the shaping font through the package-owned baker Wasm, or `--check` to rebuild temporarily and
require byte-identical output.

Use the font's retained `post`/CFF glyph names to find icon code points or produce a bake-ready Unicode set:

```sh
pnpm exec text glyphs fa-solid-900.ttf --name globe --json
pnpm exec text glyphs fa-solid-900.ttf --name globe --name earth-americas --unicode-set
```

`text glyphs` uses the package-owned baker Wasm; fonts without authored names still report exact glyph IDs.

### Load, shape, and render

```ts
import { createFontStack, createTextRuntime } from '@pmndrs/text';
import { bitmap } from '@pmndrs/text/raster/bitmap';
import { mtsdf } from '@pmndrs/text/raster/mtsdf';
import { slug } from '@pmndrs/text/raster/slug';

const runtime = await createTextRuntime({
async: {
Expand All @@ -274,6 +294,11 @@ const Noto = await runtime.loadFont({
raster: { technique: mtsdf },
});

const [InterBitmap, InterMsdf, InterSlug] = await runtime.loadFont({
input: { baked: '/fonts/Inter.font.glb' },
rasters: [{ technique: bitmap, options: { strikes: [32] } }, { technique: mtsdf }, { technique: slug }],
});

const UiFont = createFontStack(Inter, Noto);

const paragraphs = runtime.createParagraphBatch({
Expand Down
4 changes: 2 additions & 2 deletions apps/benchmarks/fixtures/fonts/inter-v4.1/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"formatVersion": 0,
"descriptorHash": "f1c0e6c2ead13ceab41f2e39aa883e139fcecfb59c08bee7818242615170331d",
"expectedCore": {
"artifactBytes": 172156,
"artifactSha256": "af7bfb85f04a6a63c6462735a6e8ec6d739576adb354c07ca51e744814db2f7b",
"artifactBytes": 172144,
"artifactSha256": "edf896923f38c9e6080e176540699a7b96b7cd15606b0522447750e7595170b5",
"shapingHash": "6a96d9c6f9e59fd6aeb51848413bd4dd8711730a5479a7d004979d80f3b3cd09",
"shapingSfntBytes": 147192,
"extentsBytes": 23496,
Expand Down
Binary file modified apps/benchmarks/fixtures/rendering/inter-bitmap-16.font.glb
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
{
"fontFixture": "inter",
"file": "inter-bitmap-16.font.glb",
"bytes": 927164,
"sha256": "55e0f03fcd9cec9312b03f58de112d299982e82db368d9a666db120ef8a4f471",
"bytes": 927152,
"sha256": "6ae4795a1398c3a31aaa10d941fa08e5a1545a3ea86e392bc3f641fd1c1f2d9c",
"raster": {
"decodedGpuBytes": 695296,
"pages": [
Expand Down
4 changes: 2 additions & 2 deletions apps/benchmarks/fixtures/results/bake-host-baseline-v0.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"browser": "149.0.7827.55"
},
"artifact": {
"bytes": 172156,
"sha256": "af7bfb85f04a6a63c6462735a6e8ec6d739576adb354c07ca51e744814db2f7b"
"bytes": 172144,
"sha256": "edf896923f38c9e6080e176540699a7b96b7cd15606b0522447750e7595170b5"
},
"offline": {
"coldMedianMs": 4.155290999999977,
Expand Down
3 changes: 1 addition & 2 deletions apps/benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm --filter @pmndrs/text-font-baker build && vite",
"dev": "pnpm --filter @pmndrs/text build && vite",
"build": "node ./scripts/build.mts",
"test": "node ./scripts/test.mts",
"check": "node ./scripts/check.mts"
Expand All @@ -13,7 +13,6 @@
"@base-ui/react": "1.6.0",
"@fontsource-variable/geist": "5.3.0",
"@pmndrs/text": "workspace:*",
"@pmndrs/text-font-baker": "workspace:*",
"@pmndrs/text-glyph-example-raster": "workspace:*",
"@react-three/fiber": "10.0.0-alpha.2",
"class-variance-authority": "0.7.1",
Expand Down
4 changes: 2 additions & 2 deletions apps/benchmarks/scripts/generate-paragraph-cjk-contract.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';

import type { ParagraphStyle } from '@pmndrs/text';
import { createFontBaker } from '@pmndrs/text-font-baker';
import { createFontBaker } from '@pmndrs/text/bake';

import { paragraphLayoutContract } from '../src/benchmark/paragraph-layout-digest.ts';
import {
Expand All @@ -27,7 +27,7 @@ const coverage = Object.values(retained.cases)
.replace(/[\u{FE00}-\u{FE0F}\u{E0100}-\u{E01EF}]/gu, '');
const [source, bakerWasm] = await Promise.all([
readFile(new URL('../fixtures/fonts/noto-sans-cjk-2.004/NotoSansCJKjp-Regular.otf', import.meta.url)),
readFile(new URL('../../../packages/font-baker/dist/font_baker.wasm', import.meta.url)),
readFile(new URL('../../../packages/text/dist/font_baker.wasm', import.meta.url)),
]);
const baker = await createFontBaker(bakerWasm);
const artifact = baker.bake({ source, descriptor: { formatVersion: 0, fontFaceIndex: 0 } }).artifacts[0];
Expand Down
Loading
Loading