Skip to content

fix(text): load font atlases from explicit metrics and image URLs - #713

Merged
stormmuller merged 2 commits into
devfrom
claude/sharp-carson-2922ju
Oct 6, 2026
Merged

stormmuller merged 2 commits into
devfrom
claude/sharp-carson-2922ju

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

Implements design/font-atlas-loading.md.

FontAtlasCache.getOrLoad(jsonUrl) used to find the atlas image by reading atlasImage from the JSON and resolving it next to the JSON URL. That breaks under any bundler that hashes asset names, and the default font had no exports path a bundler could import.

  • FontAtlasCache: getOrLoad({ metricsUrl, imageUrl }), get(metricsUrl).
    • Keyed by metricsUrl. Concurrent calls share one load. A call with the same metrics and a different image URL rejects, whether the first load is still running or finished. A failed load can be retried.
    • The JSON fetch and the image load now run in parallel.
    • The image's natural size is checked against atlasSize; on a mismatch it rejects, naming both URLs.
    • load/assets are private; it no longer implements AssetCache. resolveRelativeToKey is deleted.
  • Data: atlasImage is removed from FontAtlasData, FontAtlasFileData, the validator, the generator and assets/fonts/default/default.json. Old files that still have it load as before, because the validator ignores unknown fields. formatVersion stays 2.
  • Exports: new ./fonts/default/default.json and ./fonts/default/default.png subpaths. check-exports excludes those two entrypoints, since attw checks TypeScript resolution and a PNG can't have types.
  • Docs-site demos (13): import the default font through the new subpath instead of keeping a copy in static/fonts/default, which is deleted.
    • JSON: new URL('@forge-game-engine/forge/fonts/default/default.json', import.meta.url).
    • PNG: imported as a module. Docusaurus's legacy file-loader also processes new URL image requests and emits JavaScript as the .png. Found while testing; the webpack section of the guide now mentions it.
    • Added a *.png module declaration for the import.
  • e2e: fixed the stale synthetic atlas in text-effects-overlap.ts (formatVersion: 1, missing capHeight). check-types:e2e now passes.
  • Guides: text/index.md, text/loading-a-font-atlas.md (Vite and webpack examples, plus gotchas), text/rendering-text.md, text/generating-a-font-atlas.md, ui/labels-and-text.md, asset-loading/index.md. The "copy the files out of node_modules" instructions are gone.

Plan review (solution-reviewer): REVISE, three items:

  1. How check-exports passes: excluded the two asset entrypoints.
  2. Webpack: moved the docs site onto the exports subpath and added a webpack example to the guide.
  3. ImageCache.getOrLoad doesn't share concurrent loads either. Left as a follow-up, since it's outside this design.

Its optional "delete get" suggestion was not taken, because the design keeps get as public API.

Definition of done, verified:

  • A Vite 8 production build that imports @forge-game-engine/forge/fonts/default/default.json?url and .png loads the atlas from hashed URLs (95 glyphs, 512px image).
  • The docs-site production build emits both files under hashed names, and all 13 font demos load and render them, under both docusaurus serve and docusaurus start.

Related issue(s)

Design: design/font-atlas-loading.md. The Galactic Journey follow-up is in a separate PR in Forge-Game-Engine/demo and needs the release that ships this.

Verification checklist

  • npm run check-types passes with 0 errors (and check-types:e2e)
  • npm test passes (1797 tests)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • Any new/changed public API is exported from the module's index.ts (FontAtlasUrls via font-atlas-cache.ts)
  • Documentation under /documentation-site/docs/docs is updated
  • Demos updated and verified:
    • root npm run build
    • docs-site npm run typecheck and npm run build
    • all 13 font demos loaded in Chromium: no engine errors, text renders. The only console error was the external Font Awesome script, which the sandbox proxy blocks.
  • Full e2e suite: 38/38 pass

Changelog

  • Bullets added under ## [Unreleased] in CHANGELOG.md:
    • #### Added: the default font's exports subpath
    • #### Changed: the new getOrLoad signature, with migration notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01BnSddjDV7weKQBwDYkmgNA


Generated by Claude Code

FontAtlasCache.getOrLoad now takes { metricsUrl, imageUrl } instead of
resolving the image named in the JSON's atlasImage field next to the JSON,
which broke under bundlers that hash asset names. Concurrent loads of the
same atlas are shared, a conflicting image URL for the same metrics
rejects, and the image's size is checked against atlasSize.

atlasImage is removed from the data, the validator and the generator. The
default font is exported as @forge-game-engine/forge/fonts/default/*.json
and *.png so bundlers can import it, and the docs-site demos now import it
that way instead of keeping a static copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnSddjDV7weKQBwDYkmgNA
@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/text/font-atlas/font-atlas-cache.ts 96.55% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@stormmuller
stormmuller enabled auto-merge (squash) October 6, 2026 18:03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnSddjDV7weKQBwDYkmgNA
@stormmuller
stormmuller merged commit 804d37a into dev Oct 6, 2026
12 checks passed
@stormmuller
stormmuller deleted the claude/sharp-carson-2922ju branch October 6, 2026 18:14
stormmuller pushed a commit that referenced this pull request Oct 6, 2026
…j6xxo

Brings in explicit font atlas URLs (#713) and HDR colors (#710). The game
states demo loads the default font atlas from the package, as the other
demos now do, since static/fonts is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE
stormmuller added a commit that referenced this pull request Oct 6, 2026
…es (#711)

* feat(states): add game states, run conditions and state-scoped entities

Implements design/game-states.md.

- ecs: `runIf` on `addSystem` and `addSystemGroup`, checked just before
  the system or group runs; a gated system isn't queried.
- ecs: a built-in `firstSystemGroup` that runs before every other group.
  Groups ordered after it run at the start of the tick, before every other
  group, so a state's exit and enter groups run before input and gameplay.
- states: new module with `createGameState`, `inState`/`onEnter`/`onExit`
  and `addStateScopedComponent`.
- Docs: run conditions and the first group in the ECS guides, a new Game
  States guide and a game states demo.
- design/game-states.md: records the start-of-tick placement the exit and
  enter groups need, and the scoped-removal group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* Merge remote-tracking branch 'origin/dev' into claude/serene-mendel-1j6xxo

Brings in explicit font atlas URLs (#713) and HDR colors (#710). The game
states demo loads the default font atlas from the package, as the other
demos now do, since static/fonts is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): link the demo where the guide uses it, not in the intro

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): name the example state type GameStateName, not Screen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): say what happens without runIf, drop the dependency-injection aside

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): describe run conditions plainly instead of as gates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): describe paused systems without implying they hold state, drop round-specific wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): drop the Time aside from the run conditions section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): drop the input section from the game states guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): describe enter and exit systems as reacting to transitions, not setting up a state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): drop the several-worlds section from the game states guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(skills): make document-feature produce isolated, literal technical documentation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): rewrite the game states docs as isolated technical reference

Applies the document-feature skill to the game states guide, the
state-scoped entities guide, the run conditions section of the System
guide, the first group section of the World guide, and the GameState and
firstSystemGroup JSDoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(skills): document-feature writes feature guides, leaving per-member detail to the API reference

The skill now separates guides (the user manual) from the generated API
reference, requires an outline of headings named after the feature's
concepts and tasks, and bans generic headings such as "Worked example",
"Gotchas" and "Common mistakes". The game states guide is rewritten to it
as a single page; the separate state-scoped entities page is folded in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

* docs(states): leave entity removal behavior to the World guide

The game states guide no longer describes what happens to a removed
entity's children; that's removal and parenting behavior, documented
where those features are. The document-feature skill gains the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants