diff --git a/.claude/launch.json b/.claude/launch.json index b4c8b3c8a..15324947b 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -2,15 +2,31 @@ "version": "0.0.1", "configurations": [ { - "name": "dev", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["--filter", "@pmndrs/glyph-benchmarks", "run", "dev", "--port", "5273", "--strictPort"], - "port": 5273 + "name": "glyph-site", + "runtimeExecutable": "mise", + "runtimeArgs": [ + "exec", + "--", + "pnpm", + "--filter", + "@pmndrs/glyph-site", + "dev", + "--port", + "5275", + "--strictPort" + ], + "port": 5275 }, { - "name": "r3f", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["--filter", "@pmndrs/glyph-r3f-hello-world", "run", "dev", "--port", "5274", "--strictPort"], + "name": "glyph-docs", + "runtimeExecutable": "npx", + "runtimeArgs": [ + "-y", + "serve", + "site/dist/docs", + "-l", + "5274" + ], "port": 5274 } ] diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/MPLUS1p-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/MPLUS1p-Regular.ttf new file mode 100644 index 000000000..e16cd9361 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/MPLUS1p-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NanumGothic-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NanumGothic-Regular.ttf new file mode 100644 index 000000000..2aa2b2090 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NanumGothic-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSans-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSans-Regular.ttf new file mode 100644 index 000000000..f27f4ff59 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSans-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArabic-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArabic-Regular.ttf new file mode 100644 index 000000000..72c9228c3 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArabic-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArmenian-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArmenian-Regular.ttf new file mode 100644 index 000000000..08069ed0a Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansArmenian-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansBengali-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansBengali-Regular.ttf new file mode 100644 index 000000000..f558175a4 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansBengali-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansDevanagari-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansDevanagari-Regular.ttf new file mode 100644 index 000000000..8dd3ec4ac Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansDevanagari-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansEthiopic-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansEthiopic-Regular.ttf new file mode 100644 index 000000000..8820031ea Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansEthiopic-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGeorgian-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGeorgian-Regular.ttf new file mode 100644 index 000000000..6c5008d91 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGeorgian-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGujarati-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGujarati-Regular.ttf new file mode 100644 index 000000000..cf100cfab Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGujarati-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGurmukhi-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGurmukhi-Regular.ttf new file mode 100644 index 000000000..3b08955a2 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansGurmukhi-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansHebrew-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansHebrew-Regular.ttf new file mode 100644 index 000000000..ef33d06cc Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansHebrew-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKannada-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKannada-Regular.ttf new file mode 100644 index 000000000..68da17f1b Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKannada-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKhmer-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKhmer-Regular.ttf new file mode 100644 index 000000000..5c22ec4e2 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansKhmer-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansLao-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansLao-Regular.ttf new file mode 100644 index 000000000..72cb42407 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansLao-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMalayalam-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMalayalam-Regular.ttf new file mode 100644 index 000000000..32dc2c625 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMalayalam-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMyanmar-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMyanmar-Regular.ttf new file mode 100644 index 000000000..5c0da8c0a Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansMyanmar-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansOriya-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansOriya-Regular.ttf new file mode 100644 index 000000000..aab55a7f3 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansOriya-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansSinhala-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansSinhala-Regular.ttf new file mode 100644 index 000000000..12c260eaf Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansSinhala-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTamil-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTamil-Regular.ttf new file mode 100644 index 000000000..3999af8ea Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTamil-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTelugu-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTelugu-Regular.ttf new file mode 100644 index 000000000..d72413a7c Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansTelugu-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansThai-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansThai-Regular.ttf new file mode 100644 index 000000000..8e1e6381d Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/NotoSansThai-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/OFL.txt b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/OFL.txt new file mode 100644 index 000000000..1becba2bb --- /dev/null +++ b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/OFL.txt @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/apps/benchmarks/fixtures/fonts/noto-sans-v2026/ZCOOLXiaoWei-Regular.ttf b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/ZCOOLXiaoWei-Regular.ttf new file mode 100644 index 000000000..2d8731a23 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/noto-sans-v2026/ZCOOLXiaoWei-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/OFL.txt b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/OFL.txt new file mode 100644 index 000000000..1efea058d --- /dev/null +++ b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Playwrite Project Authors (https://github.com/TypeTogether/Playwrite) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTrad-Regular.ttf b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTrad-Regular.ttf new file mode 100644 index 000000000..add7c2dc2 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTrad-Regular.ttf differ diff --git a/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTradGuides-Regular.ttf b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTradGuides-Regular.ttf new file mode 100644 index 000000000..1d62b9b21 Binary files /dev/null and b/apps/benchmarks/fixtures/fonts/playwrite-us-trad-1.003/PlaywriteUSTradGuides-Regular.ttf differ diff --git a/apps/benchmarks/scripts/sync-noto-sans-fixture.mts b/apps/benchmarks/scripts/sync-noto-sans-fixture.mts new file mode 100644 index 000000000..ee622db53 --- /dev/null +++ b/apps/benchmarks/scripts/sync-noto-sans-fixture.mts @@ -0,0 +1,129 @@ +import { resolve } from 'node:path'; + +import { syncImmutableFixture } from './support/immutable-fixture.mts'; + +// google/fonts ships Noto as variable, which the baker rejects outright with +// UnsupportedVariableFont. The Noto project's own site repository carries hinted +// static builds of every script, so the fixture takes them from there and no +// font instancer — and no Python — enters the repository. +const commit = '3a06b1c521155492df224d33464b3c7b2852d861'; +const directory = resolve('fixtures/fonts/noto-sans-v2026'); +const check = process.argv.includes('--check'); +const files = [ + { + localName: 'NotoSans-Regular.ttf', + remotePath: 'fonts/NotoSans/hinted/ttf/NotoSans-Regular.ttf', + sha256: '478c558ea716033cd60c03438f628dfa75694dcf6b5f6d505a2f05fd2b4f3823', + }, + { + localName: 'NotoSansArabic-Regular.ttf', + remotePath: 'fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf', + sha256: 'bdff3e5659d67e67def05b33f749683b9376ae819d65d3dd62ac4640b3aaef48', + }, + { + localName: 'NotoSansDevanagari-Regular.ttf', + remotePath: 'fonts/NotoSansDevanagari/hinted/ttf/NotoSansDevanagari-Regular.ttf', + sha256: '306b53ecfb182a504dd8a7446093c316387d2fd8dc350d0792ed1753fe0996cd', + }, + { + localName: 'NotoSansHebrew-Regular.ttf', + remotePath: 'fonts/NotoSansHebrew/hinted/ttf/NotoSansHebrew-Regular.ttf', + sha256: 'cdefaf8efd47045f6820928eba84db5bed7557539328952b5f828315485e02ee', + }, + { + localName: 'NotoSansThai-Regular.ttf', + remotePath: 'fonts/NotoSansThai/hinted/ttf/NotoSansThai-Regular.ttf', + sha256: '61cf814eec46b294d6ea4401ac295d0cecd5207bd2331dcc5a15e7301d30ee44', + }, + { + localName: 'NotoSansGeorgian-Regular.ttf', + remotePath: 'fonts/NotoSansGeorgian/hinted/ttf/NotoSansGeorgian-Regular.ttf', + sha256: 'd3e33254b09e7bb2c5cf0f17e554b80462056c5a107097f258d495168c3a9346', + }, + { + localName: 'NotoSansArmenian-Regular.ttf', + remotePath: 'fonts/NotoSansArmenian/hinted/ttf/NotoSansArmenian-Regular.ttf', + sha256: '720df88c332417a235b4d6209d14ec2e2bf4bfe2a954b7453d869ea593bfce1e', + }, + { + localName: 'NotoSansBengali-Regular.ttf', + remotePath: 'fonts/NotoSansBengali/hinted/ttf/NotoSansBengali-Regular.ttf', + sha256: 'b55c62ee531e3214da6c0701daecea89a52ba42db7d8206b92e6b51f397a3193', + }, + { + localName: 'NotoSansTamil-Regular.ttf', + remotePath: 'fonts/NotoSansTamil/hinted/ttf/NotoSansTamil-Regular.ttf', + sha256: '3c0a186feb3c63c7f6d63e1511dcdc144e745ae09b98e217c83f3e317974f6f9', + }, + { + localName: 'NotoSansTelugu-Regular.ttf', + remotePath: 'fonts/NotoSansTelugu/hinted/ttf/NotoSansTelugu-Regular.ttf', + sha256: 'b274780b69d1d23fe84b55e809a152cb2ac5306d33864b1f87622f6971871aae', + }, + { + localName: 'NotoSansKhmer-Regular.ttf', + remotePath: 'fonts/NotoSansKhmer/hinted/ttf/NotoSansKhmer-Regular.ttf', + sha256: 'e66675f2082788f0511a714bef5a1748928294b38c8e286a96ea73a864b5e605', + }, + { + localName: 'NotoSansLao-Regular.ttf', + remotePath: 'fonts/NotoSansLao/hinted/ttf/NotoSansLao-Regular.ttf', + sha256: '0a86e5e1ccfe34ca78c43fac6829dc751b42bcc469272a9a55325aae587bfbe7', + }, + { + localName: 'NotoSansMyanmar-Regular.ttf', + remotePath: 'fonts/NotoSansMyanmar/hinted/ttf/NotoSansMyanmar-Regular.ttf', + sha256: 'fafce4db400bc0b214907ccdbfb0ad2f18a57bfefd08c8a571830b84088cf2fc', + }, + { + localName: 'NotoSansSinhala-Regular.ttf', + remotePath: 'fonts/NotoSansSinhala/hinted/ttf/NotoSansSinhala-Regular.ttf', + sha256: '9e32612d47004552f3125e78648a9e2e7899a216ccd3cefbb93a9b5f4c809feb', + }, + { + localName: 'NotoSansEthiopic-Regular.ttf', + remotePath: 'fonts/NotoSansEthiopic/hinted/ttf/NotoSansEthiopic-Regular.ttf', + sha256: 'f6f7fc379db9438959a2b0527e7a2cf36ea9c84626d56ec444fff37fc24c3c10', + }, + { + localName: 'NotoSansGujarati-Regular.ttf', + remotePath: 'fonts/NotoSansGujarati/hinted/ttf/NotoSansGujarati-Regular.ttf', + sha256: '9b5a7aaeeb649a2e75a49d8b006a1f87db1b61c0df3b001609f4e0725d88dbf6', + }, + { + localName: 'NotoSansKannada-Regular.ttf', + remotePath: 'fonts/NotoSansKannada/hinted/ttf/NotoSansKannada-Regular.ttf', + sha256: '9ad74dc64838c6855b96f671fc08e425a58921b9d0c71712ea79c328a27e6e38', + }, + { + localName: 'NotoSansMalayalam-Regular.ttf', + remotePath: 'fonts/NotoSansMalayalam/hinted/ttf/NotoSansMalayalam-Regular.ttf', + sha256: 'c08de7fa8d032a5d6a4d120fb82c78cec60b362a4e73fa26360d89759ff2a7f9', + }, + { + localName: 'NotoSansGurmukhi-Regular.ttf', + remotePath: 'fonts/NotoSansGurmukhi/hinted/ttf/NotoSansGurmukhi-Regular.ttf', + sha256: '658d0207da305a1411c539a8b0bbeda64d4146e54fb4827facddb890b6b90d74', + }, + { + localName: 'NotoSansOriya-Regular.ttf', + remotePath: 'fonts/NotoSansOriya/hinted/ttf/NotoSansOriya-Regular.ttf', + sha256: 'a16645d056017927406546aa78e4ce15e782fd8783467267b75450453d007415', + }, +] as const; + +await syncImmutableFixture({ + baseUrl: `https://raw.githubusercontent.com/notofonts/notofonts.github.io/${commit}`, + check, + directory, + files, +}); + +/* @workflow +{ + "name": "font:noto-sans:sync", + "summary": "Synchronize the authenticated Noto Sans static fixtures for every scripted language.", + "requirements": "Network access to the pinned source.", + "writes": "Checked-in static fonts." +} +*/ diff --git a/apps/benchmarks/scripts/sync-playwrite-fixture.mts b/apps/benchmarks/scripts/sync-playwrite-fixture.mts new file mode 100644 index 000000000..d0e86655f --- /dev/null +++ b/apps/benchmarks/scripts/sync-playwrite-fixture.mts @@ -0,0 +1,44 @@ +import { resolve } from 'node:path'; + +import { syncImmutableFixture } from './support/immutable-fixture.mts'; + +// google/fonts ships only the variable build, which the baker rejects with +// UnsupportedVariableFont. The designers' own release carries static instances +// at the same commit google/fonts pins, so the fixture takes them from upstream +// and no font instancer enters the repository. +const commit = '02e4e15767f5b6c2109413429fc51879b9507ab4'; +const directory = resolve('fixtures/fonts/playwrite-us-trad-1.003'); +const check = process.argv.includes('--check'); +const files = [ + { + localName: 'PlaywriteUSTrad-Regular.ttf', + remotePath: 'fonts/ttf/PlaywriteUSTrad-Regular.ttf', + sha256: 'ac59d71c6487bee243777be1db788d8051d371e4d6ddf7798576e594908bfcf6', + }, + { + localName: 'PlaywriteUSTradGuides-Regular.ttf', + remotePath: 'fonts/ttf/PlaywriteUSTradGuides-Regular.ttf', + sha256: '1eac92a6a9349a33f9f6829ffc2c3c758ec91536e711427988a74099db395c82', + }, + { + localName: 'OFL.txt', + remotePath: 'OFL.txt', + sha256: '0370f5946020846b0ca65e44950d11411ea3bd3c4c0d755916470f9f96cd8cb8', + }, +] as const; + +await syncImmutableFixture({ + baseUrl: `https://raw.githubusercontent.com/TypeTogether/Playwrite/${commit}`, + check, + directory, + files, +}); + +/* @workflow +{ + "name": "font:playwrite:sync", + "summary": "Synchronize the authenticated Playwrite US Trad fixture.", + "requirements": "Network access to the pinned source.", + "writes": "Checked-in static fonts and license." +} +*/ diff --git a/docs/index.md b/docs/index.md index 5c010a814..133ca9715 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,7 @@ okf_version: '0.2' - [Project README](../README.md) — product overview, API preview, implementation order, and local setup. - [Project brief](planning/project-brief.md) — product outcome, scope, non-goals, and success criteria. - [Canonical roadmap](roadmap/roadmap.md) — implementation sequence, issue-sized milestones, dependencies, and exit gates. +- [Documentation site and landing page](planning/docs-site.md) — the `site/` workspace surface, its two composed static outputs, and the publication decision it defers. - [Merged v0 runtime and bake API](planning/api-shapes.md) — superseded migration fixture; it describes the merged v0 surface that the Rust render-plan cutover deleted. - [Three.js text API](planning/three-api.md) — authoritative Three-native loader, explicit `TextGroup` batching, reusable text across group disposal, retained non-throwing errors, ordering, and lifecycle contract. - [Core text API](planning/core-api.md) — authoritative API for ordered font stacks, batch-owned paragraph handles, identity-preserving capacity changes, fixed-capacity failure, synchronized updates, and renderer-ready glyph batches. diff --git a/docs/log.md b/docs/log.md index ab3f01ac2..e1d7ebad4 100644 --- a/docs/log.md +++ b/docs/log.md @@ -114,6 +114,26 @@ its hand-written identity builder, its parallel-array assertion over six public arrays, and its double-snapshot staleness re-check: 271 lines to 204, with the three things it was compensating for now guaranteed by construction. +- **The published site becomes a third top-level surface, and the wordmark becomes a proof** — `site/` joins + `packages/` and `apps/` because a deployable web surface is neither a library nor an example, and root + `docs/` is already the knowledge bundle, so the pmndrs convention of MDX-at-`docs/` moves one level down to + `site/docs`. One private package composes two static outputs into `site/dist` exactly as GitHub Pages serves + them; uikit's `static.yml` is the precedent for merging a reusable-workflow artifact with sibling outputs + under one `deploy-pages`. Nothing publishes yet, so no workflow file ships and there is nothing to disable. + Three capability questions were answered from source rather than assumed. Contour hole-punching does not + exist and is not close: the v1 boundary states plainly that body text cannot flow around a contour, so the + landing composition ships three justified columns with no exclusions at all, and Milestone 12's core — + authored two-dimensional exclusions, not glyph-ink contours — is committed immediately after (D-259). + Multi-column justified flow and `defineTextMaterial` both already exist and carry the design. And + `@react-three/postprocessing` cannot: it wraps the WebGL `EffectComposer`, so the WebGPU path is + `useRenderPipeline` with TSL display nodes, which is what paris-site does and why it carries no such + dependency. Two earlier readings were wrong and are corrected here: the reusable docs workflow composes fine + rather than owning the artifact, and drei ships a matching `11.0.0-alpha.5` under its `alpha` tag — the + incompatible peer range came from reading `latest`. Playwrite is variable-only in `google/fonts`, which the + baker rejects outright, but the upstream project ships statics at the same pinned commit, so no font + instancer and no Python toolchain enter the repository. The hero sets `glÿph` decomposed as `y` + `U+0308`: + the diaeresis is decorative and phonetically inert, and encoding it decomposed makes the wordmark its own + GPOS mark-attachment proof. ## 2026-08-18 diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index bb1e4d69b..8b310d353 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur resource: ../../apps/benchmarks workspace_package: '@pmndrs/glyph-benchmarks' documentation_type: reference -source_digest: 'sha256:b9c76d97e56f3245ce979687973d1472ea581b50fd9e3f22a3f8b419c9e1869b' +source_digest: 'sha256:6f339759add78dbf8c316c058b9f5cedfc2b88f87c869cbbfaf938b7bfb9b76d' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest diff --git a/docs/packages/glyph.md b/docs/packages/glyph.md index a784c81fc..eece816d7 100644 --- a/docs/packages/glyph.md +++ b/docs/packages/glyph.md @@ -5,7 +5,7 @@ description: Implements portable font loading, retained Rust shaping and layout, resource: ../../packages/glyph workspace_package: '@pmndrs/glyph' documentation_type: reference -source_digest: 'sha256:ef2e753861cc155d1b53842529718e4dad5066e9aa88c55b2fb95c6a679605de' +source_digest: 'sha256:8db072a8c3d465cb7f98e4ae0b40e192b2641ec2fac4d69c120f6354c01bc629' tags: [package, public-api, rust, wasm, threejs, typography] sources: - id: manifest diff --git a/docs/planning/decision-register.md b/docs/planning/decision-register.md index 1e8da6ac8..d8ea138f0 100644 --- a/docs/planning/decision-register.md +++ b/docs/planning/decision-register.md @@ -79,6 +79,7 @@ Implementation and passing fixtures are evidence, not approval. A proposed row c | D-086 | Raster-baker descriptors must satisfy `JsonValue` in the public TypeScript contract and are still validated as untrusted plugin output; each descriptor/key pair is resolved once and reused for ordering, packaging, and baking. | Accepted | | D-087 | `@pmndrs/glyph` solely owns the optimized font-baker Wasm, Rust source, tooling, and canonical URL. `@pmndrs/glyph/bake` is its Node programmatic surface and `@pmndrs/glyph/runtime-bake` is its explicit browser host; the root graph cannot reach baker or validator code. No second font-baker package is published. | Accepted | | D-016 | The root `rust-toolchain.toml` is the sole product Rust authority. The isolated coverage-fuzz workspace has one exact dated nightly authority because libFuzzer needs unstable compiler instrumentation. Root and nested mise configurations consume their contextual idiomatic files instead of duplicating Rust versions; pnpm and Cargo remain the normal command interface. | Settled for V0 | +| D-259 | The published web surface lives in `site/`, a third top-level category beside `packages/` (libraries) and `apps/` (examples and maintainer tools), because a deployable site is neither. One private package composes two static outputs into `site/dist` exactly as GitHub Pages serves them: a `@pmndrs/docs` MDX export under `/docs` and a WebGPU landing page at `/`. Root `docs/` remains the Open Knowledge Format bundle, so the generator reads `site/docs` rather than the upstream `docs` convention. The landing hero renders the wordmark `glÿph` through the library itself — Slug curves at display scale, a `defineTextMaterial` node material carrying rim, glints, and an emissive floor into TSL bloom, and a three-column justified multi-script field resolved through one ordered font stack — so every visual claim on the page is the library's own output rather than an illustration of it. The diaeresis is decorative and encoded decomposed, which makes the wordmark its own GPOS mark-attachment proof. Nothing publishes until a separate publish decision fixes the base path and the Pages workflow together. The composition uses no exclusions: text that reflows around the wordmark is committed as Milestone 12 core — authored two-dimensional exclusions, not glyph-ink contours — sequenced after the composition lands. | Accepted | D-004/005 follow the established uikit split: the core owns every feature and React only reconciles lifecycle and props. Nested text follows React Native's attributed-text model; direct props and Suspense match uikit/Drei conventions. D-006 makes the short string form canonical while preserving explicit source/baked overrides and preload identity. D-007 fixes native ESM, explicit subpath exports, module workers, and `import()`-based lazy boundaries as package invariants. D-008/009 adopt Koota's value-oriented inference at raster/plugin boundaries without applying type-level models to runtime binary data. See the [API contract](api-shapes.md). diff --git a/docs/planning/docs-site.md b/docs/planning/docs-site.md new file mode 100644 index 000000000..d131df1a2 --- /dev/null +++ b/docs/planning/docs-site.md @@ -0,0 +1,312 @@ +--- +type: Implementation Plan +title: Documentation site and landing page +description: Design for the site/ workspace surface — a pmndrs/docs MDX documentation build and a WebGPU landing page whose hero renders the wordmark through the library itself — composed into one GitHub Pages artifact that stays unpublished until the publish decision. +documentation_type: explanation +status: draft +tags: [planning, site, documentation, landing, three, tsl, slug, deployment] +generated: + by: 'anthropic/claude-opus-5' + at: '2026-08-25T00:00:00Z' +sources: + - id: pmndrs-docs + resource: https://github.com/pmndrs/docs + title: pmndrs MDX static documentation generator + - id: uikit-static + resource: https://github.com/pmndrs/uikit/blob/main/.github/workflows/static.yml + title: uikit multi-artifact GitHub Pages composition + - id: paris-site + resource: https://github.com/pmndrs/paris-site + title: pmndrs paris-site — WebGPU alpha stack consuming @pmndrs/glyph + - id: playwrite + resource: https://github.com/TypeTogether/Playwrite + title: Playwrite type family upstream source + - id: text-material + resource: ../../packages/glyph/src/three/material.ts + title: Renderer-owned text material factory + - id: text-properties + resource: ../../packages/glyph/src/text-properties.ts + title: Paragraph content-box, column, and justification properties + - id: editorial-flow + resource: editorial-flow-layout.md + title: Responsive editorial flow and mixed-raster composition + - id: r3f-hello-world + resource: ../packages/r3f-hello-world.md + title: Minimal public R3F example package +--- + +# Documentation site and landing page + +Status: accepted direction; nothing is published until the publish decision is taken separately. + +## Outcome + +One deployable web surface under `site/`, built from two independent static outputs composed into a single +GitHub Pages artifact: + +``` +/ landing — WebGPU hero rendering the wordmark through @pmndrs/glyph +/docs/* documentation — pmndrs/docs static export from MDX +``` + +The landing page is not decoration around the library. Its hero is the library's own output: exact HarfRust +shaping, a Slug render plan, a renderer-owned node material, and a three-column justified multi-script +composition. Every visual claim on the page is produced by the code the page documents. + +## Why `site/` and not `apps/` or `packages/` + +`packages/*` holds library units. `apps/*` holds runnable examples and maintainer tools, and is expected to +narrow toward examples. A deployable public web surface is neither, so it takes a third top-level category. +`AGENTS.md` gains `site/` alongside `packages/` and `apps/` in its no-root-artifacts rule, and +`pnpm-workspace.yaml` gains the entry. + +Every other `pmndrs/*` repository puts its MDX in root `docs/`. This repository cannot: root `docs/` is the +Open Knowledge Format bundle. `site/docs/` keeps the upstream name one level down, so the generator argument +reads `site/docs` exactly as uikit's reads `docs`. + +## Layout + +``` +site/ +├─ package.json @pmndrs/glyph-site — private; owns build, preview, check +├─ docs/ pure MDX; the generator's input folder +│ └─ getting-started/introduction.mdx +├─ landing/ Vite + React 19 + R3F application +│ ├─ index.html +│ ├─ vite.config.ts +│ ├─ assets/ baked .font.glb files and the license notice +│ └─ src/ +├─ scripts/ +│ └─ preview.mts @workflow site:preview +└─ dist/ gitignored; composed exactly as Pages serves it +``` + +One package rather than two. `build` composes `dist/` into the deployed layout — landing at the root, docs +under `/docs` — so a local preview is the real thing. The generator has no watch mode, which makes a preview +that matches production worth more than package isolation. The cost is that Vite dependencies and the +documentation build share one manifest; accepted deliberately. + +## Documentation surface + +`@pmndrs/docs` v4 is a published CLI, not a repository to fork. It is invoked through `pnpm dlx` at an exact +pin so a preview is reproducible and a version bump is a reviewable commit. + +```sh +pnpm dlx @pmndrs/docs@4.1.2 build site/docs site/dist/docs --format website \ + --libname '@pmndrs/glyph' --libname-short glyph --icon ♠️ \ + --github https://github.com/pmndrs/glyph \ + --home-redirect /getting-started/introduction +``` + +Every environment variable in the upstream README has a matching flag, so configuration lives in the package +manifest rather than an exported shell block. `--base-path` is omitted until publication, when it must agree +with the landing page's Vite `base`. + +`ICON` accepts either an emoji or a path beginning with `/` resolved against `MDX_BASEURL`. The path form +requires serving the MDX folder, so the emoji form is used until there is a published base URL. + +## Landing surface + +### Stack + +`pmndrs/paris-site` establishes that this alpha family works, and it already consumes this library: + +| dependency | version | evidence | +| --- | --- | --- | +| `@react-three/fiber` | `10.0.0-alpha.3` | the repository pin; see the note below | +| `@react-three/drei` | `11.0.0-alpha.5` | peers `@react-three/fiber: >=10.0.0-0`; ships a `./webgpu` entry | +| `three` | `0.185.1` | repository pin | +| `@pmndrs/glyph` | `workspace:*` | this repository | + +The site tracks the repository's existing `@react-three/fiber` pin rather than setting its own. paris-site, +`packages/glyph`, and `apps/r3f-hello-world` all sit on `10.0.0-alpha.3`, so that is the proven combination, +and drei's alpha.5 peers `>=10.0.0-0`, so nothing here requires moving past it. A single pin also keeps one +reconciler in the install; two versions in one workspace would install two copies. + +Bumping the alpha is deliberately **not** part of this work. It is a core change with a core blast radius — +`packages/glyph`, `apps/benchmarks`, and `apps/r3f-hello-world` all move together, `tests/package/r3f-webgpu.test.mjs` +asserts the pin by exact version, and alpha.4 was measured to break `` type-checking in +`tests/types/r3f-v1-api.test.ts`. That belongs in its own pull request, reviewed as an upgrade, not as a +passenger in a documentation-site branch. + +Post-processing does **not** use `@react-three/postprocessing`, which wraps the WebGL `EffectComposer` and is +incompatible with `three/webgpu` node materials. paris-site carries no such dependency. The WebGPU path is +`useRenderPipeline` from `@react-three/fiber/webgpu` composed with TSL display nodes from +`three/examples/jsm/tsl/display/`. + +The canvas follows `apps/r3f-hello-world`: `Canvas` from `@react-three/fiber/webgpu` with a `fallback` +element, so WebGPU-capable browsers use it and the rest fall back to Three's WebGL2 backend. + +### Hero + +The wordmark is **`glÿph`**, set in Playwrite US Trad Regular through the Slug technique. + +Slug rather than MSDF or bitmap: the wordmark is the largest element on the page and may scale with the +viewport, and Slug renders exact curves at any magnification. paris-site uses MSDF for small billboarded +letters, which is the right choice there and the wrong one here. + +The diaeresis is decorative — a metal umlaut in the Queensrÿche tradition. It carries no phonetic weight, +because a diaeresis marks vowel hiatus and `glyph` has no vowel cluster to break. It is not load-bearing for +pronunciation and is not claimed to be. It earns its place technically instead: the hero text is encoded +**decomposed** as `y` + `U+0308 COMBINING DIAERESIS`, so correct placement over a cursive descender requires +GPOS mark-to-base attachment. The wordmark is therefore its own shaping proof. + +The `g` stays bare. Every Latin orthography that marks a `g` — Maltese `ġ`, Esperanto `ĝ`, Turkish `ğ`, Irish +lenition — marks it to soften it. An unmarked `g` is the strongest available assertion of the hard /ɡ/. + +Material authority is the public extension point: + +```ts +defineTextMaterial((context) => { + // context.shader — the Slug technique's TSL output + // context.position — final renderer-local position with policy transform indirection + // context.createDefaultMaterial() — the technique's own material + return material // any NodeMaterial +}) +``` + +The hero material is a lit `NodeMaterial` with a rim term, animated specular glints, and an emissive floor +that feeds bloom. `components/hero-demo/lettering.tsx` in paris-site is the reference for the plumbing — +`positionNode`, `colorNode`, `opacityNode`, `lightsNode`, `emissiveNode` — not for the look. + +Lighting moves. Post-processing is bloom at minimum, applied through the render pipeline rather than a +per-material approximation. + +#### Guide rules + +`PlaywriteUSTradGuides-Regular.ttf` draws the baseline, x-height, ascender, and descender rules inside each +glyph's advance — the same quantities the layout engine computes. `glyph` spans all four: `g`, `y`, `p` +descend and `l`, `h` ascend. The upstream article also documents the underscore as a key character that +renders guides with no letter, which extends the rules past the word. + +Preferred treatment is two registered layers — rules in a dim blueprint material, letters in the hero +material — which requires the two faces to share advance widths. Same design, guides drawn inside the same +advance, so this is expected to hold, but it is a verification gate below rather than an assumption. The +single-layer fallback bakes only the Guides face and accepts one shared material. + +### Background composition + +Three justified columns of the word in many writing systems, dimmed behind the hero. Public API today: + +```ts +contentBox: { + width: { mode: 'exact', size }, + align: 'justify', + columns: { count: 3, gap }, + justify: { maxWordSpaceRatio }, +} +``` + +Columns fill in order without balancing, so the final column may run short; acceptable for a decorative field. + +**No exclusions and no hole.** The engine lays out rectangular content boxes and cannot flow text around a +contour — see [the current boundary](editorial-flow-layout.md). The hero sits over the columns. Text that +reflows around the wordmark is committed as follow-on work, below. + +Word set, resolved through one ordered font stack so the engine performs the script fallback rather than the +application selecting fonts per word: + +| language | word | script | +| --- | --- | --- | +| English | glyph | Latin | +| Greek | γλυφή | Greek | +| Russian | глиф | Cyrillic | +| Arabic | حرف | Arabic | +| Hindi | अक्षर | Devanagari | +| Japanese | 文字 | Han | +| Chinese | 字形 | Han | +| Korean | 글자 | Hangul | + +γλυφή is the etymological root of the word and carries a real acute accent. Every mark in the background is +linguistically correct; the one invented mark on the page is the hero's, and it is doing engineering work. +The set exercises RTL, conjuncts, and CJK in a few kilobytes of subsetted font data. + +### Responsive behavior and links + +Column count and type size step with the viewport; box geometry derives from `viewport` rather than fixed +sizes. Navigation — npm, GitHub, docs — is DOM positioned over the canvas, centered below the wordmark, and +remains reachable when the canvas fails to initialize. + +## Fonts and baking + +Playwrite is variable-only in `google/fonts`, and the baker rejects variable input with +`BakeErrorCode::UnsupportedVariableFont`. The upstream project ships static instances at the same commit +`google/fonts` pins, so no font instancing tool and no Python toolchain enter the repository. + +| asset | source | +| --- | --- | +| `PlaywriteUSTrad-Regular.ttf` | `TypeTogether/Playwrite@02e4e157`, `fonts/ttf/` | +| `PlaywriteUSTradGuides-Regular.ttf` | same | +| Inter, Amiri, Noto Devanagari, Noto Sans CJK | existing repository fixtures | + +Fixtures sync through `syncImmutableFixture` with SHA-256 pins and checked-in licenses, matching +`apps/benchmarks/scripts/sync-amiri-fixture.mts`. Baking uses `pnpm exec glyph bake` with `--unicodes` +restricted to the exact codepoints rendered, plus `bake:check` for byte-identical rebakes, matching +`apps/r3f-hello-world`. A combined license notice ships with the built site. + +## Composition and deployment + +GitHub Pages deploys one artifact. uikit's `static.yml` establishes the merge: the reusable +`pmndrs/docs/.github/workflows/build.yml` uploads its own `github-pages` artifact, a downstream job untars it +alongside sibling outputs, re-tars the whole tree, and `actions/deploy-pages` is pointed at the merged +artifact. This repository would pin `@v4` rather than uikit's `@v2`. + +**No workflow file ships in this work.** Nothing is published, so there is nothing to disable. CI lands with +the publish decision, together with the base-path choice it depends on: + +| | project pages | custom domain | +| --- | --- | --- | +| Vite `base` | `/glyph/` | `/` | +| `--base-path` | `/glyph/docs` | `/docs` | + +Both builds must read the same value. Local preview uses the custom-domain form. + +## Verification gates + +The package `check` is the cheap MDX fragment compile plus formatting; the full static export belongs to +`build`. Beyond that, these must be proven rather than assumed: + +1. **Mark attachment** — `y` + `U+0308` renders identically to precomposed `U+00FF`. `GDEF`/`GPOS` are present + in the font, which is not proof of coverage. Falls back to the precomposed codepoint. +2. **Guide registration** — the Guides and text faces produce identical advances, so two layers register. + Falls back to a single Guides layer. +3. **WebGL2 fallback** — the hero is judged on Three's fallback backend, not only WebGPU. CI runners have no + WebGPU, so this is also the only path CI can observe. +4. **drei under WebGPU** — each drei component used is verified against the alpha pair rather than assumed + from paris-site's usage of different components, and against alpha.4 rather than the alpha.3 it is proven on. +5. **Deterministic bakes** — `bake:check` passes byte-identically. +6. **Payload** — the subsetted multi-script set is measured against the page budget, not estimated. + +## Committed follow-on: Milestone 12 flow regions + +Once the composition is right, the hole is built rather than faked. What this page wants is Milestone 12's +**core** — conservative line-box collision against explicit authored two-dimensional exclusions — not the +tier deferred beyond it, which is deriving exclusions automatically from glyph ink or rendered pixels. + +The shaping engine is not the missing piece; the flow planner between measurement and positioning is. There +is a head start: `columns` already flows one paragraph through ordered slots, and D-197 already describes +slot-local alignment and justification. What is missing is more than one usable interval on a single baseline +plus per-band geometry. + +It remains milestone-sized rather than task-sized, because it lands in the most invariant-dense code in the +repository: the fit loop moves from one available width per line to per-slot fragment continuation, fragment +emission must keep reading order separable from visual order, UAX #9 reordering across a split baseline is the +hard correctness case, and the fixture re-pin cascade is a costed activity here rather than a side effect. + +This plan commits to sequencing it after the composition lands, on its own merits as a headline capability. +The landing page does not block on it, and nothing built now is discarded when it arrives: authored content +boxes become authored exclusions and the page reads strictly better. + +## Delivery + +Stacked pull requests off `main`, through `gh stack`: + +1. **Design and documentation shell** — this document, the roadmap entry, the decision-register entry, + `site/` registration, the MDX shell, and the preview workflow. +2. **React Three Fiber alpha.4** — one workspace-wide bump to `10.0.0-alpha.4`, gated by the existing + `apps/r3f-hello-world` check. Independent of the shell above and a prerequisite for the landing below. +3. **Landing page** — font fixtures, bake scripts, the Vite application, the hero and background composition, + and the composed `site/dist` build. + +Publication — the CI workflow, the base path, and the Pages settings — is a separate decision taken once +these land. diff --git a/docs/planning/index.md b/docs/planning/index.md index c6519b548..5a86ca192 100644 --- a/docs/planning/index.md +++ b/docs/planning/index.md @@ -21,6 +21,7 @@ - [External gpucat integration fitness plan](gpucat-integration.md) — public-surface mapping, external-package boundary, ordering/lifecycle plan, and remaining shader-reuse proof for gpucat. - [Canonical roadmap](../roadmap/roadmap.md) — authoritative implementation order and exit gates. - [uikit integration](uikit-integration.md) — third-party retained-layout integration boundary. +- [Documentation site and landing page](docs-site.md) — the `site/` surface: a pmndrs/docs MDX build under `/docs`, a WebGPU landing hero rendering the wordmark through the library, and the committed Milestone 12 follow-on. ## Data and extension contracts diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 917600577..e638f65b0 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -841,7 +841,7 @@ Deliver: - viewport, column, obstacle, text-editing, typewriter, strike, and display-transform controls with consumer-facing phase, frame, GPU, allocation, and residency evidence; - a reproducible comparison with Pretext that distinguishes approximate browser-compatible line breaking from exact GPU-ready shaping and makes no unmeasured speed claim. -Maintainers intend an editorial piece as a v1 showcase, so the typography that composition depends on is scoped into milestone 11 rather than left here: items 11.12–11.14 cover baked decoration metrics, the break-inserted hyphen contract, and `wordSpacing`, first-line indent, paragraph spacing, and justification controls. This milestone keeps only the flow-region planner itself. +Maintainers intend an editorial piece as a v1 showcase, so the typography that composition depends on is scoped into milestone 11 rather than left here: items 11.12–11.14 cover baked decoration metrics, the break-inserted hyphen contract, and `wordSpacing`, first-line indent, paragraph spacing, and justification controls. This milestone keeps only the flow-region planner itself. The landing page in [the documentation site plan](../planning/docs-site.md) is this milestone's first committed consumer: it ships a three-column justified composition with no exclusions at all, and adopts authored two-dimensional exclusions here so the wordmark reflows the field instead of overlaying it. Contour-tight glyph-ink wrapping, arbitrary rendered-pixel occlusion, balanced columns, automatic hyphenation, vertical flow, and a frozen public flow API remain deferred until the initial integration produces evidence. The [editorial flow research concept](../planning/editorial-flow-layout.md) defines the proposed internal model, benchmark composition, comparison rules, and acceptance gates. @@ -908,6 +908,17 @@ Closes the class of defect behind the displaced-record-slot corruption, in depen - [ ] randomized instanced updates in the benchmark harness, whose monotonic reveal exercises only edits that displace nothing. Exit gate: the differential oracle covers every edit class on every shipped technique and script class; reverting any correctness change in this milestone fails it. +## Documentation site and landing page + +This is product-surface work rather than a library capability milestone, so it sits outside the numbered +sequence above and carries no exit gate against the v1 API. [The site plan](../planning/docs-site.md) owns the +design and D-259 records the decision. Two static outputs compose into one GitHub Pages artifact: a +`@pmndrs/docs` MDX export under `/docs`, and a WebGPU landing page at `/` whose hero renders the wordmark +through the library itself rather than illustrating it. Nothing publishes until a separate publish decision +fixes the base path and the Pages workflow together. + +Its only tie to the sequence above runs in the other direction: the landing page ships without exclusions, and +Milestone 12's authored-exclusion core is committed immediately after the composition lands. ## Roadmap change rule diff --git a/packages/glyph/rust/bitmap-baker/src/rasterize.rs b/packages/glyph/rust/bitmap-baker/src/rasterize.rs index 99216450c..a5db2c4ef 100644 --- a/packages/glyph/rust/bitmap-baker/src/rasterize.rs +++ b/packages/glyph/rust/bitmap-baker/src/rasterize.rs @@ -141,6 +141,12 @@ pub(crate) fn rasterize_strike( let outlines = font.outline_glyphs(); crate::progress::report(progress_offset, progress_total); let mut selected_index = 0_u32; + // A glyph can be absent for two very different reasons: it was not asked + // for, or it was asked for and produced nothing. Counting them apart is + // what lets a raster that rasterised nothing at all be told from one that + // was simply never requested. + let mut requested = 0_u32; + let mut rasterized = 0_u32; for raw_glyph_id in 0..glyph_count { if coverage.is_some_and(|selection| !selection.contains(raw_glyph_id)) { records.mark_absent(raw_glyph_id)?; @@ -151,6 +157,7 @@ pub(crate) fn rasterize_strike( progress_total, ); selected_index = selected_index.saturating_add(1); + requested = requested.saturating_add(1); let glyph_id = GlyphId::new(u32::from(raw_glyph_id)); let Some(outline) = outlines.get(glyph_id) else { records.mark_absent(raw_glyph_id)?; @@ -205,6 +212,25 @@ pub(crate) fn rasterize_strike( atlas, u16::try_from(page_index).map_err(|_| overflow())?, )?; + rasterized = rasterized.saturating_add(1); + } + + // Every requested glyph came back blank. A font of nothing but spaces is not + // a thing anyone bakes, so this is the outline data failing to draw — CFF + // sources reach here — and the caller must hear about it. Left silent the + // bake writes a well-formed artifact with no coverage, text lays out at full + // width and draws holes, and nothing downstream can tell: shaping resolved + // the glyphs, so the layout's own missing-glyph count is honestly zero. + if requested > 0 && rasterized == 0 { + return Err(BitmapBakeError::new( + BitmapBakeErrorCode::InvalidGlyphOutline, + format!( + "no requested glyph produced an outline at {ppem} ppem; \ + {requested} glyph(s) were selected and every one rasterised empty, \ + which usually means this face's outline format is not supported" + ), + ) + .at("/glyphs")); } let mut finished_pages = Vec::with_capacity(pages.len()); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca74a551d..898b007bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -314,6 +314,58 @@ importers: specifier: 4.1.0 version: 4.1.0(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + site: + dependencies: + '@pmndrs/glyph': + specifier: workspace:* + version: link:../packages/glyph + '@react-three/drei': + specifier: 11.0.0-alpha.5 + version: 11.0.0-alpha.5(@react-three/fiber@10.0.0-alpha.3(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.14)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@react-three/fiber': + specifier: 10.0.0-alpha.3 + version: 10.0.0-alpha.3(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + react: + specifier: 19.2.8 + version: 19.2.8 + react-dom: + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) + three: + specifier: 0.185.1 + version: 0.185.1 + devDependencies: + '@types/node': + specifier: 24.13.3 + version: 24.13.3 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@types/three': + specifier: 0.185.4 + version: 0.185.4 + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + leva: + specifier: ^0.10.1 + version: 0.10.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + oxfmt: + specifier: 0.35.0 + version: 0.35.0 + oxlint: + specifier: 1.75.0 + version: 1.75.0 + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.1.5 + version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + packages: '@babel/code-frame@7.29.7': @@ -650,6 +702,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@modelcontextprotocol/sdk@1.30.0': resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} engines: {node: '>=18'} @@ -941,6 +996,217 @@ packages: react-dom: optional: true + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tooltip@1.2.16': + resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.11': + resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + + '@react-three/drei@11.0.0-alpha.5': + resolution: {integrity: sha512-qtMVviEV+OxzgAR/w58hv88N5EjdRdci0y38Im+G5BiJW1+E8+BGVSvfbTvqExgik+rQwcHFDsI4B0mb+wmv5A==} + peerDependencies: + '@react-three/fiber': '>=10.0.0-0' + react: ^19 + react-dom: ^19 + three: '>=0.182' + peerDependenciesMeta: + react-dom: + optional: true + '@react-three/fiber@10.0.0-alpha.3': resolution: {integrity: sha512-5Y5AGzkJiE9iD4PGQXdXv/Al5WgZe7OKM4l/RXE+RrK7zkPMqIwuujlTnb0Gmgg60Qwf/H4cro3lt3ILeaUr3g==} peerDependencies: @@ -1086,6 +1352,11 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stitches/react@1.2.8': + resolution: {integrity: sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==} + peerDependencies: + react: '>= 16.3.0' + '@tailwindcss/node@4.2.1': resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} @@ -1373,6 +1644,14 @@ packages: '@unicode/unicode-17.0.0@1.6.17': resolution: {integrity: sha512-TcBN39y5g7rmQbjnIDsW+4XlEXk6BnJMlhGyUHqUljp00affwlSLv5v7nB37pbdD/ks1BpptvILVQRMp34wPPw==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1478,6 +1757,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + ast-kit@2.2.0: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} @@ -1490,6 +1773,10 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} @@ -1502,6 +1789,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binaryen@129.0.0: resolution: {integrity: sha512-NyF5J0SfRoLDthpPh36FGTycOEv3Eqnkq3+mP5Cqt6iD9BLGGJMEVuPzu81nhLy2MMpPKmRTM9VLZihfyRQv8A==} hasBin: true @@ -1543,6 +1833,12 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} @@ -1572,6 +1868,9 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + colord@2.10.0: + resolution: {integrity: sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -1620,6 +1919,11 @@ packages: typescript: optional: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1687,6 +1991,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1855,6 +2162,14 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1894,6 +2209,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@0.5.0: + resolution: {integrity: sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA==} + engines: {node: '>= 10'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1917,6 +2236,10 @@ packages: flatted@3.4.3: resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1973,6 +2296,10 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1985,6 +2312,9 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gltf-validator@2.0.0-dev.3.10: resolution: {integrity: sha512-odJ4k0tRkGXiDGn78yDBg+fBbAIvBnXxh3RwAta0emSxGtyagFE8B4xELB1oYe3S5RD8Ci3uZAsZaascH2LAEQ==} @@ -2003,6 +2333,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hls.js@1.7.1: + resolution: {integrity: sha512-DlzIkeBAS9IIQ432k3BUf3HlwbsR0+trB1i2lDdN2gUkNkrehFurh0/48M5c1/EjlDkdGng1gwZIpwyPxvdZ/g==} + hono@4.12.32: resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==} engines: {node: '>=16.9.0'} @@ -2065,6 +2398,14 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2102,6 +2443,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -2143,6 +2488,10 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + its-fine@2.0.0: resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} peerDependencies: @@ -2224,6 +2573,12 @@ packages: ktx-parse@1.1.0: resolution: {integrity: sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==} + leva@0.10.1: + resolution: {integrity: sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2394,6 +2749,10 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -2406,6 +2765,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2424,6 +2789,10 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge-value@1.0.0: + resolution: {integrity: sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ==} + engines: {node: '>=0.10.0'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2465,6 +2834,10 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2678,6 +3051,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -2701,11 +3077,26 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + react-colorful@5.8.0: + resolution: {integrity: sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: react: ^19.2.8 + react-dropzone@12.1.0: + resolution: {integrity: sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-use-measure@2.1.7: resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} peerDependencies: @@ -2785,6 +3176,10 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2838,9 +3233,22 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2901,6 +3309,11 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + three@0.185.1: resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} @@ -2942,6 +3355,19 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + troika-three-text@0.52.5: + resolution: {integrity: sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.5: + resolution: {integrity: sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -2955,6 +3381,9 @@ packages: tsover-runtime@0.0.7: resolution: {integrity: sha512-FHHwMJzZbnP23+fU1PxXljy7j0RRRosL2r1/CRUNTqfW+l7m35JsUkM615x/cAzw4GqRdXPIl3axKMj9qzpZtQ==} + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -3060,6 +3489,13 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + v8n@1.5.1: + resolution: {integrity: sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==} + validate-npm-package-name@7.0.2: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3152,6 +3588,12 @@ packages: peerDependencies: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -3209,6 +3651,30 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zustand@5.0.14: resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} @@ -3656,6 +4122,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mediapipe/tasks-vision@0.10.17': {} + '@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)': dependencies: '@hono/node-server': 2.0.12(hono@4.12.32) @@ -3823,6 +4291,207 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.14)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.2.2(@types/react@19.2.14)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.4(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/rect': 1.1.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.3.3(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.14)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.14)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.14)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.3': {} + + '@react-three/drei@11.0.0-alpha.5(@react-three/fiber@10.0.0-alpha.3(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.14)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.185.1) + '@react-three/fiber': 10.0.0-alpha.3(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@use-gesture/react': 10.3.1(react@19.2.8) + camera-controls: 3.1.2(three@0.185.1) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.7.1 + maath: 0.10.8(@types/three@0.185.4)(three@0.185.1) + react: 19.2.8 + stats-gl: 2.4.2(@types/three@0.185.4)(three@0.185.1) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.8) + three: 0.185.1 + three-mesh-bvh: 0.8.3(three@0.185.1) + troika-three-text: 0.52.5(three@0.185.1) + tunnel-rat: 0.1.2(@types/react@19.2.14)(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + utility-types: 3.11.0 + zustand: 5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + '@react-three/fiber@10.0.0-alpha.3(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': dependencies: '@babel/runtime': 7.29.7 @@ -3917,6 +4586,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@stitches/react@1.2.8(react@19.2.8)': + dependencies: + react: 19.2.8 + '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4129,6 +4802,13 @@ snapshots: '@unicode/unicode-17.0.0@1.6.17': {} + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.8)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.8 + '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -4231,6 +4911,8 @@ snapshots: assertion-error@2.0.1: {} + assign-symbols@1.0.0: {} + ast-kit@2.2.0: dependencies: '@babel/parser': 7.29.7 @@ -4242,6 +4924,8 @@ snapshots: atomically@1.7.0: {} + attr-accept@2.2.5: {} + babel-plugin-react-compiler@1.0.0: dependencies: '@babel/types': 7.29.7 @@ -4250,6 +4934,10 @@ snapshots: baseline-browser-mapping@2.11.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binaryen@129.0.0: {} body-parser@2.3.0: @@ -4300,6 +4988,10 @@ snapshots: callsites@3.1.0: {} + camera-controls@3.1.2(three@0.185.1): + dependencies: + three: 0.185.1 + caniuse-lite@1.0.30001806: {} chai@6.2.2: {} @@ -4320,6 +5012,8 @@ snapshots: code-block-writer@13.0.3: {} + colord@2.10.0: {} + commander@11.1.0: {} commander@14.0.3: {} @@ -4363,6 +5057,10 @@ snapshots: optionalDependencies: typescript: 7.0.2 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4404,6 +5102,10 @@ snapshots: dequal@2.0.3: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.1.2: {} diff@8.0.4: {} @@ -4617,6 +5319,15 @@ snapshots: transitivePeerDependencies: - supports-color + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -4651,6 +5362,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@0.5.0: + dependencies: + tslib: 2.8.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4682,6 +5397,8 @@ snapshots: flatted@3.4.3: {} + for-in@1.0.2: {} + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -4733,6 +5450,8 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + get-value@2.0.6: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4743,6 +5462,8 @@ snapshots: globals@16.5.0: {} + glsl-noise@0.0.0: {} + gltf-validator@2.0.0-dev.3.10: {} gopd@1.2.0: {} @@ -4755,6 +5476,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hls.js@1.7.1: {} + hono@4.12.32: {} http-errors@2.0.1: @@ -4798,6 +5521,12 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -4820,6 +5549,10 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-promise@2.2.2: {} is-promise@4.0.0: {} @@ -4846,6 +5579,8 @@ snapshots: isexe@3.1.5: {} + isobject@3.0.1: {} + its-fine@2.0.0(@types/react@19.2.14)(react@19.2.8): dependencies: '@types/react-reconciler': 0.28.9(@types/react@19.2.14) @@ -4904,6 +5639,25 @@ snapshots: ktx-parse@1.1.0: {} + leva@0.10.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@stitches/react': 1.2.8(react@19.2.8) + '@use-gesture/react': 10.3.1(react@19.2.8) + colord: 2.10.0 + dequal: 2.0.3 + merge-value: 1.0.0 + react: 19.2.8 + react-colorful: 5.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) + react-dropzone: 12.1.0(react@19.2.8) + v8n: 1.5.1 + zustand: 3.7.2(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5027,6 +5781,10 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -5037,6 +5795,11 @@ snapshots: dependencies: react: 19.2.8 + maath@0.10.8(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5049,6 +5812,13 @@ snapshots: merge-stream@2.0.0: {} + merge-value@1.0.0: + dependencies: + get-value: 2.0.6 + is-extendable: 1.0.1 + mixin-deep: 1.3.2 + set-value: 2.0.1 + merge2@1.4.1: {} meshoptimizer@1.1.1: {} @@ -5078,6 +5848,11 @@ snapshots: mitt@3.0.1: {} + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + ms@2.1.3: {} nano-spawn@2.1.0: {} @@ -5301,6 +6076,12 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -5324,11 +6105,25 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 + react-colorful@5.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 scheduler: 0.27.0 + react-dropzone@12.1.0(react@19.2.8): + dependencies: + attr-accept: 2.2.5 + file-selector: 0.5.0 + prop-types: 15.8.1 + react: 19.2.8 + + react-is@16.13.1: {} + react-use-measure@2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 @@ -5430,6 +6225,13 @@ snapshots: transitivePeerDependencies: - supports-color + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + setprototypeof@1.2.0: {} shadcn@4.16.0(typescript@7.0.2): @@ -5518,8 +6320,19 @@ snapshots: source-map@0.6.1: {} + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + stackback@0.0.2: {} + stats-gl@2.4.2(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + + stats.js@0.17.0: {} + statuses@2.0.2: {} std-env@4.2.0: {} @@ -5564,6 +6377,10 @@ snapshots: tapable@2.3.3: {} + three-mesh-bvh@0.8.3(three@0.185.1): + dependencies: + three: 0.185.1 + three@0.185.1: {} tiny-invariant@1.3.3: {} @@ -5593,6 +6410,20 @@ snapshots: toidentifier@1.0.1: {} + troika-three-text@0.52.5(three@0.185.1): + dependencies: + bidi-js: 1.0.3 + three: 0.185.1 + troika-three-utils: 0.52.5(three@0.185.1) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.5(three@0.185.1): + dependencies: + three: 0.185.1 + + troika-worker-utils@0.52.0: {} + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -5608,6 +6439,14 @@ snapshots: tsover-runtime@0.0.7: {} + tunnel-rat@0.1.2(@types/react@19.2.14)(react@19.2.8): + dependencies: + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + - react + tw-animate-css@1.4.0: {} type-check@0.4.0: @@ -5716,6 +6555,10 @@ snapshots: util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + + v8n@1.5.1: {} + validate-npm-package-name@7.0.2: {} vary@1.1.2: {} @@ -5766,6 +6609,10 @@ snapshots: playwright: 1.61.1 vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webpack-virtual-modules@0.6.2: {} which@2.0.2: @@ -5813,6 +6660,17 @@ snapshots: zod@3.25.76: {} + zustand@3.7.2(react@19.2.8): + optionalDependencies: + react: 19.2.8 + + zustand@4.5.7(@types/react@19.2.14)(react@19.2.8): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.8 + zustand@5.0.14(@types/react@19.2.14)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): optionalDependencies: '@types/react': 19.2.14 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2ae83f502..2ab856154 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,10 @@ packages: - apps/* - packages/* + - site minimumReleaseAgeExclude: + - '@pmndrs/scheduler@0.2.0' - typegpu@0.12.3 onlyBuiltDependencies: [] diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 000000000..46c8ccc58 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,2 @@ +dist/ +.npx-cache/ diff --git a/site/docs/core-api/introduction.mdx b/site/docs/core-api/introduction.mdx new file mode 100644 index 000000000..175a4e9fd --- /dev/null +++ b/site/docs/core-api/introduction.mdx @@ -0,0 +1,46 @@ +--- +title: Core API +description: Ordered font stacks, batch-owned paragraph handles, identity-preserving capacity changes, and renderer-ready glyph batches. +nav: 0 +sourcecode: site/docs/core-api/introduction.mdx +--- + + + The renderer-neutral core: what loads a font, what owns a paragraph, and what the engine hands a renderer once layout + has committed. + + + + How a font request resolves, and why a raster's options are part of its identity + How an ordered font stack picks a face per cluster + What a paragraph handle owns, and what survives a capacity change + When layout is readable, and what a measurement costs + + +## Runtime and font loading + +{/* What a font request is, what `useFont` and the loader return, and the +lifecycle of a registered font. Worth covering here: raster options form part +of the raster key, so a request whose options differ from the baked artifact +finds no match and falls back to runtime generation. */} + +## Loaded fonts and fallback + +{/* Ordered font stacks: `{ fonts: [...] }`, resolution per cluster, and what +happens when no face in the stack covers a code point. */} + +## Paragraph input + +{/* Content boxes, styles, paint, and spans. `wrap`, `align`, `columns`, +`justify` bounds and `lastLine` live here. */} + +## Layout query values + +{/* `measureLayout()` and `ParagraphLayoutSummary`: `contentWidth`, +`contentHeight`, `firstBaseline`, `lastBaseline`, `glyphCount`, +`missingGlyphCount`, `overflowed` — and what each is measured against. */} + +## Synchronization boundary + +{/* When committed layout is readable, what forces a synchronous query, and the +cost of asking. */} diff --git a/site/docs/getting-started/introduction.mdx b/site/docs/getting-started/introduction.mdx new file mode 100644 index 000000000..65123706e --- /dev/null +++ b/site/docs/getting-started/introduction.mdx @@ -0,0 +1,65 @@ +--- +title: Introduction +description: Portable font baking, Unicode shaping, paragraph layout, and batched text rendering for every Canvas. +nav: 0 +--- + + + `@pmndrs/glyph` shapes and lays out text in Rust/Wasm, then publishes a retained render plan for the active renderer. + The maintained Three.js integration supports Bitmap, MSDF, and Slug through WebGPU and Three's WebGL fallback. + + +## Install + +```sh +npm install @pmndrs/glyph +``` + +## Render text with React Three Fiber + +```tsx +import { Text, TextGroup, useFont } from '@pmndrs/glyph/react'; +import { msdf } from '@pmndrs/glyph/three/msdf'; + +const fontRequest = { + input: { baked: '/fonts/Inter.font.glb' }, + raster: { technique: msdf }, +} as const; + +useFont.preload(fontRequest); + +function Labels() { + const inter = useFont(fontRequest); + + return ( + + + Hello world + + + ); +} +``` + +`Text` is a retained paragraph and a Three `Object3D`. A nested `Text` is a span, and inherits the surrounding +font, style, paint, and material unless it overrides them. + +`TextGroup` is an optional batching and ordering boundary. It collects descendant `Text` objects through the +ordinary scene graph, so regular Three groups may appear between them. A standalone `Text` has the same text +semantics and lazily owns an implicit batch of one. + +## Bake a font + +Fonts are baked ahead of time into a `.font.glb` carrying the shaping data and every raster you ask for. + +```sh +npx glyph bake --input Inter-Regular.ttf --output inter.font.glb \ + --unicodes U+0020-007E --bitmap 32 --msdf --slug +``` + +`--unicodes` subsets the shaping font, so an artifact only carries the coverage you render. diff --git a/site/landing/assets/chorus-armenian.font.glb b/site/landing/assets/chorus-armenian.font.glb new file mode 100644 index 000000000..776c4397e Binary files /dev/null and b/site/landing/assets/chorus-armenian.font.glb differ diff --git a/site/landing/assets/chorus-bengali.font.glb b/site/landing/assets/chorus-bengali.font.glb new file mode 100644 index 000000000..37866f426 Binary files /dev/null and b/site/landing/assets/chorus-bengali.font.glb differ diff --git a/site/landing/assets/chorus-coverage.json b/site/landing/assets/chorus-coverage.json new file mode 100644 index 000000000..1f00db9a9 --- /dev/null +++ b/site/landing/assets/chorus-coverage.json @@ -0,0 +1,179 @@ +{ + "faces": [ + { + "chars": " Gacdefghijklmnoprstyz·íýăάήαγλμρυφабвгзиклнуфіữ", + "glyphs": 48, + "name": "latin", + "points": [ + "U+0020", + "U+0047", + "U+0061", + "U+0063", + "U+0064", + "U+0065", + "U+0066", + "U+0067", + "U+0068", + "U+0069", + "U+006A", + "U+006B", + "U+006C", + "U+006D", + "U+006E", + "U+006F", + "U+0070", + "U+0072", + "U+0073", + "U+0074", + "U+0079", + "U+007A", + "U+00B7", + "U+00ED", + "U+00FD", + "U+0103", + "U+03AC", + "U+03AE", + "U+03B1", + "U+03B3", + "U+03BB", + "U+03BC", + "U+03C1", + "U+03C5", + "U+03C6", + "U+0430", + "U+0431", + "U+0432", + "U+0433", + "U+0437", + "U+0438", + "U+043A", + "U+043B", + "U+043D", + "U+0443", + "U+0444", + "U+0456", + "U+1EEF" + ] + }, + { + "chars": "אהוטירשת", + "glyphs": 8, + "name": "hebrew", + "points": ["U+05D0", "U+05D4", "U+05D5", "U+05D8", "U+05D9", "U+05E8", "U+05E9", "U+05EA"] + }, + { + "chars": "अकणरवष्", + "glyphs": 7, + "name": "devanagari", + "points": ["U+0905", "U+0915", "U+0923", "U+0930", "U+0935", "U+0937", "U+094D"] + }, + { + "chars": "অকরষ্", + "glyphs": 5, + "name": "bengali", + "points": ["U+0985", "U+0995", "U+09B0", "U+09B7", "U+09CD"] + }, + { + "chars": "ਅਖਰੱ", + "glyphs": 4, + "name": "gurmukhi", + "points": ["U+0A05", "U+0A16", "U+0A30", "U+0A71"] + }, + { + "chars": "અકરષ્", + "glyphs": 5, + "name": "gujarati", + "points": ["U+0A85", "U+0A95", "U+0AB0", "U+0AB7", "U+0ACD"] + }, + { + "chars": "ଅକରଷ୍", + "glyphs": 5, + "name": "oriya", + "points": ["U+0B05", "U+0B15", "U+0B30", "U+0B37", "U+0B4D"] + }, + { + "chars": "எதழு்", + "glyphs": 5, + "name": "tamil", + "points": ["U+0B8E", "U+0BA4", "U+0BB4", "U+0BC1", "U+0BCD"] + }, + { + "chars": "ంఅకరష్", + "glyphs": 6, + "name": "telugu", + "points": ["U+0C02", "U+0C05", "U+0C15", "U+0C30", "U+0C37", "U+0C4D"] + }, + { + "chars": "ಅಕರಷ್", + "glyphs": 5, + "name": "kannada", + "points": ["U+0C85", "U+0C95", "U+0CB0", "U+0CB7", "U+0CCD"] + }, + { + "chars": "ംഅകരഷ്", + "glyphs": 6, + "name": "malayalam", + "points": ["U+0D02", "U+0D05", "U+0D15", "U+0D30", "U+0D37", "U+0D4D"] + }, + { + "chars": "අකරු", + "glyphs": 4, + "name": "sinhala", + "points": ["U+0D85", "U+0D9A", "U+0DBB", "U+0DD4"] + }, + { + "chars": "กขรอะั", + "glyphs": 6, + "name": "thai", + "points": ["U+0E01", "U+0E02", "U+0E23", "U+0E2D", "U+0E30", "U+0E31"] + }, + { + "chars": "ກຕນວສອັົ", + "glyphs": 8, + "name": "lao", + "points": ["U+0E81", "U+0E95", "U+0E99", "U+0EA7", "U+0EAA", "U+0EAD", "U+0EB1", "U+0EBB"] + }, + { + "chars": "ករសអ្", + "glyphs": 5, + "name": "khmer", + "points": ["U+1780", "U+179A", "U+179F", "U+17A2", "U+17D2"] + }, + { + "chars": "აოს", + "glyphs": 3, + "name": "georgian", + "points": ["U+10D0", "U+10DD", "U+10E1"] + }, + { + "chars": "առտ", + "glyphs": 3, + "name": "armenian", + "points": ["U+0561", "U+057C", "U+057F"] + }, + { + "chars": "ልደፊ", + "glyphs": 3, + "name": "ethiopic", + "points": ["U+120D", "U+12F0", "U+134A"] + }, + { + "chars": "字形文", + "glyphs": 3, + "name": "japanese", + "points": ["U+5B57", "U+5F62", "U+6587"] + }, + { + "chars": "글리자프", + "glyphs": 4, + "name": "korean", + "points": ["U+AE00", "U+B9AC", "U+C790", "U+D504"] + } + ], + "generated": "site/scripts/bake-chorus.mts", + "totals": { + "faces": 20, + "points": 143, + "words": 45 + } +} diff --git a/site/landing/assets/chorus-devanagari.font.glb b/site/landing/assets/chorus-devanagari.font.glb new file mode 100644 index 000000000..7ad367fde Binary files /dev/null and b/site/landing/assets/chorus-devanagari.font.glb differ diff --git a/site/landing/assets/chorus-ethiopic.font.glb b/site/landing/assets/chorus-ethiopic.font.glb new file mode 100644 index 000000000..2c3d63ed8 Binary files /dev/null and b/site/landing/assets/chorus-ethiopic.font.glb differ diff --git a/site/landing/assets/chorus-georgian.font.glb b/site/landing/assets/chorus-georgian.font.glb new file mode 100644 index 000000000..5811372b1 Binary files /dev/null and b/site/landing/assets/chorus-georgian.font.glb differ diff --git a/site/landing/assets/chorus-gujarati.font.glb b/site/landing/assets/chorus-gujarati.font.glb new file mode 100644 index 000000000..940afd1b7 Binary files /dev/null and b/site/landing/assets/chorus-gujarati.font.glb differ diff --git a/site/landing/assets/chorus-gurmukhi.font.glb b/site/landing/assets/chorus-gurmukhi.font.glb new file mode 100644 index 000000000..ad570df0d Binary files /dev/null and b/site/landing/assets/chorus-gurmukhi.font.glb differ diff --git a/site/landing/assets/chorus-hebrew.font.glb b/site/landing/assets/chorus-hebrew.font.glb new file mode 100644 index 000000000..4e946ba59 Binary files /dev/null and b/site/landing/assets/chorus-hebrew.font.glb differ diff --git a/site/landing/assets/chorus-japanese.font.glb b/site/landing/assets/chorus-japanese.font.glb new file mode 100644 index 000000000..6444ab809 Binary files /dev/null and b/site/landing/assets/chorus-japanese.font.glb differ diff --git a/site/landing/assets/chorus-kannada.font.glb b/site/landing/assets/chorus-kannada.font.glb new file mode 100644 index 000000000..ef5831792 Binary files /dev/null and b/site/landing/assets/chorus-kannada.font.glb differ diff --git a/site/landing/assets/chorus-khmer.font.glb b/site/landing/assets/chorus-khmer.font.glb new file mode 100644 index 000000000..73e14c07f Binary files /dev/null and b/site/landing/assets/chorus-khmer.font.glb differ diff --git a/site/landing/assets/chorus-korean.font.glb b/site/landing/assets/chorus-korean.font.glb new file mode 100644 index 000000000..99cc39daa Binary files /dev/null and b/site/landing/assets/chorus-korean.font.glb differ diff --git a/site/landing/assets/chorus-lao.font.glb b/site/landing/assets/chorus-lao.font.glb new file mode 100644 index 000000000..9d2c6e40e Binary files /dev/null and b/site/landing/assets/chorus-lao.font.glb differ diff --git a/site/landing/assets/chorus-latin.font.glb b/site/landing/assets/chorus-latin.font.glb new file mode 100644 index 000000000..9f66e5f9d Binary files /dev/null and b/site/landing/assets/chorus-latin.font.glb differ diff --git a/site/landing/assets/chorus-malayalam.font.glb b/site/landing/assets/chorus-malayalam.font.glb new file mode 100644 index 000000000..0b325378a Binary files /dev/null and b/site/landing/assets/chorus-malayalam.font.glb differ diff --git a/site/landing/assets/chorus-oriya.font.glb b/site/landing/assets/chorus-oriya.font.glb new file mode 100644 index 000000000..5339b572f Binary files /dev/null and b/site/landing/assets/chorus-oriya.font.glb differ diff --git a/site/landing/assets/chorus-sinhala.font.glb b/site/landing/assets/chorus-sinhala.font.glb new file mode 100644 index 000000000..ce434aac1 Binary files /dev/null and b/site/landing/assets/chorus-sinhala.font.glb differ diff --git a/site/landing/assets/chorus-tamil.font.glb b/site/landing/assets/chorus-tamil.font.glb new file mode 100644 index 000000000..abddbba7a Binary files /dev/null and b/site/landing/assets/chorus-tamil.font.glb differ diff --git a/site/landing/assets/chorus-telugu.font.glb b/site/landing/assets/chorus-telugu.font.glb new file mode 100644 index 000000000..3d97080b4 Binary files /dev/null and b/site/landing/assets/chorus-telugu.font.glb differ diff --git a/site/landing/assets/chorus-thai.font.glb b/site/landing/assets/chorus-thai.font.glb new file mode 100644 index 000000000..0e5306d91 Binary files /dev/null and b/site/landing/assets/chorus-thai.font.glb differ diff --git a/site/landing/assets/playwrite-glyph.font.glb b/site/landing/assets/playwrite-glyph.font.glb new file mode 100644 index 000000000..4027e2ab8 Binary files /dev/null and b/site/landing/assets/playwrite-glyph.font.glb differ diff --git a/site/landing/index.html b/site/landing/index.html new file mode 100644 index 000000000..10c2c11be --- /dev/null +++ b/site/landing/index.html @@ -0,0 +1,16 @@ + + + + + + @pmndrs/glyph + + + +
+ + + diff --git a/site/landing/src/anchor.ts b/site/landing/src/anchor.ts new file mode 100644 index 000000000..6af027dff --- /dev/null +++ b/site/landing/src/anchor.ts @@ -0,0 +1,19 @@ +/** + * Publishes where the wordmark's line box ends as a fraction of the frame, so + * the DOM overlay can sit a fixed optical gap beneath it at any viewport instead + * of being parked at the bottom of the screen. + * + * A custom property rather than React state on purpose: this changes on resize + * and on every relayout, and pushing it through a render would re-run the scene + * for something only the stylesheet consumes. + */ +let published = -1; + +export function publishMarkBottom(fraction: number): void { + const clamped = Math.min(0.94, Math.max(0.06, fraction)); + // Sub-tenth-of-a-percent changes are below the threshold of anything visible + // and would otherwise write to the DOM every frame. + if (Math.abs(clamped - published) < 0.001) return; + published = clamped; + document.documentElement.style.setProperty('--mark-bottom', `${(clamped * 100).toFixed(2)}%`); +} diff --git a/site/landing/src/chorus-stack.ts b/site/landing/src/chorus-stack.ts new file mode 100644 index 000000000..872e889d9 --- /dev/null +++ b/site/landing/src/chorus-stack.ts @@ -0,0 +1,45 @@ +// Generated by site/scripts/bake-chorus.mts. Do not edit. +import face0 from '../assets/chorus-latin.font.glb?url'; +import face1 from '../assets/chorus-hebrew.font.glb?url'; +import face2 from '../assets/chorus-devanagari.font.glb?url'; +import face3 from '../assets/chorus-bengali.font.glb?url'; +import face4 from '../assets/chorus-gurmukhi.font.glb?url'; +import face5 from '../assets/chorus-gujarati.font.glb?url'; +import face6 from '../assets/chorus-oriya.font.glb?url'; +import face7 from '../assets/chorus-tamil.font.glb?url'; +import face8 from '../assets/chorus-telugu.font.glb?url'; +import face9 from '../assets/chorus-kannada.font.glb?url'; +import face10 from '../assets/chorus-malayalam.font.glb?url'; +import face11 from '../assets/chorus-sinhala.font.glb?url'; +import face12 from '../assets/chorus-thai.font.glb?url'; +import face13 from '../assets/chorus-lao.font.glb?url'; +import face14 from '../assets/chorus-khmer.font.glb?url'; +import face15 from '../assets/chorus-georgian.font.glb?url'; +import face16 from '../assets/chorus-armenian.font.glb?url'; +import face17 from '../assets/chorus-ethiopic.font.glb?url'; +import face18 from '../assets/chorus-japanese.font.glb?url'; +import face19 from '../assets/chorus-korean.font.glb?url'; + +/** Fallback order: Latin first, CJK last, scripted faces in between. */ +export const CHORUS_URLS = [ + face0, + face1, + face2, + face3, + face4, + face5, + face6, + face7, + face8, + face9, + face10, + face11, + face12, + face13, + face14, + face15, + face16, + face17, + face18, + face19, +] as const; diff --git a/site/landing/src/chorus-words.ts b/site/landing/src/chorus-words.ts new file mode 100644 index 000000000..c303f1e1b --- /dev/null +++ b/site/landing/src/chorus-words.ts @@ -0,0 +1,78 @@ +/** + * The word, in as many writing systems as the stack can carry. + * + * This list is the single source of truth. `site/scripts/bake-chorus.mts` reads + * it, asks every face what it can actually draw, and bakes each one to exactly + * the glyphs it ends up owning. Add or change a word and re-run the bake — + * nothing here needs to know which font will carry it, and the script fails + * loudly naming the code points if none can. + * + * Interleaving these in one stream is not decoration. Latin next to Arabic and + * Hebrew forces the bidi algorithm to resolve direction runs on every line, the + * Indic entries need reordering and conjuncts, Arabic needs cursive joining, and + * the CJK pairs carry no spaces to break at. The field is a shaping proof that + * happens to read as typography. + */ +export const WORDS = [ + // Latin + 'glyph', + 'glifo', + 'glyphe', + 'Glyphe', + 'glif', + 'glýf', + 'kirjain', + 'tecken', + 'harf', + 'chữ', + 'znak', + 'litera', + 'jel', + 'glifă', + 'písmo', + 'raide', + // Greek and Cyrillic + 'γλυφή', + 'γράμμα', + 'глиф', + 'гліф', + 'буква', + 'знак', + // Right-to-left + 'אות', + 'שריטה', + // Indic + 'अक्षर', + 'वर्ण', + 'অক্ষর', + 'ਅੱਖਰ', + 'અક્ષર', + 'ଅକ୍ଷର', + 'எழுத்து', + 'అక్షరం', + 'ಅಕ್ಷರ', + 'അക്ഷരം', + 'අකුර', + // South-east Asia + 'อักขระ', + 'ຕົວອັກສອນ', + 'អក្សរ', + // Caucasus and Horn of Africa + 'ასო', + 'տառ', + 'ፊደል', + // CJK + '文字', + '字形', + '글자', + '글리프', +] as const; + +/** + * The subset that reorders. Bidi resolves these correctly inside the stream, but + * a right-to-left run inside a left-to-right justified line reads as a jump, and + * too many of them make the field look mis-spaced rather than multilingual. The + * chorus meters how often one is drawn instead of dropping them: their presence + * is the point, their density is a matter of taste. + */ +export const RTL_WORDS: ReadonlySet = new Set(['حرف', 'نويسه', 'ٹائپ', 'אות', 'שריטה']); diff --git a/site/landing/src/chorus.tsx b/site/landing/src/chorus.tsx new file mode 100644 index 000000000..654524b60 --- /dev/null +++ b/site/landing/src/chorus.tsx @@ -0,0 +1,257 @@ +import { Text, TextGroup, useFont } from '@pmndrs/glyph/react'; +import type { Text as ThreeText } from '@pmndrs/glyph/three'; +import { useFrame } from '@react-three/fiber/webgpu'; +import { msdf } from '@pmndrs/glyph/three/msdf'; +import { useThree } from '@react-three/fiber/webgpu'; +import { useMemo, useRef } from 'react'; + +/** Must match site/scripts/bake-chorus.mts. */ +const EM_SIZE = 32; +const PIXEL_RANGE = 6; + +import { CHORUS_URLS } from './chorus-stack'; +import { RTL_WORDS, WORDS } from './chorus-words'; +import { live } from './look'; + +/** + * One continuous stream, long enough to overflow three full-height columns at + * any viewport. There are no line breaks in it: the engine decides every break, + * fills each column to its bounded height, and carries the remainder into the + * next. + * + * Drawn at random rather than cycled. A round-robin through the word list lays + * the same languages down the same relative positions on every line, which the + * eye reads as a pattern rather than as text. Seeded so the field is identical + * on every load — the layout has to be stable to be judged, and a page that + * reshuffles itself on refresh cannot be art-directed. + */ +const SEED = 0x9e3779b9; + +/** mulberry32 — small, fast, and good enough for scattering words. */ +function random(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = (state + 0x6d2b79f5) >>> 0; + let drawn = Math.imul(state ^ (state >>> 15), 1 | state); + drawn = (drawn + Math.imul(drawn ^ (drawn >>> 7), 61 | drawn)) ^ drawn; + return ((drawn ^ (drawn >>> 14)) >>> 0) / 4_294_967_296; + }; +} + +const LTR = WORDS.filter((word) => !RTL_WORDS.has(word)); +const RTL = WORDS.filter((word) => RTL_WORDS.has(word)); + +/** + * Words in short same-script runs rather than shuffled one at a time. + * + * A paragraph binds a different atlas for every font run, so scattering + * twenty-one faces word-by-word makes nearly every word its own draw — measured + * at 2429 draw calls for one paragraph. Emitting a handful of words per script + * before switching cuts the switches by the run length while still reading as + * mixed, because the eye samples a line, not a word. + */ +function stream(rtlShare: number, count: number, runLength: number): string { + const next = random(SEED); + const words: string[] = []; + let previous = ''; + + while (words.length < count) { + const pool = next() < rtlShare ? RTL : LTR; + const script = scriptOf(pool[Math.floor(next() * pool.length)]!); + const run = Math.max(1, Math.round(runLength * (0.5 + next()))); + const family = pool.filter((word) => scriptOf(word) === script); + + for (let index = 0; index < run && words.length < count; index += 1) { + let pick = previous; + // Never twice in a row: a repeat reads as a mistake rather than a shuffle. + while (pick === previous && family.length > 1) pick = family[Math.floor(next() * family.length)]!; + if (family.length === 1) pick = family[0]!; + previous = pick; + words.push(pick); + } + } + // Word · word · word. The dot gives the eye a boundary it can trust when the + // scripts on either side of it are ones it cannot read. + return words.join(' \u00b7 '); +} + +/** Which face will end up drawing this word, near enough to group by. */ +function scriptOf(word: string): string { + const point = word.codePointAt(0)!; + if (point < 0x0370) return 'latin'; + if (point < 0x0400) return 'greek'; + if (point < 0x0590) return 'cyrillic'; + if (point < 0x0600) return 'hebrew'; + if (point < 0x0900) return 'arabic'; + if (point < 0x3000) return `indic-${(point >> 7).toString(16)}`; + return 'cjk'; +} + +/** Rebuilt only when the mix changes, which is a panel action rather than a frame. */ +const cache = new Map(); +function chorusFor(rtlShare: number, count: number, runLength: number): string { + const share = Math.round(rtlShare * 100) / 100; + const key = `${share}:${count}:${runLength}`; + let text = cache.get(key); + if (text === undefined) { + text = stream(share, count, runLength); + cache.set(key, text); + } + return text; +} + +/** + * Column count by width, in CSS pixels. + * + * Three columns stop being columns and start being slivers well before a phone. + * A justified line needs enough words on it that the spaces can absorb the + * remainder; a narrow column with long words justifies into gap-toothed + * nonsense. Two holds down to tablet width, one from there. + */ +function columnsFor(width: number): number { + if (width < 560) return 1; + if (width < 940) return 2; + return 3; +} + +// Slug: exact outlines rather than an atlas. MSDF bakes a full-size atlas per +// face regardless of glyph count — twenty-two of those came to 448 MB of +// texture for a few hundred glyphs — while Slug carries curve data, so the +// cost scales with the glyphs actually baked rather than with the face count. +// The options are part of the raster identity, so the runtime request has to +// name the same ones the artifact was baked with. Ask for the defaults against +// a 32-texel atlas and the loader finds no matching raster, falls back to +// generating one, and fails for want of the source bytes. +const REQUESTS = CHORUS_URLS.map( + (url) => + ({ + input: { baked: url }, + raster: { options: { emSize: EM_SIZE, pixelRange: PIXEL_RANGE }, technique: msdf }, + }) as const, +); + +for (const request of REQUESTS) useFont.preload(request); + +/** + * The editorial field the wordmark sits over. + * + * One paragraph and one ordered font stack: the engine resolves each script to + * the face that covers it, so the fallback is the library's own rather than the + * application choosing a font per word. + */ +export function Chorus() { + // The request list is a module constant emitted by the bake script, so its + // length never changes at runtime and hook order is stable. + // eslint-disable-next-line react-hooks/rules-of-hooks + const fonts = REQUESTS.map((request) => useFont(request)); + + const field = useRef>(null); + + // A hole in a line of justified text is either a stretched space or a word + // that failed to shape. The engine knows which, so ask it rather than guess. + const reported = useRef(false); + useFrame(() => { + if (reported.current) return; + const summary = field.current?.layout(); + if (!summary || summary.glyphCount === 0) return; + reported.current = true; + if (import.meta.env.DEV) { + console.info('[chorus]', { + glyphCount: summary.glyphCount, + lineCount: summary.lineCount, + missingGlyphCount: summary.missingGlyphCount, + overflowed: summary.overflowed, + }); + } + }); + + const size = useThree((state) => state.size); + const viewport = useThree((state) => state.viewport); + + const stack = useMemo(() => ({ fonts: fonts as unknown as readonly [(typeof fonts)[0], ...typeof fonts] }), [fonts]); + + // The field sits behind the mark, so the frustum has opened up by the time it + // reaches that depth and the box has to grow to match. + const depth = live.chorusDepth; + const spread = 1 + depth / 6; + const width = viewport.width * spread * 1.08; + const height = viewport.height * spread * 1.2; + + const columns = columnsFor(size.width); + const gap = (live.chorusGap * width) / Math.max(size.width, 1); + + const fontSize = width * live.chorusSize; + + if (import.meta.env.DEV) { + const perCssPixel = width / Math.max(size.width, 1); + (globalThis as unknown as { __geom: unknown }).__geom = { + boxWidthWorld: +width.toFixed(2), + columnWidthCss: +((width - gap * (columns - 1)) / columns / perCssPixel).toFixed(1), + columns, + fontSizeCss: +(fontSize / perCssPixel).toFixed(1), + gapCss: +(gap / perCssPixel).toFixed(1), + gapWorld: +gap.toFixed(3), + screenWidthCss: size.width, + spread: +spread.toFixed(3), + viewportWorld: +viewport.width.toFixed(2), + }; + } + + return ( + // `independent` lets Rust reorder compatible draws. The default is + // `ordered`, which forbids it — correct for text that overlaps itself, and + // needlessly strict here: these words never touch each other, so nothing + // depends on the order they are composited in, and same-atlas runs scattered + // through the paragraph can collapse into one draw. + + 0.5 ? 'justify' : 'start', + // The gap is specified in screen pixels and converted, so a column + // never closes up to a hairline just because the field sits deeper or + // the viewport got narrow. + columns: { count: columns, gap }, + // Columns fill top to bottom and then move across, so the engine needs a + // bounded height to know where one column ends. + height: { mode: 'exact', size: height }, + // Elastic both ways, with the remainder spilling into letter spacing + // rather than into the word gaps. Growth alone is what produces rivers: + // a line that can only stretch has to open every space to reach flush, + // where one that can also shrink usually absorbs the deficit invisibly. + justify: { + letterSpaceExpansion: live.chorusLetter, + maxWordSpaceRatio: live.chorusMaxSpace, + minWordSpaceRatio: live.chorusMinSpace, + }, + // Only the final line of the whole flow is short, so leaving it ragged + // is correct rather than stretching it across the column. + lastLine: 'auto', + overflow: 'clip', + width: { mode: 'exact', size: width }, + // 'word' cannot break inside a word, exactly as CSS cannot, so a word + // wider than the column runs straight through the gap into the next + // one. The benchmark's editorial workload never meets this because it + // sets Latin ipsum in half-width columns; a third-width column of + // Khmer, Lao and Tamil meets it constantly. 'character' is the + // engine's own answer, and for a decorative field a mid-word break + // costs nothing next to an overflow. + wrap: live.chorusBreak > 0.5 ? 'character' : 'word', + }} + font={stack} + ref={field} + // Set back by value, not only by depth: the field has to read as ground + // for the mark, and at full strength it competes with it. + paint={{ color: '#8fa3c4', opacity: live.chorusDim }} + position={[-width / 2, height / 2, -depth]} + style={{ fontSize, lineHeight: live.chorusLeading }} + > + {chorusFor(live.chorusRtl, live.chorusWords, live.chorusRun)} + + + ); +} diff --git a/site/landing/src/dev/look-panel.tsx b/site/landing/src/dev/look-panel.tsx new file mode 100644 index 000000000..81ab4c100 --- /dev/null +++ b/site/landing/src/dev/look-panel.tsx @@ -0,0 +1,163 @@ +import { button, folder, useControls } from 'leva'; + +import { LOOK, type LookValues, graphVersion, live } from '../look'; + +/** + * The leva panel, and every table it needs. + * + * Dev only, and kept in its own module for a measured reason: leva builds its + * stitches theme at module scope, so a static import survives tree-shaking even + * when every use of it is behind `import.meta.env.DEV`. A production build of + * an earlier revision carried leva's whole panel UI. Reaching this module only + * through `import()` inside a branch the constant folds away leaves the code in + * a chunk that production never emits. + */ +const GRAPH_KEYS = new Set([ + 'bloomRadius', + 'bloomStrength', + 'bloomThreshold', + 'envResolution', + 'flareAttenuation', + 'flareSamples', + 'flareSpacing', + 'flareThreshold', +]); + +const RANGES: Record = { + aberrationFalloff: [0, 8, 0.1], + aberrationPeak: [0, 0.8, 0.005], + ambient: [0, 3, 0.005], + bloomRadius: [0, 1, 0.01], + chorusDepth: [0, 12, 0.1], + chorusDim: [0, 1, 0.001], + chorusJustify: [0, 1, 1], + chorusBreak: [0, 1, 1], + chorusGap: [0, 220, 2], + chorusWords: [200, 8000, 100], + shakeAim: [0, 0.4, 0.001], + shakeDamping: [0.02, 2, 0.01], + shakeAmount: [0, 1.2, 0.005], + shakeSpeed: [0, 1, 0.005], + chorusRtl: [0, 0.6, 0.01], + chorusRun: [1, 40, 1], + chorusLetter: [0, 0.4, 0.005], + chorusMaxSpace: [1, 3, 0.01], + chorusMinSpace: [0.4, 1, 0.01], + chorusLeading: [0.6, 2.2, 0.01], + chorusSize: [0.008, 0.09, 0.001], + bloomStrength: [0, 2, 0.01], + bloomThreshold: [0, 1.5, 0.005], + curvature: [0, 2, 0.01], + emissive: [0, 0.3, 0.001], + envIntensity: [0, 4, 0.01], + envResolution: [64, 1024, 64], + exposure: [0.1, 2, 0.01], + fillIntensity: [0, 4, 0.01], + flareAttenuation: [1, 60, 1], + flareSamples: [1, 12, 1], + flareSpacing: [0, 1, 0.01], + flareThreshold: [0, 1.5, 0.01], + keyElevation: [-1.5, 1.5, 0.01], + keyIntensity: [0, 12, 0.05], + keyRadius: [0, 14, 0.1], + keySpeed: [0, 2, 0.01], + markGap: [0, 2, 0.01], + measure: [0.05, 0.9, 0.005], + metalness: [0, 1, 0.01], + rimAngle: [0, 6.283, 0.01], + rimIntensity: [0, 4, 0.01], + roughness: [0, 1, 0.005], + studioBack: [0, 4, 0.01], + studioKey: [0, 6, 0.01], + studioSides: [0, 6, 0.01], + studioTop: [0, 6, 0.01], +}; + +const GROUPS = { + Mark: ['measure', 'markGap', 'curvature', 'metalness', 'roughness', 'emissive'], + Chorus: [ + 'chorusJustify', + 'chorusSize', + 'chorusLeading', + 'chorusDim', + 'chorusDepth', + 'chorusMinSpace', + 'chorusMaxSpace', + 'chorusLetter', + 'chorusGap', + 'chorusBreak', + 'chorusRtl', + 'chorusRun', + 'chorusWords', + ], + Light: [ + 'keyIntensity', + 'keyRadius', + 'keyElevation', + 'keySpeed', + 'fillIntensity', + 'rimIntensity', + 'rimAngle', + 'ambient', + ], + Studio: ['studioKey', 'studioTop', 'studioSides', 'studioBack', 'envIntensity', 'envResolution'], + Bloom: ['bloomStrength', 'bloomRadius', 'bloomThreshold'], + Flare: ['flareSamples', 'flareSpacing', 'flareAttenuation', 'flareThreshold'], + Camera: ['shakeAmount', 'shakeAim', 'shakeSpeed', 'shakeDamping'], + Lens: ['exposure', 'aberrationPeak', 'aberrationFalloff'], +} as const satisfies Record; + +const KEYS = Object.keys(LOOK) as (keyof LookValues)[]; + +function pathOf(key: keyof LookValues): string { + const group = Object.entries(GROUPS).find(([, keys]) => (keys as readonly string[]).includes(key)); + return `${group![0]}.${key}`; +} + +function apply(key: keyof LookValues, value: number): void { + if (live[key] === value) return; + live[key] = value; + if (GRAPH_KEYS.has(key)) graphVersion.value += 1; +} + +function schema(keys: readonly (keyof LookValues)[]) { + return Object.fromEntries( + keys.map((key) => { + const [min, max, step] = RANGES[key]; + return [key, { max, min, onChange: (value: number) => apply(key, value), step, value: LOOK[key] }]; + }), + ); +} + +/** + * The panel. Rendered outside ``; it never re-renders the scene, it just + * writes into `live`. + */ +export function LookPanel() { + const [values, set] = useControls(() => ({ + ...Object.fromEntries( + Object.entries(GROUPS).map(([name, keys]) => [name, folder(schema(keys), { collapsed: true })]), + ), + Reset: button(() => { + set(Object.fromEntries(KEYS.map((key) => [pathOf(key), LOOK[key]]))); + }), + 'Copy for handoff': button(() => { + const block = [ + 'export const LOOK: Readonly = Object.freeze({', + ...KEYS.map((key) => ` ${key}: ${Math.round(live[key] * 1e4) / 1e4},`), + '});', + ].join('\n'); + void navigator.clipboard.writeText(block); + }), + })); + + // leva flattens folder contents by key, so this bridges the panel into the + // record the frame loop reads. The scene is never re-rendered by it. + const held = values as Partial>; + for (const key of KEYS) { + const value = held[key]; + if (typeof value === 'number') apply(key, value); + } + + return null; +} diff --git a/site/landing/src/dev/panel.tsx b/site/landing/src/dev/panel.tsx new file mode 100644 index 000000000..8bb0de450 --- /dev/null +++ b/site/landing/src/dev/panel.tsx @@ -0,0 +1,29 @@ +import { Leva } from 'leva'; +import { useEffect } from 'react'; + +import { LookPanel } from './look-panel'; +import { bindDebugToggle, useDebugVisible } from './visibility'; + +/** + * The development overlays: leva's panel, the look controls that write into + * `live`, and the space bar that hides them. + * + * Reached only through `import()` from a branch guarded by `import.meta.env.DEV`, + * which Vite folds to `false` in a production build. Everything below — leva, + * its stitches theme, the key binding — then sits in a chunk production never + * emits. A static import would not: leva builds its theme at module scope, so + * tree-shaking cannot drop it. + */ +export default function DevOverlays() { + const visible = useDebugVisible(); + useEffect(bindDebugToggle, []); + + // `hidden` rather than unmounting: leva keeps its store either way, so a + // toggle never resets a value that was dialled in. + return ( + <> +