diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..6594946 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,57 @@ +# libmsdf + +[![Build status](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml/badge.svg?branch=main&event=push)](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml) +[![NuGet](https://img.shields.io/nuget/vpre/kajarlabs.osu.Framework.MsdfTextRendering.svg)](https://www.nuget.org/packages/kajarlabs.osu.Framework.MsdfTextRendering) +[![GitHub release](https://img.shields.io/github/v/release/KAJARLABORATORIES/libmsdf?include_prereleases)](https://github.com/KAJARLABORATORIES/libmsdf/releases/latest) + +MSDF text rendering for [osu!framework](https://github.com/ppy/osu-framework). + +## Using libmsdf in your game + +If you are interested in **using** the library, install the package and start from the [setting up MSDF fonts](https://github.com/KAJARLABORATORIES/libmsdf/wiki/Setting-Up-MSDF-Fonts) wiki page, which walks through generating an atlas, registering it and drawing text with it. + +``` +dotnet add package kajarlabs.osu.Framework.MsdfTextRendering --prerelease +``` + +The library is pre-`1.0.0`. The public API may change between `0.x` releases, so pin an exact version if you depend on it. + +The rest of the information on this page is related to working *on* the library, not *using* it! + +## Objectives + +osu!framework renders text from pre-rasterised bitmap atlases. That is fast and predictable, but a glyph baked at one size degrades once it is drawn far beyond it, which shows up in any UI that zooms, animates scale, or shares a typeface across a wide range of sizes. + +`libmsdf` provides a second rendering path built on [multi-channel signed distance fields](https://github.com/Chlumsky/msdfgen), where glyph coverage is reconstructed in the fragment shader instead of sampled from a fixed-resolution texture. + +- Stay a **drop-in alternative**, not a replacement. `MsdfSpriteText` mirrors `SpriteText`, and MSDF glyphs implement the framework's own glyph abstractions so the existing text builder, layout and localisation keep working. +- Keep the atlas pipeline **external and standard**. Atlases are produced by [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen), so nothing here is a bespoke font format. +- Cover the same text features users already expect: kerning, multiline, truncation, fixed width, fallback glyphs and shadows. +MSDF is not a free upgrade for every case. At small, static sizes a hinted bitmap font is cheaper and often looks better; this library is aimed at the cases where one atlas has to serve many sizes. + +## Requirements + +- A desktop platform with the [.NET 10.0 SDK](https://dotnet.microsoft.com/download). +- [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen) for generating font atlases. +- When working with the codebase, we recommend an IDE with intellisense and syntax highlighting, such as [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) extensions installed. + +### Building + +``` +dotnet build +dotnet test +``` + +Visual test scenes live in `tests`, and are the recommended place to develop and debug rendering changes: a distance-field bug is something you see, not something an assertion catches. + +## Contributing + +Contributions can be made via pull requests to this repository. If you are unsure where to start, check the [list of open issues](https://github.com/KAJARLABORATORIES/libmsdf/issues). + +When reporting a rendering bug, include a minimal reproduction and the atlas and JSON metadata that trigger it. "The glyph looks wrong" is impossible to act on without the distance range it was generated with. + +## License + +This library is licensed under the [MIT license](https://opensource.org/licenses/MIT). Please see [the license file](../COPYING) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. + +The MSDF technique itself comes from [msdfgen](https://github.com/Chlumsky/msdfgen) by Viktor Chlumský, also MIT licensed. Atlases generated with `msdf-atlas-gen` carry the license of the font they were generated from; check it before shipping one. diff --git a/.github/README.nuget.md b/.github/README.nuget.md new file mode 100644 index 0000000..6594946 --- /dev/null +++ b/.github/README.nuget.md @@ -0,0 +1,57 @@ +# libmsdf + +[![Build status](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml/badge.svg?branch=main&event=push)](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml) +[![NuGet](https://img.shields.io/nuget/vpre/kajarlabs.osu.Framework.MsdfTextRendering.svg)](https://www.nuget.org/packages/kajarlabs.osu.Framework.MsdfTextRendering) +[![GitHub release](https://img.shields.io/github/v/release/KAJARLABORATORIES/libmsdf?include_prereleases)](https://github.com/KAJARLABORATORIES/libmsdf/releases/latest) + +MSDF text rendering for [osu!framework](https://github.com/ppy/osu-framework). + +## Using libmsdf in your game + +If you are interested in **using** the library, install the package and start from the [setting up MSDF fonts](https://github.com/KAJARLABORATORIES/libmsdf/wiki/Setting-Up-MSDF-Fonts) wiki page, which walks through generating an atlas, registering it and drawing text with it. + +``` +dotnet add package kajarlabs.osu.Framework.MsdfTextRendering --prerelease +``` + +The library is pre-`1.0.0`. The public API may change between `0.x` releases, so pin an exact version if you depend on it. + +The rest of the information on this page is related to working *on* the library, not *using* it! + +## Objectives + +osu!framework renders text from pre-rasterised bitmap atlases. That is fast and predictable, but a glyph baked at one size degrades once it is drawn far beyond it, which shows up in any UI that zooms, animates scale, or shares a typeface across a wide range of sizes. + +`libmsdf` provides a second rendering path built on [multi-channel signed distance fields](https://github.com/Chlumsky/msdfgen), where glyph coverage is reconstructed in the fragment shader instead of sampled from a fixed-resolution texture. + +- Stay a **drop-in alternative**, not a replacement. `MsdfSpriteText` mirrors `SpriteText`, and MSDF glyphs implement the framework's own glyph abstractions so the existing text builder, layout and localisation keep working. +- Keep the atlas pipeline **external and standard**. Atlases are produced by [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen), so nothing here is a bespoke font format. +- Cover the same text features users already expect: kerning, multiline, truncation, fixed width, fallback glyphs and shadows. +MSDF is not a free upgrade for every case. At small, static sizes a hinted bitmap font is cheaper and often looks better; this library is aimed at the cases where one atlas has to serve many sizes. + +## Requirements + +- A desktop platform with the [.NET 10.0 SDK](https://dotnet.microsoft.com/download). +- [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen) for generating font atlases. +- When working with the codebase, we recommend an IDE with intellisense and syntax highlighting, such as [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) extensions installed. + +### Building + +``` +dotnet build +dotnet test +``` + +Visual test scenes live in `tests`, and are the recommended place to develop and debug rendering changes: a distance-field bug is something you see, not something an assertion catches. + +## Contributing + +Contributions can be made via pull requests to this repository. If you are unsure where to start, check the [list of open issues](https://github.com/KAJARLABORATORIES/libmsdf/issues). + +When reporting a rendering bug, include a minimal reproduction and the atlas and JSON metadata that trigger it. "The glyph looks wrong" is impossible to act on without the distance range it was generated with. + +## License + +This library is licensed under the [MIT license](https://opensource.org/licenses/MIT). Please see [the license file](../COPYING) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. + +The MSDF technique itself comes from [msdfgen](https://github.com/Chlumsky/msdfgen) by Viktor Chlumský, also MIT licensed. Atlases generated with `msdf-atlas-gen` carry the license of the font they were generated from; check it before shipping one. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e462ab9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: Continuous Integration + +on: + push: + branches: [develop] + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ${{ matrix.os.fullname }} + timeout-minutes: 60 + env: + OSU_EXECUTION_MODE: ${{ matrix.threadingMode }} + strategy: + fail-fast: false + matrix: + os: + - { prettyname: Windows, fullname: windows-latest, configuration: Debug } + - { prettyname: macOS, fullname: macos-latest, configuration: Debug } # ARM: veja o item 3 abaixo + - { prettyname: Linux, fullname: ubuntu-latest, configuration: Debug } + - { prettyname: Linux, fullname: ubuntu-latest, configuration: Release } + threadingMode: ["SingleThread", "MultiThreaded"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install .NET 10.0.x + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Compile + run: dotnet build -c ${{ matrix.os.configuration }} osu-framework-msdf.slnx + + - name: Test + run: > + dotnet test osu-framework-msdf.slnx + -c ${{ matrix.os.configuration }} + --no-build + --results-directory ${{ github.workspace }}/TestResults + --logger "trx;LogFileName=TestResults-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }}.trx" + -- NUnit.ConsoleOut=0 + + # Sobe os resultados mesmo se o teste falhar. + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: libmsdf-test-results-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }} + path: ${{ github.workspace }}/TestResults/TestResults-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }}.trx + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e9a8c35 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,144 @@ +name: Package and NuGet + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+*" + +permissions: + contents: read + +jobs: + notify_process_deploy: + name: Notify start + runs-on: ubuntu-latest + continue-on-error: true + env: + WEBHOOK_URL: ${{ secrets.DISCORD_INFRA_WEBHOOK_URL }} + steps: + - name: Notify Deployment + run: | + export TITLE="🚀 Deployment in Progress: libmsdf ($GITHUB_REF_NAME)" + export URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + export DESCRIPTION="A new deployment for **libmsdf** has started."$'\n'"Stay tuned for updates!"$'\n'"➜ [View Workflow Run]($URL)" + export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID" + + BODY="$(jq --null-input '{ + embeds: [{ + title: env.TITLE, + color: 15098112, + description: env.DESCRIPTION, + url: env.URL, + author: { name: env.GITHUB_ACTOR, icon_url: env.ACTOR_ICON } + }] + }')" + + curl --fail -H "Content-Type: application/json" -d "$BODY" "$WEBHOOK_URL" + + test: + name: Test (${{ matrix.threadingMode }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: true + matrix: + threadingMode: ["SingleThread", "MultiThreaded"] + env: + OSU_EXECUTION_MODE: ${{ matrix.threadingMode }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Test + run: dotnet test -c Release + + pack: + name: Pack (libmsdf) + needs: [test] + runs-on: ubuntu-latest + environment: production + timeout-minutes: 15 + permissions: + contents: read + id-token: write + env: + ARTIFACTS: ${{ github.workspace }}/artifacts + VERSION: ${{ github.ref_name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Pack (libmsdf) + run: dotnet pack lib -c Release /p:Version="${VERSION#v}" -o "$ARTIFACTS" + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: libmsdf + path: ${{ github.workspace }}/artifacts/*.nupkg + if-no-files-found: error + + - name: NuGet login (OIDC) + id: login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 + with: + user: ${{ vars.NUGET_USER }} + + - name: Publish packages to NuGet + env: + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: > + dotnet nuget push "$ARTIFACTS"/*.nupkg + --api-key "$NUGET_API_KEY" + --source https://api.nuget.org/v3/index.json + --skip-duplicate + + notify_status_deploy: + name: Notify result + needs: [test, pack] + if: always() + runs-on: ubuntu-latest + continue-on-error: true + env: + PACK_RESULT: ${{ needs.pack.result }} + WEBHOOK_URL: ${{ secrets.DISCORD_INFRA_WEBHOOK_URL }} + steps: + - name: Notify Deployment Status + run: | + export TITLE="🚀 Deployment Completed: libmsdf ($GITHUB_REF_NAME)" + export URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + + if [ "$PACK_RESULT" = "success" ]; then + STATUS="✅ Deployment successful!"; export COLOR=3066993 + else + STATUS="❌ Deployment failed. Check logs for details."; export COLOR=15158332 + fi + + export DESCRIPTION="Deployment for **libmsdf** has finished."$'\n'"$STATUS"$'\n'"➜ [View Workflow Run]($URL)" + export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID" + + BODY="$(jq --null-input '{ + embeds: [{ + title: env.TITLE, + color: (env.COLOR | tonumber), + description: env.DESCRIPTION, + url: env.URL, + author: { name: env.GITHUB_ACTOR, icon_url: env.ACTOR_ICON } + }] + }')" + + curl --fail -H "Content-Type: application/json" -d "$BODY" "$WEBHOOK_URL" diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml new file mode 100644 index 0000000..52aaba7 --- /dev/null +++ b/.github/workflows/report-nunit.yml @@ -0,0 +1,42 @@ +name: Annotate CI run with Test Results + +on: + workflow_run: + workflows: ["Continuous Integration"] + types: [completed] + +permissions: + contents: read + actions: read + checks: write + +jobs: + annotate: + name: Annotate CI run with Test Results + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + persist-credentials: false + + - name: Download results + uses: actions/download-artifact@v4 + with: + pattern: libmsdf-test-results-* + merge-multiple: true + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Annotate CI run with Test Results + uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 + with: + name: Results + path: "*.trx" + reporter: dotnet-trx + list-suites: "failed" + list-tests: "failed" diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e222d25 --- /dev/null +++ b/COPYING @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 KAJAR LABORATORIES + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Directory.Build.props b/Directory.Build.props index 6b853ec..b53025a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,11 +18,15 @@ - - + + - - + + + + True + README.md + Default @@ -44,6 +48,7 @@ KAJAR LABORATORIES MSDF Text Rendering for osu!framework Automated release. MIT + README.md https://github.com/KAJARLABORATORIES/libmsdf https://github.com/KAJARLABORATORIES/libmsdf diff --git a/analysis/banned-symbols.txt b/analysis/banned-symbols.txt index de25143..5e088f8 100644 --- a/analysis/banned-symbols.txt +++ b/analysis/banned-symbols.txt @@ -3,14 +3,9 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead. M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. -T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable instead. -M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. -M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. M:System.Guid.#ctor;Probably meaning to use Guid.CreateVersion7() instead. If actually wanting empty, use Guid.Empty. -M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. -M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. diff --git a/lib/Graphics/Containers/IMsdfTextPart.cs b/lib/Graphics/Containers/IMsdfTextPart.cs new file mode 100644 index 0000000..3d827b3 --- /dev/null +++ b/lib/Graphics/Containers/IMsdfTextPart.cs @@ -0,0 +1,12 @@ +using osu.Framework.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public interface IMsdfTextPart +{ + IEnumerable Drawables { get; } + + event Action>? DrawablePartsRecreated; + + void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer); +} diff --git a/lib/Graphics/Containers/MsdfTextChunk.cs b/lib/Graphics/Containers/MsdfTextChunk.cs new file mode 100644 index 0000000..7927b55 --- /dev/null +++ b/lib/Graphics/Containers/MsdfTextChunk.cs @@ -0,0 +1,115 @@ +using System.Globalization; +using System.Text; +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Framework.Localisation; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public class MsdfTextChunk : MsdfTextPart + where TMsdfSpriteText : MsdfSpriteText, new() +{ + private readonly LocalisableString text; + private readonly bool newLineIsParagraph; + private readonly Func creationFunc; + private readonly Action? creationParameters; + + public MsdfTextChunk(LocalisableString text, bool newLineIsParagraph, Func creationFunc, Action? creationParameters = null) + { + this.text = text; + this.newLineIsParagraph = newLineIsParagraph; + this.creationFunc = creationFunc; + this.creationParameters = creationParameters; + } + + protected override IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer) + { + var currentContent = textFlowContainer.Localisation?.GetLocalisedString(text) ?? text.ToString(); + + var drawables = new List(); + + if (!newLineIsParagraph) + { + var newLine = new MsdfTextNewLine(true); + + newLine.RecreateDrawablesFor(textFlowContainer); + drawables.AddRange(newLine.Drawables); + } + + drawables.AddRange(CreateDrawablesFor(currentContent, textFlowContainer)); + return drawables; + } + + protected virtual IEnumerable CreateDrawablesFor(string text, MsdfTextFlowContainer textFlowContainer) + { + var first = true; + var sprites = new List(); + + foreach (var l in text.Split('\n')) + { + if (!first) + { + var lastChild = sprites.LastOrDefault() ?? textFlowContainer.Children.LastOrDefault(); + + if (lastChild != null) + { + var newLine = new MsdfTextFlowContainer.NewLineContainer(newLineIsParagraph); + + sprites.Add(newLine); + } + } + + foreach (var word in SplitWords(l)) + { + if (string.IsNullOrEmpty(word)) continue; + + var textSprite = CreateSpriteText(textFlowContainer); + + textSprite.Text = word; + sprites.Add(textSprite); + } + + first = false; + } + + return sprites; + } + + protected string[] SplitWords(string text) + { + var words = new List(); + var builder = new StringBuilder(); + + for (var i = 0; i < text.Length; i++) + { + if (i == 0 + || char.IsSeparator(text[i - 1]) + || char.IsControl(text[i - 1]) + || char.GetUnicodeCategory(text[i - 1]) == UnicodeCategory.DashPunctuation + || text[i - 1] == '/' + || text[i - 1] == '\\' + || (isCjkCharacter(text[i - 1]) && !char.IsPunctuation(text[i]))) + { + words.Add(builder.ToString()); + builder.Clear(); + } + + builder.Append(text[i]); + } + + if (builder.Length > 0) + words.Add(builder.ToString()); + + return words.ToArray(); + + static bool isCjkCharacter(char c) => c >= '\x2E80' && c <= '\x9FFF'; + } + + protected virtual TMsdfSpriteText CreateSpriteText(MsdfTextFlowContainer textFlowContainer) + { + var spriteText = creationFunc.Invoke(); + textFlowContainer.ApplyDefaultCreationParameters(spriteText); + creationParameters?.Invoke(spriteText); + return spriteText; + } +} diff --git a/lib/Graphics/Containers/MsdfTextFlowContainer.cs b/lib/Graphics/Containers/MsdfTextFlowContainer.cs new file mode 100644 index 0000000..2425043 --- /dev/null +++ b/lib/Graphics/Containers/MsdfTextFlowContainer.cs @@ -0,0 +1,503 @@ +using System.Buffers; +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osuTK; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public partial class MsdfTextFlowContainer : CompositeDrawable +{ + private readonly Action? defaultCreationParameters; + + private readonly List parts = []; + + private readonly Cached partsCache = new(); + + public float FirstLineIndent + { + get => Flow.FirstLineIndent; + set => Flow.FirstLineIndent = value; + } + + public float ContentIndent + { + get => Flow.ContentIndent; + set => Flow.ContentIndent = value; + } + + public float ParagraphSpacing + { + get => Flow.ParagraphSpacing; + set => Flow.ParagraphSpacing = value; + } + + public float LineSpacing + { + get => Flow.LineSpacing; + set => Flow.LineSpacing = value; + } + + public Anchor TextAnchor + { + get => Flow.TextAnchor; + set => Flow.TextAnchor = value; + } + + public LocalisableString Text + { + set + { + Flow.Clear(); + parts.Clear(); + + AddText(value); + } + } + + public new Axes RelativeSizeAxes + { + get => base.RelativeSizeAxes; + set + { + base.RelativeSizeAxes = value; + setFlowSizing(); + } + } + + public new Axes AutoSizeAxes + { + get => base.AutoSizeAxes; + set + { + base.AutoSizeAxes = value; + setFlowSizing(); + } + } + + public override float Width + { + get => base.Width; + set + { + base.Width = value; + setFlowSizing(); + } + } + + private void setFlowSizing() + { + if (AutoSizeAxes.HasFlagFast(Axes.X)) + { + Flow.RelativeSizeAxes = Axes.None; + Flow.AutoSizeAxes = Axes.Both; + } + else + { + Flow.AutoSizeAxes = Axes.Y; + Flow.RelativeSizeAxes = Axes.X; + } + } + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public Vector2 Spacing + { + get => Flow.Spacing; + set => Flow.Spacing = value; + } + + public Vector2 MaximumSize + { + get => Flow.MaximumSize; + set => Flow.MaximumSize = value; + } + + public new bool Masking + { + get => base.Masking; + set => base.Masking = value; + } + + public FillDirection Direction + { + get => Flow.Direction; + set => Flow.Direction = value; + } + + public IEnumerable Children => Flow.Children; + + [Resolved] + internal LocalisationManager? Localisation { get; private set; } + + protected readonly MsdfInnerFlow Flow; + private readonly Bindable localisationParameters = new(); + + public MsdfTextFlowContainer(Action? defaultCreationParameters = null) + { + this.defaultCreationParameters = defaultCreationParameters; + + InternalChild = Flow = CreateFlow().With(f => f.AutoSizeAxes = Axes.Both); + } + + protected virtual MsdfInnerFlow CreateFlow() => new(); + + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + if (Localisation != null) + localisationParameters.Value = Localisation.CurrentParameters.Value; + + RecreateAllParts(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (Localisation != null) + { + localisationParameters.BindValueChanged(_ => partsCache.Invalidate()); + ((IBindable)localisationParameters).BindTo(Localisation.CurrentParameters); + } + } + + protected override void Update() + { + base.Update(); + + if (!partsCache.IsValid) + RecreateAllParts(); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (TextAnchor.HasFlagFast(Anchor.x2)) + return base.Compare(y, x); + + return base.Compare(x, y); + } + + public IMsdfTextPart AddText(LocalisableString text, Action? creationParameters = null) + where TMsdfSpriteText : MsdfSpriteText, new() + => AddPart(CreateChunkFor(text, true, () => new TMsdfSpriteText(), creationParameters)); + + public IMsdfTextPart AddText(LocalisableString text, Action? creationParameters = null) + => AddPart(CreateChunkFor(text, true, CreateSpriteText, creationParameters)); + + public void AddText(TMsdfSpriteText text, Action? creationParameters = null) + where TMsdfSpriteText : MsdfSpriteText + { + defaultCreationParameters?.Invoke(text); + creationParameters?.Invoke(text); + AddPart(new MsdfTextPartManual(text.Yield())); + } + + public IMsdfTextPart AddParagraph(LocalisableString paragraph, Action? creationParameters = null) + where TMsdfSpriteText : MsdfSpriteText, new() + => AddPart(CreateChunkFor(paragraph, false, () => new TMsdfSpriteText(), creationParameters)); + + /// + public IMsdfTextPart AddParagraph(LocalisableString paragraph, Action? creationParameters = null) + => AddPart(CreateChunkFor(paragraph, false, CreateSpriteText, creationParameters)); + + protected internal virtual MsdfTextChunk CreateChunkFor(LocalisableString text, bool newLineIsParagraph, Func creationFunc, Action? creationParameters = null) + where TMsdfSpriteText : MsdfSpriteText, new() + => new(text, newLineIsParagraph, creationFunc, creationParameters); + + public void NewLine() => AddPart(new MsdfTextNewLine(false)); + + public void NewParagraph() => AddPart(new MsdfTextNewLine(true)); + + protected internal virtual MsdfSpriteText CreateSpriteText() => new(); + + internal void ApplyDefaultCreationParameters(MsdfSpriteText spriteText) => defaultCreationParameters?.Invoke(spriteText); + + public void Clear(bool disposeChildren = true) + { + Flow.Clear(disposeChildren); + parts.Clear(); + } + + protected internal IMsdfTextPart AddPart(IMsdfTextPart part) + { + parts.Add(part); + + if (partsCache.IsValid) + recreatePart(part); + + return part; + } + + public bool RemovePart(IMsdfTextPart partToRemove) + { + if (!parts.Remove(partToRemove)) + return false; + + partsCache.Invalidate(); + return true; + } + + protected virtual void RecreateAllParts() + { + foreach (var manualPart in parts.OfType()) + Flow.RemoveRange(manualPart.Drawables, false); + + Flow.Clear(true); + + foreach (var part in parts) + recreatePart(part); + + partsCache.Validate(); + } + + private void recreatePart(IMsdfTextPart part) + { + part.RecreateDrawablesFor(this); + foreach (var drawable in part.Drawables) + Flow.Add(drawable); + } + + protected partial class MsdfInnerFlow : FillFlowContainer + { + public float FirstLineIndent + { + get; + set + { + if (field == value) return; + + field = value; + + InvalidateLayout(); + } + } + + public float ContentIndent + { + get; + set + { + if (field == value) return; + + field = value; + + InvalidateLayout(); + } + } + + public float ParagraphSpacing + { + get; + set + { + if (field == value) return; + + field = value; + + InvalidateLayout(); + } + } = 0.5f; + + public float LineSpacing + { + get; + set + { + if (field == value) return; + + field = value; + + InvalidateLayout(); + } + } + + public Anchor TextAnchor + { + get; + set + { + if (field == value) + return; + + field = value; + + Anchor = value; + Origin = value; + + InvalidateLayout(); + } + } = Anchor.TopLeft; + + protected override IEnumerable ComputeLayoutPositions() + { + var max = MaximumSize; + + if (max == Vector2.Zero) + { + var s = ChildSize; + + max.X = AutoSizeAxes.HasFlagFast(Axes.X) ? float.PositiveInfinity : s.X; + max.Y = AutoSizeAxes.HasFlagFast(Axes.Y) ? float.PositiveInfinity : s.Y; + } + + var children = FlowingChildren.ToArray(); + if (children.Length == 0) + yield break; + + var layoutPositions = ArrayPool.Shared.Rent(children.Length); + + var rowIndices = ArrayPool.Shared.Rent(children.Length); + + var rowWidths = new List { 0 }; + var lineBaseHeights = new List { 0 }; + + var rowHeight = 0.0f; + var current = Vector2.Zero; + + var size = Vector2.Zero; + + try + { + for (var i = 0; i < children.Length; ++i) + { + var c = children[i]; + + static Axes toAxes(FillDirection direction) + { + switch (direction) + { + case FillDirection.Full: + return Axes.Both; + + case FillDirection.Horizontal: + return Axes.X; + + case FillDirection.Vertical: + return Axes.Y; + + default: + throw new ArgumentException($"{direction} is not defined"); + } + } + + if ((c.RelativeSizeAxes & AutoSizeAxes & toAxes(Direction)) != 0 + && (c.FillMode != FillMode.Fit || c.RelativeSizeAxes != Axes.Both || c.Size.X > RelativeChildSize.X + || c.Size.Y > RelativeChildSize.Y || AutoSizeAxes == Axes.Both)) + { + throw new InvalidOperationException( + "Drawables inside a fill flow container may not have a relative size axis that the fill flow container is filling in and auto sizing for. " + + $"The fill flow container is set to flow in the {Direction} direction and autosize in {AutoSizeAxes} axes and the child is set to relative size in {c.RelativeSizeAxes} axes."); + } + + if (c.RelativeAnchorPosition != Vector2.Zero) + throw new InvalidOperationException($"All drawables in a {nameof(MsdfTextFlowContainer)} must not specify custom {nameof(RelativeAnchorPosition)}s. Only (0,0) is supported."); + if (c.RelativeOriginPosition != Vector2.Zero) + throw new InvalidOperationException($"All drawables in a {nameof(MsdfTextFlowContainer)} must not specify custom {nameof(RelativeOriginPosition)}s. Only (0,0) is supported."); + + if (i == 0) + { + size = c.BoundingBox.Size; + current.X = ContentIndent + FirstLineIndent; + } + + var rowWidth = current.X + size.X; + + if (Direction != FillDirection.Horizontal && (Precision.DefinitelyBigger(rowWidth, max.X) || Direction == FillDirection.Vertical || c is NewLineContainer)) + { + current.X = ContentIndent; + current.Y += rowHeight * (1 + LineSpacing); + + if (c is NewLineContainer nlc) + current.Y += nlc.IndicatesNewParagraph ? rowHeight * ParagraphSpacing : 0; + + layoutPositions[i] = current; + + rowWidths.Add(current.X + size.X); + lineBaseHeights.Add((c as IHasLineBaseHeight)?.LineBaseHeight ?? 0); + + rowHeight = 0; + } + else + { + layoutPositions[i] = current; + + rowWidths[^1] = rowWidth; + lineBaseHeights[^1] = Math.Max(lineBaseHeights[^1], (c as IHasLineBaseHeight)?.LineBaseHeight ?? 0); + } + + rowIndices[i] = rowWidths.Count - 1; + var stride = Vector2.Zero; + + if (i < children.Length - 1) + { + stride = size; + + c = children[i + 1]; + size = c.BoundingBox.Size; + } + + stride += Spacing; + + if (stride.Y > rowHeight) + rowHeight = stride.Y; + + current.X += stride.X; + } + + if (!float.IsFinite(max.X)) + { + var newMax = 0.0f; + foreach (var rowWidth in rowWidths) + newMax = MathF.Max(newMax, rowWidth); + max.X = newMax; + } + + for (var i = 0; i < children.Length; i++) + { + var c = children[i]; + var layoutPosition = layoutPositions[i]; + var rowOffsetToEnd = max.X - rowWidths[rowIndices[i]]; + + if (TextAnchor.HasFlagFast(Anchor.x1)) + layoutPosition.X += rowOffsetToEnd / 2; + else if (TextAnchor.HasFlagFast(Anchor.x2)) + layoutPosition.X += rowOffsetToEnd; + + if (c is IHasLineBaseHeight hasLineBaseHeight) + layoutPosition.Y += lineBaseHeights[rowIndices[i]] - hasLineBaseHeight.LineBaseHeight; + + yield return layoutPosition; + } + } + finally + { + ArrayPool.Shared.Return(layoutPositions); + ArrayPool.Shared.Return(rowIndices); + } + } + } + + public partial class NewLineContainer : Container + { + public readonly bool IndicatesNewParagraph; + + public NewLineContainer(bool newParagraph) + { + IndicatesNewParagraph = newParagraph; + } + } +} diff --git a/lib/Graphics/Containers/MsdfTextNewLine.cs b/lib/Graphics/Containers/MsdfTextNewLine.cs new file mode 100644 index 0000000..de4bbf8 --- /dev/null +++ b/lib/Graphics/Containers/MsdfTextNewLine.cs @@ -0,0 +1,20 @@ +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public class MsdfTextNewLine : MsdfTextPart +{ + private readonly bool indicatesNewParagraph; + + public MsdfTextNewLine(bool indicatesNewParagraph) + { + this.indicatesNewParagraph = indicatesNewParagraph; + } + + protected override IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer) + { + var newLineContainer = new MsdfTextFlowContainer.NewLineContainer(indicatesNewParagraph); + return newLineContainer.Yield(); + } +} diff --git a/lib/Graphics/Containers/MsdfTextPart.cs b/lib/Graphics/Containers/MsdfTextPart.cs new file mode 100644 index 0000000..078f1d8 --- /dev/null +++ b/lib/Graphics/Containers/MsdfTextPart.cs @@ -0,0 +1,26 @@ +using osu.Framework.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public abstract class MsdfTextPart : IMsdfTextPart +{ + public IEnumerable Drawables { get; } + + public event Action>? DrawablePartsRecreated; + + private readonly List drawables = []; + + protected MsdfTextPart() + { + Drawables = drawables.AsReadOnly(); + } + + public void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer) + { + drawables.Clear(); + drawables.AddRange(CreateDrawablesFor(textFlowContainer)); + DrawablePartsRecreated?.Invoke(drawables); + } + + protected abstract IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer); +} diff --git a/lib/Graphics/Containers/MsdfTextPartManual.cs b/lib/Graphics/Containers/MsdfTextPartManual.cs new file mode 100644 index 0000000..01aad96 --- /dev/null +++ b/lib/Graphics/Containers/MsdfTextPartManual.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; +using osu.Framework.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; + +public class MsdfTextPartManual : IMsdfTextPart +{ + public IEnumerable Drawables { get; } + + public event Action>? DrawablePartsRecreated + { + add { } + remove { } + } + + public MsdfTextPartManual(IEnumerable drawables) + { + Drawables = drawables.ToImmutableArray(); + } + + public void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer) + { + } +} diff --git a/lib/Graphics/Sprites/MsdfSpriteText.cs b/lib/Graphics/Sprites/MsdfSpriteText.cs new file mode 100644 index 0000000..1847e38 --- /dev/null +++ b/lib/Graphics/Sprites/MsdfSpriteText.cs @@ -0,0 +1,597 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Development; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Layout; +using osu.Framework.Localisation; +using osu.Framework.Text; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; + +public partial class MsdfSpriteText : Drawable, IHasLineBaseHeight, ITexturedShaderDrawable, IHasText, IHasFilterTerms, IFillFlowContainer, IHasCurrentValue +{ + private static readonly char[] default_never_fixed_width_characters = ['.', ',', ':', ' ', '\u00A0', '\u202F']; + + [Resolved] + private MsdfFontStore msdfFonts { get; set; } = null!; + + [Resolved] + private LocalisationManager localisation { get; set; } = null!; + + private ILocalisedBindableString localisedText = null!; + + public IShader? TextureShader { get; protected set; } + + private float distanceRange; + private bool storeReady; + + public MsdfSpriteText() + { + current.BindValueChanged(text => + { + if (localisedText == null || text.NewValue != localisedText.Value) + Text = text.NewValue; + }); + + AddLayout(charactersCache); + AddLayout(shadowOffsetCache); + AddLayout(textBuilderCache); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "MsdfGlyph"); + + localisedText = localisation.GetLocalisedBindableString(Text); + + updateFontStore(); + + storeReady = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + localisedText.BindValueChanged(str => + { + current.Value = localisedText.Value; + + if (string.IsNullOrEmpty(str.NewValue)) + { + if (requiresAutoSizedWidth) + base.Width = Padding.TotalHorizontal; + if (requiresAutoSizedHeight) + base.Height = Padding.TotalVertical; + } + + invalidate(true); + }, true); + } + + public LocalisableString Text + { + get; + set + { + if (field.Equals(value)) + return; + + field = value; + localisedText?.Text = value; + } + } = string.Empty; + + private readonly BindableWithCurrent current = new(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private string displayedText => localisedText?.Value ?? Text.ToString(); + + public FontUsage Font + { + get; + set + { + field = value; + invalidate(true, true); + shadowOffsetCache.Invalidate(); + + if (storeReady) + updateFontStore(); + } + } + + public bool AllowMultiline + { + get; + set + { + if (field == value) + return; + + if (value) + Truncate = false; + + field = value; + invalidate(true, true); + } + } = true; + + public bool Shadow + { + get; + set + { + if (field == value) + return; + + field = value; + Invalidate(Invalidation.DrawNode); + } + } + + public Color4 ShadowColour + { + get; + set + { + if (field == value) + return; + + field = value; + Invalidate(Invalidation.DrawNode); + } + } = new Color4(0, 0, 0, 0.2f); + + public Vector2 ShadowOffset + { + get; + set + { + if (field == value) + return; + + field = value; + invalidate(true); + shadowOffsetCache.Invalidate(); + } + } = new Vector2(0, 0.06f); + + public bool UseFullGlyphHeight + { + get; + set + { + if (field == value) + return; + + field = value; + invalidate(true, true); + } + } = true; + + public bool Truncate + { + get; + set + { + if (field == value) + return; + + if (value) + AllowMultiline = false; + + field = value; + invalidate(true, true); + } + } + + public string EllipsisString + { + get; + set + { + if (field == value) + return; + + field = value; + invalidate(true, true); + } + } = "…"; + + public bool IsTruncated { get; private set; } + + private bool requiresAutoSizedWidth => explicitWidth == null && (RelativeSizeAxes & Axes.X) == 0; + + private bool requiresAutoSizedHeight => explicitHeight == null && (RelativeSizeAxes & Axes.Y) == 0; + + private float? explicitWidth; + + public override float Width + { + get + { + if (requiresAutoSizedWidth) + computeCharacters(); + return base.Width; + } + set + { + if (explicitWidth == value) + return; + + base.Width = value; + explicitWidth = value; + + invalidate(true, true); + } + } + + public float MaxWidth + { + get; + set + { + if (field == value) + return; + + field = value; + invalidate(true, true); + } + } = float.PositiveInfinity; + + private float? explicitHeight; + + public override float Height + { + get + { + if (requiresAutoSizedHeight) + computeCharacters(); + return base.Height; + } + set + { + if (explicitHeight == value) + return; + + base.Height = value; + explicitHeight = value; + + invalidate(true, true); + } + } + + public override Vector2 Size + { + get + { + if (requiresAutoSizedWidth || requiresAutoSizedHeight) + computeCharacters(); + return base.Size; + } + set + { + Width = value.X; + Height = value.Y; + } + } + + public Vector2 Spacing + { + get; + set + { + if (field == value) + return; + + field = value; + invalidate(true, true); + } + } + + public MarginPadding Padding + { + get; + set + { + if (field.Equals(value)) + return; + + if (!Validation.IsFinite(value)) + throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}."); + + field = value; + invalidate(true, true); + } + } + + public override bool IsPresent => base.IsPresent && (AlwaysPresent || !string.IsNullOrEmpty(displayedText)); + + private readonly LayoutValue charactersCache = new(Invalidation.DrawSize | Invalidation.Presence, InvalidationSource.Parent); + + private readonly List charactersBacking = []; + + private List characters + { + get + { + computeCharacters(); + return charactersBacking; + } + } + + private void computeCharacters() + { + if (LoadState >= LoadState.Loaded) + Debug.Assert(ThreadSafety.IsUpdateThread); + + if (!storeReady) + return; + + if (charactersCache.IsValid) + return; + + IsTruncated = false; + + charactersBacking.Clear(); + + var textBounds = Vector2.Zero; + + try + { + if (string.IsNullOrEmpty(displayedText)) + return; + + var textBuilder = getTextBuilder(); + + textBuilder.Reset(); + textBuilder.AddText(displayedText); + textBounds = textBuilder.Bounds; + + if (textBuilder is TruncatingTextBuilder truncatingTextBuilder) + IsTruncated = truncatingTextBuilder.IsTruncated; + } + finally + { + if (requiresAutoSizedWidth) + base.Width = textBounds.X + Padding.Right; + if (requiresAutoSizedHeight) + base.Height = textBounds.Y + Padding.Bottom; + + base.Width = Math.Min(base.Width, MaxWidth); + + charactersCache.Validate(); + } + } + + private readonly LayoutValue shadowOffsetCache = new(Invalidation.DrawInfo, InvalidationSource.Parent); + + private Vector2 premultipliedShadowOffset => + shadowOffsetCache.IsValid ? shadowOffsetCache.Value : shadowOffsetCache.Value = ToScreenSpace(ShadowOffset * Font.Size) - ToScreenSpace(Vector2.Zero); + + private void invalidate(bool characters = false, bool textBuilder = false) + { + if (characters) + charactersCache.Invalidate(); + + if (textBuilder) + InvalidateTextBuilder(); + + Invalidate(Invalidation.RequiredParentSizeToFit); + } + + protected override DrawNode CreateDrawNode() => new MsdfSpriteTextDrawNode(this); + + /// + /// The characters that should be excluded from fixed-width application. Defaults to (".", ",", ":", " ") if null. + /// + protected virtual char[]? FixedWidthExcludeCharacters => null; + + /// + /// The character to use to calculate the fixed width width. Defaults to 'm'. + /// + protected virtual char FixedWidthReferenceCharacter => 'm'; + + /// + /// The character to fallback to use if a character glyph lookup failed. + /// + /// + /// Native defaults this to '•', but MSDF atlases are generated from a + /// charset file that does not necessarily include it, in + /// which case the fallback lookup itself silently fails and the character is dropped entirely. + /// '?' is used instead since it's part of the basic ASCII range every generated atlas covers. + /// + protected virtual char FallbackCharacter => '?'; + + private readonly LayoutValue textBuilderCache = new(Invalidation.DrawSize, InvalidationSource.Parent); + + protected void InvalidateTextBuilder() => textBuilderCache.Invalidate(); + + protected virtual TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) + { + var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters; + + var builderMaxWidth = requiresAutoSizedWidth + ? MaxWidth + : ApplyRelativeAxes(RelativeSizeAxes, new Vector2(Math.Min(MaxWidth, base.Width), base.Height), FillMode).X - Padding.Right; + + if (AllowMultiline) + { + return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, + excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); + } + + if (Truncate) + { + return new TruncatingTextBuilder(store, Font, builderMaxWidth, EllipsisString, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, + excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); + } + + return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking, + excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); + } + + private TextBuilder getTextBuilder() + { + if (!textBuilderCache.IsValid) + textBuilderCache.Value = CreateTextBuilder(msdfFonts); + + return textBuilderCache.Value; + } + + private void updateFontStore() + { + distanceRange = msdfFonts.GetDistanceRange(Font.FontName) ?? 0f; + } + + public override string ToString() => $@"""{displayedText}"" " + base.ToString(); + + public float LineBaseHeight + { + get + { + computeCharacters(); + return textBuilderCache.Value.LineBaseHeight; + } + } + + public IEnumerable FilterTerms => Text.Yield(); + + private class MsdfSpriteTextDrawNode : TexturedShaderDrawNode + { + protected new MsdfSpriteText Source => (MsdfSpriteText)base.Source; + + private bool shadow; + private ColourInfo shadowColour; + private Vector2 shadowOffset; + private float distanceRange; + + private List? parts; + private IUniformBuffer? parametersBuffer; + + public MsdfSpriteTextDrawNode(MsdfSpriteText source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + updateScreenSpaceCharacters(); + distanceRange = Source.distanceRange; + shadow = Source.Shadow; + + if (shadow) + { + shadowColour = Source.ShadowColour; + shadowOffset = Source.premultipliedShadowOffset; + } + } + + protected override void Draw(IRenderer renderer) + { + Debug.Assert(parts != null); + + base.Draw(renderer); + + BindTextureShader(renderer); + + var avgColour = (Color4)DrawColourInfo.Colour.AverageColour; + var shadowAlpha = MathF.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); + + var finalShadowColour = DrawColourInfo.Colour; + finalShadowColour.ApplyChild(shadowColour.MultiplyAlpha(shadowAlpha)); + + for (var i = 0; i < parts.Count; i++) + { + if (shadow) + { + var shadowQuad = parts[i].DrawQuad; + + renderer.DrawQuad(parts[i].Texture, + new Quad( + shadowQuad.TopLeft + shadowOffset, + shadowQuad.TopRight + shadowOffset, + shadowQuad.BottomLeft + shadowOffset, + shadowQuad.BottomRight + shadowOffset), + finalShadowColour, inflationPercentage: parts[i].InflationPercentage); + } + + renderer.DrawQuad(parts[i].Texture, parts[i].DrawQuad, DrawColourInfo.Colour, inflationPercentage: parts[i].InflationPercentage); + } + + UnbindTextureShader(renderer); + } + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new MsdfParameters { DistanceRange = new UniformFloat { Value = distanceRange } }; + + shader.BindUniformBlock("m_MsdfParameters", parametersBuffer); + } + + private void updateScreenSpaceCharacters() + { + var partCount = Source.characters.Count; + + if (parts == null) + parts = new List(partCount); + else + { + parts.Clear(); + parts.EnsureCapacity(partCount); + } + + foreach (var character in Source.characters) + { + parts.Add(new ScreenSpaceGlyphPart + { + DrawQuad = Source.ToScreenSpace(character.DrawRectangle), + InflationPercentage = Vector2.Zero, + Texture = character.Texture, + }); + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct MsdfParameters + { + public UniformFloat DistanceRange; + private readonly UniformPadding12 padding; + } + + private struct ScreenSpaceGlyphPart + { + public Quad DrawQuad; + public Vector2 InflationPercentage; + public Texture Texture; + } + } +} diff --git a/lib/IO/Stores/MsdfFontStore.cs b/lib/IO/Stores/MsdfFontStore.cs new file mode 100644 index 0000000..ca9d174 --- /dev/null +++ b/lib/IO/Stores/MsdfFontStore.cs @@ -0,0 +1,144 @@ +using System.Collections.Concurrent; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Text; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores; + +public class MsdfFontStore : IDisposable, ITexturedGlyphLookupStore +{ + private readonly IRenderer renderer; + private readonly ShaderManager shaders; + + private readonly List sources = []; + private readonly ConcurrentDictionary<(string, char), ITexturedCharacterGlyph?> namespacedGlyphCache = []; + + private Task? childStoreLoadTasks; + + public MsdfFontStore(IRenderer renderer, ShaderManager shaders) + { + this.renderer = renderer; + this.shaders = shaders; + } + + public void AddFont(IResourceStore store, string family, string weight) + { + var source = new MsdfGlyphSource(renderer, shaders, store, family, weight); + + sources.Add(source); + queueLoad(source); + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + var key = (fontName ?? string.Empty, character); + + if (namespacedGlyphCache.TryGetValue(key, out var existing)) + return existing; + + return namespacedGlyphCache[key] = resolveGlyph(fontName, character); + } + + public float? GetDistanceRange(string? fontName) + => tryGetSource(fontName)?.DistanceRange; + + public IShader? GetShader(string? fontName) + => tryGetSource(fontName)?.Shader; + + ITexturedCharacterGlyph? ITexturedGlyphLookupStore.Get(string? fontName, char character) + => Get(fontName, character); + + Task ITexturedGlyphLookupStore.GetAsync(string fontName, char character) + => Task.FromResult(Get(fontName, character)); + + public static float ReadDistanceRange(byte[] json) + => MsdfGlyphSource.ReadDistanceRange(json); + + private MsdfGlyphSource? tryGetSource(string? fontName) + { + var name = fontName ?? string.Empty; + + foreach (var source in sources) + { + if (source.FontName.EndsWith(name, StringComparison.Ordinal)) + return source; + } + + foreach (var source in sources) + { + if (!name.StartsWith(source.Family, StringComparison.Ordinal)) + continue; + + var style = name[source.Family.Length..]; + + if (style.Length > 0) + { + if (style[0] != '-') + continue; + + style = style[1..]; + } + + var (weight, italic) = MsdfGlyphSource.ParseStyle(style); + + if (italic == source.IsItalic && weight.Equals(source.NormalizedWeight, StringComparison.Ordinal)) + return source; + } + + return null; + } + + private ITexturedCharacterGlyph? resolveGlyph(string? fontName, char character) + { + var primary = string.IsNullOrEmpty(fontName) ? null : tryGetSource(fontName); + + if (primary?.HasGlyph(character) == true) + return primary.Get(character); + + foreach (var source in sources.OrderBy(s => primary != null && s.Family == primary.Family ? 0 : 1)) + { + if (source == primary || !source.HasGlyph(character)) + continue; + + if (primary != null && source.DistanceRange != primary.DistanceRange) + { + Logger.Log($"{nameof(MsdfFontStore)}: '{character}' (U+{(int)character:X4}) falls back from '{primary.FontName}' to '{source.FontName}', " + + $"whose distanceRange ({source.DistanceRange}) differs -- a single text draw uses one distanceRange, so its edges may look off.", + LoggingTarget.Runtime, LogLevel.Important); + } + + return source.Get(character, primary); + } + + return null; + } + + private void queueLoad(MsdfGlyphSource source) + { + var previousLoadStream = childStoreLoadTasks; + + childStoreLoadTasks = Task.Run(async () => + { + if (previousLoadStream != null) + await previousLoadStream.ConfigureAwait(false); + + try + { + Logger.Log($"Loading Typeface {source.FontName}...", level: LogLevel.Debug); + source.Load(); + Logger.Log($"Loaded Typeface {source.FontName}!", level: LogLevel.Debug); + } + catch + { + } + }); + } + + public void Dispose() + { + foreach (var source in sources) + source?.Dispose(); + } +} diff --git a/lib/IO/Stores/MsdfGlyphSource.cs b/lib/IO/Stores/MsdfGlyphSource.cs new file mode 100644 index 0000000..c8b3323 --- /dev/null +++ b/lib/IO/Stores/MsdfGlyphSource.cs @@ -0,0 +1,305 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using kajarlabs.osu.Framework.MsdfTextRendering.Text; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Text; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores; + +internal sealed class MsdfGlyphSource : IDisposable +{ + public Texture Atlas + { + get + { + ensureLoaded(); + return field; + } + private set; + } = null!; + + public IShader Shader + { + get + { + ensureLoaded(); + return field; + } + private set; + } = null!; + + public float Ascender + { + get + { + ensureLoaded(); + return field; + } + private set; + } + + public float Descender + { + get + { + ensureLoaded(); + return field; + } + private set; + } + + public float LineHeight + { + get + { + ensureLoaded(); + return field; + } + private set; + } + + public float DistanceRange + { + get + { + ensureLoaded(); + return field; + } + private set; + } + + public string Family { get; } + + public string FontName { get; } + + public string NormalizedWeight { get; } + + public bool IsItalic { get; } + + public IReadOnlyDictionary Glyphs + { + get + { + ensureLoaded(); + return field; + } + private set; + } = new Dictionary(); + + public IReadOnlyDictionary<(char First, char Second), float> Kerning + { + get + { + ensureLoaded(); + return field; + } + private set; + } = new Dictionary<(char, char), float>(); + + private readonly IRenderer renderer; + private readonly ShaderManager shaders; + private readonly IResourceStore store; + private readonly string weight; + + private readonly TaskCompletionSource loadCompletionSource = new(); + + private TextureStore? textures; + + public MsdfGlyphSource(IRenderer renderer, ShaderManager shaders, IResourceStore store, string family, string weight) + { + this.renderer = renderer; + this.shaders = shaders; + this.store = store; + this.weight = weight; + + Family = family; + FontName = string.IsNullOrEmpty(weight) ? family : $"{family}-{weight}"; + + (NormalizedWeight, IsItalic) = ParseStyle(weight); + } + + public void Load() + { + textures = new TextureStore( + renderer, + new TextureLoaderStore(store), + useAtlas: false, + filteringMode: TextureFilteringMode.Linear, + manualMipmaps: true + ); + + var texturePath = $@"Typefaces/{Family}/{Family}-{weight}.png"; + var jsonPath = $@"Typefaces/{Family}/{Family}-{weight}.json"; + + Atlas = textures.Get(texturePath); + Shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "MsdfGlyph"); + + var json = store.Get(jsonPath) ?? throw new InvalidOperationException($"Could not find embedded resource '{jsonPath}'."); + var atlasJson = parseAtlasJson(json); + + Ascender = atlasJson.Metrics.Ascender; + Descender = atlasJson.Metrics.Descender; + LineHeight = atlasJson.Metrics.LineHeight; + DistanceRange = atlasJson.Atlas.DistanceRange; + + var newGlyphs = new Dictionary(); + + foreach (var glyph in atlasJson.Glyphs) + { + newGlyphs[(char)glyph.Unicode] = new MsdfGlyph( + glyph.Advance, + toRectangle(glyph.PlaneBounds), + toRectangle(glyph.AtlasBounds) + ); + } + + Glyphs = newGlyphs; + + var newKerning = new Dictionary<(char, char), float>(); + + foreach (var pair in atlasJson.Kerning) + newKerning[((char)pair.Unicode1, (char)pair.Unicode2)] = pair.Advance; + + Kerning = newKerning; + + loadCompletionSource.SetResult(true); + } + + public bool HasGlyph(char character) + { + try + { + ensureLoaded(); + } + catch + { + return false; + } + + return Glyphs.ContainsKey(character); + } + + public ITexturedCharacterGlyph? Get(char character, MsdfGlyphSource? metricsSource = null) + { + ensureLoaded(); + + if (!Glyphs.TryGetValue(character, out var glyph)) + { + Logger.Log($"{nameof(MsdfGlyphSource)}: no glyph for character '{character}' (U+{(int)character:X4}) in atlas '{FontName}' -- skipping.", LoggingTarget.Runtime, LogLevel.Debug); + return null; + } + + var texture = glyph.AtlasBounds is RectangleF atlasBound ? Atlas.Crop(atlasBound) : Atlas; + return new MsdfTexturedCharacterGlyph(this, metricsSource ?? this, character, glyph, texture); + } + + public static float ReadDistanceRange(byte[] json) + => parseAtlasJson(json).Atlas.DistanceRange; + + public static (string Weight, bool Italic) ParseStyle(string? style) + { + var weight = style ?? string.Empty; + var italic = weight.EndsWith("Italic", StringComparison.Ordinal); + + if (italic) + weight = weight[..^"Italic".Length]; + + if (weight.Equals("Regular", StringComparison.Ordinal)) + weight = string.Empty; + + return (weight, italic); + } + + private void ensureLoaded() + => loadCompletionSource.Task.GetResultSafely(); + + private static AtlasJson parseAtlasJson(byte[] json) + => JsonSerializer.Deserialize(json) ?? throw new InvalidOperationException("Failed to parse MSDF atlas JSON."); + + private static RectangleF? toRectangle(BoundsJson? bounds) + => bounds == null ? null : new RectangleF(bounds.Left, bounds.Top, bounds.Right - bounds.Left, bounds.Bottom - bounds.Top); + + public void Dispose() + => textures?.Dispose(); + + private sealed class AtlasJson + { + [JsonPropertyName("atlas")] + public AtlasMetaJson Atlas { get; set; } = new AtlasMetaJson(); + + [JsonPropertyName("metrics")] + public MetricsJson Metrics { get; set; } = new MetricsJson(); + + [JsonPropertyName("glyphs")] + public List Glyphs { get; set; } = new List(); + + [JsonPropertyName("kerning")] + public List Kerning { get; set; } = new List(); + } + + private sealed class AtlasMetaJson + { + [JsonPropertyName("distanceRange")] + public float DistanceRange { get; set; } + } + + private sealed class MetricsJson + { + [JsonPropertyName("ascender")] + public float Ascender { get; set; } + + [JsonPropertyName("descender")] + public float Descender { get; set; } + + [JsonPropertyName("lineHeight")] + public float LineHeight { get; set; } + } + + private sealed class GlyphJson + { + [JsonPropertyName("unicode")] + public int Unicode { get; set; } + + [JsonPropertyName("advance")] + public float Advance { get; set; } + + [JsonPropertyName("planeBounds")] + public BoundsJson? PlaneBounds { get; set; } + + [JsonPropertyName("atlasBounds")] + public BoundsJson? AtlasBounds { get; set; } + } + + private sealed class BoundsJson + { + [JsonPropertyName("left")] + public float Left { get; set; } + + [JsonPropertyName("top")] + public float Top { get; set; } + + [JsonPropertyName("right")] + public float Right { get; set; } + + [JsonPropertyName("bottom")] + public float Bottom { get; set; } + } + + private sealed class KerningJson + { + [JsonPropertyName("unicode1")] + public int Unicode1 { get; set; } + + [JsonPropertyName("unicode2")] + public int Unicode2 { get; set; } + + [JsonPropertyName("advance")] + public float Advance { get; set; } + } +} diff --git a/lib/MsdfTextRendering.cs b/lib/MsdfTextRendering.cs new file mode 100644 index 0000000..5c578de --- /dev/null +++ b/lib/MsdfTextRendering.cs @@ -0,0 +1,11 @@ +using osu.Framework.IO.Stores; + +namespace kajarlabs.osu.Framework.MsdfTextRendering; + +public static class MsdfTextRendering +{ + public static void CreateCapabilities(ResourceStore store) + { + store.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(MsdfTextRendering).Assembly), @"Resources")); + } +} diff --git a/lib/Resources/Shaders/sh_MsdfGlyph.fs b/lib/Resources/Shaders/sh_MsdfGlyph.fs new file mode 100644 index 0000000..f50d9bc --- /dev/null +++ b/lib/Resources/Shaders/sh_MsdfGlyph.fs @@ -0,0 +1,43 @@ +#ifndef MSDFGLYPH_FS +#define MSDFGLYPH_FS + +#include "sh_Utils.h" +#include "sh_Masking.h" +#include "sh_TextureWrapping.h" + +layout(location = 2) in mediump vec2 v_TexCoord; + +layout(std140, set = 0, binding = 0) uniform m_MsdfParameters +{ + mediump float g_DistanceRange; +}; + +layout(set = 1, binding = 0) uniform lowp texture2D m_Texture; +layout(set = 1, binding = 1) uniform lowp sampler m_Sampler; + +layout(location = 0) out vec4 o_Colour; + +float medianOfThree(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +float screenPxRange(vec2 texCoord) +{ + vec2 unitRange = vec2(g_DistanceRange) / vec2(textureSize(sampler2D(m_Texture, m_Sampler), 0)); + vec2 screenTexSize = vec2(1.0) / fwidth(texCoord); + return max(0.5 * dot(unitRange, screenTexSize), 1.0); +} + +void main(void) +{ + vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); + vec3 msd = texture(sampler2D(m_Texture, m_Sampler), wrappedCoord).rgb; + + float signedDistance = medianOfThree(msd.r, msd.g, msd.b) - 0.5; + float coverage = clamp(signedDistance * screenPxRange(v_TexCoord) + 0.5, 0.0, 1.0); + + o_Colour = getRoundedColor(vec4(1.0, 1.0, 1.0, coverage), wrappedCoord); +} + +#endif diff --git a/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs b/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs new file mode 100644 index 0000000..8e24730 --- /dev/null +++ b/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs @@ -0,0 +1,30 @@ +#ifndef MSDFGLYPHHARDTHRESHOLD_FS +#define MSDFGLYPHHARDTHRESHOLD_FS + +#include "sh_Utils.h" +#include "sh_Masking.h" +#include "sh_TextureWrapping.h" + +layout(location = 2) in mediump vec2 v_TexCoord; + +layout(set = 0, binding = 0) uniform lowp texture2D m_Texture; +layout(set = 0, binding = 1) uniform lowp sampler m_Sampler; + +layout(location = 0) out vec4 o_Colour; + +float medianOfThree(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +void main(void) +{ + vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); + vec3 msd = texture(sampler2D(m_Texture, m_Sampler), wrappedCoord).rgb; + + float coverage = medianOfThree(msd.r, msd.g, msd.b) > 0.5 ? 1.0 : 0.0; + + o_Colour = getRoundedColor(vec4(1.0, 1.0, 1.0, coverage), wrappedCoord); +} + +#endif diff --git a/lib/Text/MsdfGlyph.cs b/lib/Text/MsdfGlyph.cs new file mode 100644 index 0000000..149c6d6 --- /dev/null +++ b/lib/Text/MsdfGlyph.cs @@ -0,0 +1,5 @@ +using osu.Framework.Graphics.Primitives; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Text; + +public readonly record struct MsdfGlyph(float Advance, RectangleF? PlaneBounds, RectangleF? AtlasBounds); diff --git a/lib/Text/MsdfTexturedCharacterGlyph.cs b/lib/Text/MsdfTexturedCharacterGlyph.cs new file mode 100644 index 0000000..485df9f --- /dev/null +++ b/lib/Text/MsdfTexturedCharacterGlyph.cs @@ -0,0 +1,52 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores; +using osu.Framework.Graphics.Textures; +using osu.Framework.Text; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Text; + +public readonly struct MsdfTexturedCharacterGlyph : ITexturedCharacterGlyph +{ + public Texture Texture { get; } + + public float Width { get; } + + public float Height { get; } + + public float XOffset { get; } + + public float YOffset { get; } + + public float XAdvance { get; } + + public float Baseline { get; } + + public char Character { get; } + + private readonly MsdfGlyphSource source; + private readonly float lineHeight; + + internal MsdfTexturedCharacterGlyph(MsdfGlyphSource source, MsdfGlyphSource metricsSource, char character, MsdfGlyph glyph, Texture texture) + { + this.source = source; + + Character = character; + Texture = texture; + + this.lineHeight = metricsSource.LineHeight; + var lineHeight = this.lineHeight; + var planeBounds = glyph.PlaneBounds ?? default; + + Width = planeBounds.Width / lineHeight; + Height = planeBounds.Height / lineHeight; + XOffset = planeBounds.Left / lineHeight; + YOffset = (planeBounds.Top - metricsSource.Ascender) / lineHeight; + + XAdvance = glyph.Advance / lineHeight; + + Baseline = -metricsSource.Ascender / lineHeight; + } + + public float GetKerning(T lastGlyph) + where T : ICharacterGlyph + => source.Kerning.TryGetValue((lastGlyph.Character, Character), out var kerning) ? kerning / lineHeight : 0; +} diff --git a/tests/Graphics/SampleGameFont.cs b/tests/Graphics/SampleGameFont.cs new file mode 100644 index 0000000..673433c --- /dev/null +++ b/tests/Graphics/SampleGameFont.cs @@ -0,0 +1,48 @@ +using osu.Framework.Graphics.Sprites; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Graphics; + +public static class SampleGameFont +{ + public const float DEFAULT_FONT_SIZE = 18.0f; + + public static FontUsage WorkSans => GetFont(FontTypeface.WorkSans); + + public static FontUsage GetFont(FontTypeface typeface = FontTypeface.WorkSans, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Regular, bool italics = false, bool fixedWidth = false) + { + var familyStr = GetFamilyString(typeface); + + return new FontUsage(familyStr, size, GetWeightString(familyStr, weight), getItalics(italics), fixedWidth); + } + + public static string GetFamilyString(FontTypeface typeface) + { + return typeface.ToString(); + } + + public static string GetWeightString(string family, FontWeight weight) + { + return weight.ToString(); + } + + private static bool getItalics(in bool italicsRequested) + => false; +} + +public enum FontTypeface +{ + WorkSans, +} + +public enum FontWeight +{ + Thin = 100, + ExtraLight = 200, + Light = 300, + Regular = 400, + Medium = 500, + SemiBold = 600, + Bold = 700, + ExtraBold = 800, + Black = 900, +} diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt index aee16ae..8b821ad 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt index d1992e3..8d320fa 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png index 940fcf7..92d8a12 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png index 329797c..636e0f5 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt index 3bda659..e46a86d 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png index 95a0832..5c27fc2 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt index 270d17e..f5b6bd3 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png index 1477a18..af419ca 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt index 0eb8e9e..3387cb3 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png index e58e65c..f254ffe 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt index f9d4350..bbc903e 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png index a4bd60d..9192fba 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt index b92852e..fffa54c 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt index a453487..226ebe3 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png index aeff560..698b61d 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt index cf3597a..eb66f02 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png index f39b66f..111cc73 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt index 8c375ea..0de90d4 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt index e3a887c..9e2270e 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png index 55e68cf..15d85bf 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt index 9a2cf33..a61fddc 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt index 7239fa0..ec69a2e 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt differ diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png index 99ec6fa..b107cd9 100644 Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png differ diff --git a/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json new file mode 100644 index 0000000..9928545 --- /dev/null +++ b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":4,"distanceRangeMiddle":0,"size":48,"width":856,"height":856,"yOrigin":"top"},"metrics":{"emSize":1,"lineHeight":1.448,"ascender":-1.1599999999999999,"descender":0.28800000000000003,"underlineY":0.14999999999999999,"underlineThickness":0.050000000000000003},"glyphs":[{"unicode":12288,"advance":1},{"unicode":12289,"advance":1,"planeBounds":{"left":0.0090000000000000375,"top":-0.28125,"right":0.38400000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":186.5,"top":797.5,"right":204.5,"bottom":816.5}},{"unicode":12290,"advance":1,"planeBounds":{"left":-0.003416666666666659,"top":-0.30208333333333331,"right":0.39241666666666669,"bottom":0.11458333333333333},"atlasBounds":{"left":105.5,"top":797.5,"right":124.5,"bottom":817.5}},{"unicode":12291,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.67708333333333326,"right":0.82291666666666663,"bottom":-0.072916666666666671},"atlasBounds":{"left":407.5,"top":764.5,"right":438.5,"bottom":793.5}},{"unicode":12292,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.88541666666666663,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":96.5,"right":49.5,"bottom":145.5}},{"unicode":12293,"advance":1,"planeBounds":{"left":0.054333333333333296,"top":-0.84374999999999989,"right":0.8876666666666666,"bottom":0.072916666666666657},"atlasBounds":{"left":495.5,"top":338.5,"right":535.5,"bottom":382.5}},{"unicode":12294,"advance":1,"planeBounds":{"left":0.10624999999999998,"top":-0.82291666666666663,"right":0.91875000000000007,"bottom":0.11458333333333333},"atlasBounds":{"left":666.5,"top":244.5,"right":705.5,"bottom":289.5}},{"unicode":12295,"advance":1,"planeBounds":{"left":-5.3290705182007515e-18,"top":-0.88541666666666663,"right":1,"bottom":0.11458333333333333},"atlasBounds":{"left":492.5,"top":96.5,"right":540.5,"bottom":144.5}},{"unicode":12296,"advance":1,"planeBounds":{"left":0.53883333333333328,"top":-0.90624999999999989,"right":0.99716666666666665,"bottom":0.13541666666666669},"atlasBounds":{"left":407.5,"top":0.5,"right":429.5,"bottom":50.5}},{"unicode":12297,"advance":1,"planeBounds":{"left":0.0028333333333333483,"top":-0.90624999999999989,"right":0.46116666666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":430.5,"top":0.5,"right":452.5,"bottom":50.5}},{"unicode":12298,"advance":1,"planeBounds":{"left":0.48558333333333331,"top":-0.90624999999999989,"right":1.0064166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":453.5,"top":0.5,"right":478.5,"bottom":50.5}},{"unicode":12299,"advance":1,"planeBounds":{"left":-0.0074166666666666556,"top":-0.90624999999999989,"right":0.51341666666666663,"bottom":0.13541666666666669},"atlasBounds":{"left":479.5,"top":0.5,"right":504.5,"bottom":50.5}},{"unicode":12300,"advance":1,"planeBounds":{"left":0.59966666666666668,"top":-0.90625,"right":1.0163333333333333,"bottom":-0.15625},"atlasBounds":{"left":70.5,"top":59.5,"right":90.5,"bottom":95.5}},{"unicode":12301,"advance":1,"planeBounds":{"left":-0.0163333333333333,"top":-0.61458333333333326,"right":0.40033333333333337,"bottom":0.13541666666666669},"atlasBounds":{"left":91.5,"top":59.5,"right":111.5,"bottom":95.5}},{"unicode":12302,"advance":1,"planeBounds":{"left":0.55333333333333323,"top":-0.90625,"right":1.0116666666666667,"bottom":-0.13541666666666666},"atlasBounds":{"left":633.5,"top":686.5,"right":655.5,"bottom":723.5}},{"unicode":12303,"advance":1,"planeBounds":{"left":-0.011666666666666631,"top":-0.63541666666666663,"right":0.44666666666666671,"bottom":0.13541666666666669},"atlasBounds":{"left":656.5,"top":686.5,"right":678.5,"bottom":723.5}},{"unicode":12304,"advance":1,"planeBounds":{"left":0.61808333333333332,"top":-0.90624999999999989,"right":1.0139166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":528.5,"top":0.5,"right":547.5,"bottom":50.5}},{"unicode":12305,"advance":1,"planeBounds":{"left":-0.013916666666666641,"top":-0.90624999999999989,"right":0.38191666666666668,"bottom":0.13541666666666669},"atlasBounds":{"left":548.5,"top":0.5,"right":567.5,"bottom":50.5}},{"unicode":12306,"advance":1,"planeBounds":{"left":0.11458333333333331,"top":-0.76041666666666663,"right":0.88541666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":818.5,"top":338.5,"right":855.5,"bottom":377.5}},{"unicode":12307,"advance":1,"planeBounds":{"left":0.052083333333333322,"top":-0.82291666666666663,"right":0.94791666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":812.5,"top":292.5,"right":855.5,"bottom":335.5}},{"unicode":12308,"advance":1,"planeBounds":{"left":0.66983333333333328,"top":-0.90624999999999989,"right":1.0031666666666665,"bottom":0.15625},"atlasBounds":{"left":185.5,"top":0.5,"right":201.5,"bottom":51.5}},{"unicode":12309,"advance":1,"planeBounds":{"left":-0.0031666666666666466,"top":-0.90624999999999989,"right":0.33016666666666672,"bottom":0.15625},"atlasBounds":{"left":202.5,"top":0.5,"right":218.5,"bottom":51.5}},{"unicode":12310,"advance":1,"planeBounds":{"left":0.57574999999999998,"top":-0.90624999999999989,"right":1.01325,"bottom":0.13541666666666669},"atlasBounds":{"left":568.5,"top":0.5,"right":589.5,"bottom":50.5}},{"unicode":12311,"advance":1,"planeBounds":{"left":-0.013249999999999977,"top":-0.90624999999999989,"right":0.42425000000000002,"bottom":0.13541666666666669},"atlasBounds":{"left":590.5,"top":0.5,"right":611.5,"bottom":50.5}},{"unicode":12312,"advance":1,"planeBounds":{"left":0.60966666666666669,"top":-0.92708333333333337,"right":1.0263333333333333,"bottom":0.15625},"atlasBounds":{"left":143.5,"top":0.5,"right":163.5,"bottom":52.5}},{"unicode":12313,"advance":1,"planeBounds":{"left":-0.026333333333333302,"top":-0.92708333333333337,"right":0.39033333333333337,"bottom":0.15625},"atlasBounds":{"left":164.5,"top":0.5,"right":184.5,"bottom":52.5}},{"unicode":12314,"advance":1,"planeBounds":{"left":0.64449999999999996,"top":-0.90624999999999989,"right":1.0195000000000001,"bottom":0.13541666666666669},"atlasBounds":{"left":612.5,"top":0.5,"right":630.5,"bottom":50.5}},{"unicode":12315,"advance":1,"planeBounds":{"left":-0.019499999999999986,"top":-0.90624999999999989,"right":0.35549999999999998,"bottom":0.13541666666666669},"atlasBounds":{"left":631.5,"top":0.5,"right":649.5,"bottom":50.5}},{"unicode":12316,"advance":1,"planeBounds":{"left":-3.552713678800501e-17,"top":-0.57291666666666663,"right":0.99999999999999989,"bottom":-0.19791666666666666},"atlasBounds":{"left":318.5,"top":797.5,"right":366.5,"bottom":815.5}},{"unicode":12317,"advance":1,"planeBounds":{"left":0.624,"top":-0.90625,"right":0.999,"bottom":-0.40625},"atlasBounds":{"left":782.5,"top":827.5,"right":800.5,"bottom":851.5}},{"unicode":12318,"advance":1,"planeBounds":{"left":0.0010000000000000302,"top":-0.90625,"right":0.376,"bottom":-0.40625},"atlasBounds":{"left":801.5,"top":827.5,"right":819.5,"bottom":851.5}},{"unicode":12319,"advance":1,"planeBounds":{"left":0.0010000000000000302,"top":-0.36458333333333326,"right":0.376,"bottom":0.13541666666666669},"atlasBounds":{"left":763.5,"top":827.5,"right":781.5,"bottom":851.5}},{"unicode":12320,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.78125,"right":1.0104166666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":72.5,"top":604.5,"right":121.5,"bottom":644.5}},{"unicode":12321,"advance":1,"planeBounds":{"left":0.41466666666666663,"top":-0.86458333333333337,"right":0.58133333333333326,"bottom":0.09375},"atlasBounds":{"left":93.5,"top":244.5,"right":101.5,"bottom":290.5}},{"unicode":12322,"advance":1,"planeBounds":{"left":0.28575,"top":-0.86458333333333337,"right":0.72325000000000006,"bottom":0.09375},"atlasBounds":{"left":102.5,"top":244.5,"right":123.5,"bottom":290.5}},{"unicode":12323,"advance":1,"planeBounds":{"left":0.18024999999999997,"top":-0.86458333333333326,"right":0.86775000000000002,"bottom":0.11458333333333333},"atlasBounds":{"left":178.5,"top":195.5,"right":211.5,"bottom":242.5}},{"unicode":12324,"advance":1,"planeBounds":{"left":0.066916666666666638,"top":-0.82291666666666663,"right":0.92108333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":490.5,"top":429.5,"right":531.5,"bottom":472.5}},{"unicode":12325,"advance":1,"planeBounds":{"left":0.20183333333333331,"top":-0.84374999999999989,"right":0.78516666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":588.5,"top":338.5,"right":616.5,"bottom":382.5}},{"unicode":12326,"advance":1,"planeBounds":{"left":0.016333333333333293,"top":-0.82291666666666663,"right":0.97466666666666668,"bottom":-0.40625},"atlasBounds":{"left":809.5,"top":686.5,"right":855.5,"bottom":706.5}},{"unicode":12327,"advance":1,"planeBounds":{"left":0.016833333333333308,"top":-0.82291666666666663,"right":0.97516666666666663,"bottom":-0.11458333333333334},"atlasBounds":{"left":749.5,"top":59.5,"right":795.5,"bottom":93.5}},{"unicode":12328,"advance":1,"planeBounds":{"left":0.027249999999999972,"top":-0.84375,"right":0.96474999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":532.5,"top":429.5,"right":577.5,"bottom":472.5}},{"unicode":12329,"advance":1,"planeBounds":{"left":0.03816666666666664,"top":-0.86458333333333337,"right":0.95483333333333331,"bottom":0.09375},"atlasBounds":{"left":124.5,"top":244.5,"right":168.5,"bottom":290.5}},{"unicode":12330,"advance":0,"planeBounds":{"left":-1.0449166666666667,"top":0.072916666666666685,"right":-0.77408333333333346,"bottom":0.36458333333333331},"atlasBounds":{"left":484.5,"top":797.5,"right":497.5,"bottom":811.5}},{"unicode":12331,"advance":0,"planeBounds":{"left":-1.0449166666666667,"top":-1.1145833333333333,"right":-0.77408333333333346,"bottom":-0.82291666666666663},"atlasBounds":{"left":456.5,"top":797.5,"right":469.5,"bottom":811.5}},{"unicode":12332,"advance":0,"planeBounds":{"left":-0.22591666666666668,"top":-1.1145833333333333,"right":0.04491666666666666,"bottom":-0.82291666666666663},"atlasBounds":{"left":416.5,"top":797.5,"right":429.5,"bottom":811.5}},{"unicode":12333,"advance":0,"planeBounds":{"left":-0.22591666666666668,"top":0.072916666666666685,"right":0.04491666666666666,"bottom":0.36458333333333331},"atlasBounds":{"left":470.5,"top":797.5,"right":483.5,"bottom":811.5}},{"unicode":12334,"advance":0.25,"planeBounds":{"left":0.010416666666666659,"top":-0.51041666666666663,"right":0.23958333333333331,"bottom":-0.26041666666666669},"atlasBounds":{"left":844.5,"top":59.5,"right":855.5,"bottom":71.5}},{"unicode":12335,"advance":0.25,"planeBounds":{"left":0.010416666666666659,"top":-0.61458333333333337,"right":0.23958333333333331,"bottom":-0.15625},"atlasBounds":{"left":56.5,"top":797.5,"right":67.5,"bottom":819.5}},{"unicode":12336,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":-0.57291666666666663,"right":1.0520833333333333,"bottom":-0.19791666666666666},"atlasBounds":{"left":264.5,"top":797.5,"right":317.5,"bottom":815.5}},{"unicode":12337,"advance":1,"planeBounds":{"left":0.14290151515151517,"top":-1.3854166666666665,"right":0.78873484848484854,"bottom":0.59375},"atlasBounds":{"left":0.5,"top":0.5,"right":31.5,"bottom":95.5}},{"unicode":12338,"advance":1,"planeBounds":{"left":0.13690151515151516,"top":-1.3854166666666665,"right":0.90773484848484853,"bottom":0.59375},"atlasBounds":{"left":32.5,"top":0.5,"right":69.5,"bottom":95.5}},{"unicode":12339,"advance":1,"planeBounds":{"left":0.13623484848484849,"top":-0.88541666666666652,"right":0.74040151515151498,"bottom":0.17708333333333334},"atlasBounds":{"left":219.5,"top":0.5,"right":248.5,"bottom":51.5}},{"unicode":12340,"advance":1,"planeBounds":{"left":0.13690151515151516,"top":-0.88541666666666652,"right":0.90773484848484853,"bottom":0.17708333333333334},"atlasBounds":{"left":249.5,"top":0.5,"right":286.5,"bottom":51.5}},{"unicode":12341,"advance":1,"planeBounds":{"left":0.14658333333333329,"top":-0.92708333333333337,"right":0.79241666666666666,"bottom":0.09375},"atlasBounds":{"left":50.5,"top":96.5,"right":81.5,"bottom":145.5}},{"unicode":12342,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.88541666666666663,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":82.5,"top":96.5,"right":131.5,"bottom":145.5}},{"unicode":12343,"advance":1,"planeBounds":{"left":-0.012416666666666656,"top":-0.88541666666666663,"right":1.0084166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":132.5,"top":96.5,"right":181.5,"bottom":145.5}},{"unicode":12344,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":212.5,"top":195.5,"right":258.5,"bottom":242.5}},{"unicode":12345,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":259.5,"top":195.5,"right":305.5,"bottom":242.5}},{"unicode":12346,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.13541666666666669},"atlasBounds":{"left":647.5,"top":96.5,"right":693.5,"bottom":144.5}},{"unicode":12347,"advance":1,"planeBounds":{"left":0.1953333333333333,"top":-0.88541666666666663,"right":0.77866666666666662,"bottom":0.13541666666666669},"atlasBounds":{"left":182.5,"top":96.5,"right":210.5,"bottom":145.5}},{"unicode":12348,"advance":1,"planeBounds":{"left":0.062499999999999951,"top":-0.82291666666666663,"right":0.93749999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":104.5,"top":518.5,"right":146.5,"bottom":560.5}},{"unicode":12349,"advance":1,"planeBounds":{"left":0.035659090909090842,"top":-0.76041666666666663,"right":0.97315909090909081,"bottom":0.03125},"atlasBounds":{"left":221.5,"top":686.5,"right":266.5,"bottom":724.5}},{"unicode":12350,"advance":1,"planeBounds":{"left":-4.085620730620576e-17,"top":-0.88541666666666663,"right":0.99999999999999989,"bottom":0.11458333333333333},"atlasBounds":{"left":694.5,"top":96.5,"right":742.5,"bottom":144.5}},{"unicode":12351,"advance":1,"planeBounds":{"left":0.19791666666666663,"top":-0.82291666666666663,"right":0.80208333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":629.5,"top":429.5,"right":658.5,"bottom":472.5}},{"unicode":12353,"advance":1,"planeBounds":{"left":0.1300833333333333,"top":-0.71875,"right":0.9009166666666667,"bottom":0.09375},"atlasBounds":{"left":450.5,"top":645.5,"right":487.5,"bottom":684.5}},{"unicode":12354,"advance":1,"planeBounds":{"left":0.059083333333333321,"top":-0.84375,"right":0.95491666666666664,"bottom":0.09375},"atlasBounds":{"left":706.5,"top":244.5,"right":749.5,"bottom":289.5}},{"unicode":12355,"advance":1,"planeBounds":{"left":0.13308333333333328,"top":-0.61458333333333326,"right":0.9039166666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":646.5,"top":726.5,"right":683.5,"bottom":759.5}},{"unicode":12356,"advance":1,"planeBounds":{"left":0.075083333333333308,"top":-0.76041666666666663,"right":0.97091666666666665,"bottom":0.052083333333333336},"atlasBounds":{"left":488.5,"top":645.5,"right":531.5,"bottom":684.5}},{"unicode":12357,"advance":1,"planeBounds":{"left":0.19549999999999998,"top":-0.69791666666666663,"right":0.82050000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":532.5,"top":645.5,"right":562.5,"bottom":684.5}},{"unicode":12358,"advance":1,"planeBounds":{"left":0.12091666666666664,"top":-0.84375,"right":0.8500833333333333,"bottom":0.09375},"atlasBounds":{"left":750.5,"top":244.5,"right":785.5,"bottom":289.5}},{"unicode":12359,"advance":1,"planeBounds":{"left":0.14049999999999996,"top":-0.69791666666666663,"right":0.89049999999999996,"bottom":0.09375},"atlasBounds":{"left":394.5,"top":686.5,"right":430.5,"bottom":724.5}},{"unicode":12360,"advance":1,"planeBounds":{"left":0.064083333333333325,"top":-0.84375,"right":0.95991666666666664,"bottom":0.09375},"atlasBounds":{"left":786.5,"top":244.5,"right":829.5,"bottom":289.5}},{"unicode":12361,"advance":1,"planeBounds":{"left":0.12174999999999996,"top":-0.67708333333333326,"right":0.93424999999999991,"bottom":0.11458333333333333},"atlasBounds":{"left":431.5,"top":686.5,"right":470.5,"bottom":724.5}},{"unicode":12362,"advance":1,"planeBounds":{"left":0.058749999999999948,"top":-0.84375,"right":0.99624999999999986,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":292.5,"right":45.5,"bottom":337.5}},{"unicode":12363,"advance":1,"planeBounds":{"left":0.03191666666666667,"top":-0.84374999999999989,"right":1.0110833333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":638.5,"top":338.5,"right":685.5,"bottom":382.5}},{"unicode":12364,"advance":1,"planeBounds":{"left":0.019,"top":-0.90625,"right":1.0190000000000001,"bottom":0.09375},"atlasBounds":{"left":743.5,"top":96.5,"right":791.5,"bottom":144.5}},{"unicode":12365,"advance":1,"planeBounds":{"left":0.11366666666666668,"top":-0.84375,"right":0.90533333333333332,"bottom":0.09375},"atlasBounds":{"left":46.5,"top":292.5,"right":84.5,"bottom":337.5}},{"unicode":12366,"advance":1,"planeBounds":{"left":0.076666666666666647,"top":-0.88541666666666663,"right":0.99333333333333329,"bottom":0.09375},"atlasBounds":{"left":306.5,"top":195.5,"right":350.5,"bottom":242.5}},{"unicode":12367,"advance":1,"planeBounds":{"left":0.15277419354838706,"top":-0.86458333333333326,"right":0.77777419354838706,"bottom":0.11458333333333333},"atlasBounds":{"left":351.5,"top":195.5,"right":381.5,"bottom":242.5}},{"unicode":12368,"advance":1,"planeBounds":{"left":0.14268333333333333,"top":-0.86458333333333326,"right":0.89268333333333327,"bottom":0.11458333333333333},"atlasBounds":{"left":382.5,"top":195.5,"right":418.5,"bottom":242.5}},{"unicode":12369,"advance":1,"planeBounds":{"left":0.072583333333333319,"top":-0.82291666666666663,"right":0.9684166666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":85.5,"top":292.5,"right":128.5,"bottom":337.5}},{"unicode":12370,"advance":1,"planeBounds":{"left":0.06433333333333334,"top":-0.86458333333333326,"right":1.0226666666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":792.5,"top":96.5,"right":838.5,"bottom":144.5}},{"unicode":12371,"advance":1,"planeBounds":{"left":0.12899999999999998,"top":-0.76041666666666663,"right":0.87899999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":431.5,"top":604.5,"right":467.5,"bottom":644.5}},{"unicode":12372,"advance":1,"planeBounds":{"left":0.10616666666666666,"top":-0.88541666666666663,"right":1.0228333333333333,"bottom":0.09375},"atlasBounds":{"left":419.5,"top":195.5,"right":463.5,"bottom":242.5}},{"unicode":12373,"advance":1,"planeBounds":{"left":0.11416666666666662,"top":-0.84375,"right":0.90583333333333327,"bottom":0.09375},"atlasBounds":{"left":153.5,"top":292.5,"right":191.5,"bottom":337.5}},{"unicode":12374,"advance":1,"planeBounds":{"left":0.09866666666666668,"top":-0.90625,"right":1.0153333333333332,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":146.5,"right":44.5,"bottom":194.5}},{"unicode":12375,"advance":1,"planeBounds":{"left":0.19249999999999995,"top":-0.82291666666666663,"right":0.94249999999999989,"bottom":0.09375},"atlasBounds":{"left":781.5,"top":338.5,"right":817.5,"bottom":382.5}},{"unicode":12376,"advance":1,"planeBounds":{"left":0.17949999999999994,"top":-0.82291666666666663,"right":0.92949999999999988,"bottom":0.11458333333333333},"atlasBounds":{"left":192.5,"top":292.5,"right":228.5,"bottom":337.5}},{"unicode":12377,"advance":1,"planeBounds":{"left":0.054166666666666606,"top":-0.84375,"right":0.97083333333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":169.5,"top":244.5,"right":213.5,"bottom":290.5}},{"unicode":12378,"advance":1,"planeBounds":{"left":0.020833333333333315,"top":-0.88541666666666663,"right":0.97916666666666663,"bottom":0.11458333333333333},"atlasBounds":{"left":45.5,"top":146.5,"right":91.5,"bottom":194.5}},{"unicode":12379,"advance":1,"planeBounds":{"left":-0.0060833333333333659,"top":-0.82291666666666663,"right":0.9730833333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":474.5,"right":47.5,"bottom":517.5}},{"unicode":12380,"advance":1,"planeBounds":{"left":-0.024333333333333335,"top":-0.84375,"right":1.0173333333333332,"bottom":0.09375},"atlasBounds":{"left":805.5,"top":146.5,"right":855.5,"bottom":191.5}},{"unicode":12381,"advance":1,"planeBounds":{"left":0.051999999999999956,"top":-0.82291666666666663,"right":0.92699999999999994,"bottom":0.09375},"atlasBounds":{"left":31.5,"top":384.5,"right":73.5,"bottom":428.5}},{"unicode":12382,"advance":1,"planeBounds":{"left":0.023250000000000007,"top":-0.82291666666666663,"right":0.96074999999999999,"bottom":0.09375},"atlasBounds":{"left":74.5,"top":384.5,"right":119.5,"bottom":428.5}},{"unicode":12383,"advance":1,"planeBounds":{"left":0.056583333333333312,"top":-0.84375,"right":0.95241666666666669,"bottom":0.09375},"atlasBounds":{"left":253.5,"top":292.5,"right":296.5,"bottom":337.5}},{"unicode":12384,"advance":1,"planeBounds":{"left":0.02091666666666667,"top":-0.84375,"right":1.0000833333333332,"bottom":0.09375},"atlasBounds":{"left":297.5,"top":292.5,"right":344.5,"bottom":337.5}},{"unicode":12385,"advance":1,"planeBounds":{"left":0.06433333333333334,"top":-0.84375,"right":0.89766666666666661,"bottom":0.09375},"atlasBounds":{"left":345.5,"top":292.5,"right":385.5,"bottom":337.5}},{"unicode":12386,"advance":1,"planeBounds":{"left":0.030916666666666637,"top":-0.90625,"right":1.0100833333333332,"bottom":0.09375},"atlasBounds":{"left":92.5,"top":146.5,"right":139.5,"bottom":194.5}},{"unicode":12387,"advance":1,"planeBounds":{"left":0.10966666666666668,"top":-0.55208333333333326,"right":0.90133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":329.5,"top":764.5,"right":367.5,"bottom":794.5}},{"unicode":12388,"advance":1,"planeBounds":{"left":0.021749999999999985,"top":-0.69791666666666663,"right":0.95925000000000005,"bottom":0.03125},"atlasBounds":{"left":352.5,"top":59.5,"right":397.5,"bottom":94.5}},{"unicode":12389,"advance":1,"planeBounds":{"left":0.01541666666666662,"top":-0.88541666666666663,"right":0.99458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":386.5,"top":292.5,"right":433.5,"bottom":337.5}},{"unicode":12390,"advance":1,"planeBounds":{"left":0.035583333333333335,"top":-0.78124999999999989,"right":0.93141666666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":310.5,"top":561.5,"right":353.5,"bottom":602.5}},{"unicode":12391,"advance":1,"planeBounds":{"left":0.034749999999999948,"top":-0.78125,"right":0.97224999999999995,"bottom":0.09375},"atlasBounds":{"left":235.5,"top":518.5,"right":280.5,"bottom":560.5}},{"unicode":12392,"advance":1,"planeBounds":{"left":0.15641666666666665,"top":-0.82291666666666663,"right":0.88558333333333328,"bottom":0.072916666666666657},"atlasBounds":{"left":820.5,"top":429.5,"right":855.5,"bottom":472.5}},{"unicode":12393,"advance":1,"planeBounds":{"left":0.12508333333333332,"top":-0.86458333333333337,"right":1.0209166666666667,"bottom":0.09375},"atlasBounds":{"left":214.5,"top":244.5,"right":257.5,"bottom":290.5}},{"unicode":12394,"advance":1,"planeBounds":{"left":0.044249999999999963,"top":-0.84375,"right":0.9817499999999999,"bottom":0.09375},"atlasBounds":{"left":434.5,"top":292.5,"right":479.5,"bottom":337.5}},{"unicode":12395,"advance":1,"planeBounds":{"left":0.073999999999999969,"top":-0.80208333333333337,"right":0.94899999999999995,"bottom":0.09375},"atlasBounds":{"left":132.5,"top":474.5,"right":174.5,"bottom":517.5}},{"unicode":12396,"advance":1,"planeBounds":{"left":0.030916666666666672,"top":-0.84374999999999989,"right":1.0100833333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":177.5,"top":384.5,"right":224.5,"bottom":428.5}},{"unicode":12397,"advance":1,"planeBounds":{"left":0.0035833333333333377,"top":-0.84375,"right":1.0244166666666668,"bottom":0.09375},"atlasBounds":{"left":480.5,"top":292.5,"right":529.5,"bottom":337.5}},{"unicode":12398,"advance":1,"planeBounds":{"left":0.039666666666666663,"top":-0.76041666666666663,"right":0.95633333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":545.5,"top":604.5,"right":589.5,"bottom":644.5}},{"unicode":12399,"advance":1,"planeBounds":{"left":0.063750000000000001,"top":-0.82291666666666663,"right":1.00125,"bottom":0.09375},"atlasBounds":{"left":225.5,"top":384.5,"right":270.5,"bottom":428.5}},{"unicode":12400,"advance":1,"planeBounds":{"left":0.041916666666666637,"top":-0.88541666666666663,"right":1.0210833333333333,"bottom":0.09375},"atlasBounds":{"left":464.5,"top":195.5,"right":511.5,"bottom":242.5}},{"unicode":12401,"advance":1,"planeBounds":{"left":0.043916666666666639,"top":-0.88541666666666663,"right":1.0230833333333333,"bottom":0.09375},"atlasBounds":{"left":512.5,"top":195.5,"right":559.5,"bottom":242.5}},{"unicode":12402,"advance":1,"planeBounds":{"left":0.058749999999999948,"top":-0.78125,"right":0.99624999999999986,"bottom":0.09375},"atlasBounds":{"left":422.5,"top":518.5,"right":467.5,"bottom":560.5}},{"unicode":12403,"advance":1,"planeBounds":{"left":0.042916666666666638,"top":-0.86458333333333326,"right":1.0220833333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":560.5,"top":195.5,"right":607.5,"bottom":242.5}},{"unicode":12404,"advance":1,"planeBounds":{"left":0.045916666666666675,"top":-0.86458333333333326,"right":1.0250833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":608.5,"top":195.5,"right":655.5,"bottom":242.5}},{"unicode":12405,"advance":1,"planeBounds":{"left":-0.0084166666666667025,"top":-0.82291666666666663,"right":1.0124166666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":175.5,"top":474.5,"right":224.5,"bottom":517.5}},{"unicode":12406,"advance":1,"planeBounds":{"left":-0.030833333333333365,"top":-0.82291666666666663,"right":1.0108333333333333,"bottom":0.09375},"atlasBounds":{"left":271.5,"top":384.5,"right":321.5,"bottom":428.5}},{"unicode":12407,"advance":1,"planeBounds":{"left":-0.03133333333333338,"top":-0.82291666666666663,"right":1.0103333333333333,"bottom":0.09375},"atlasBounds":{"left":322.5,"top":384.5,"right":372.5,"bottom":428.5}},{"unicode":12408,"advance":1,"planeBounds":{"left":0.013916666666666678,"top":-0.69791666666666663,"right":0.99308333333333332,"bottom":0.010416666666666666},"atlasBounds":{"left":796.5,"top":59.5,"right":843.5,"bottom":93.5}},{"unicode":12409,"advance":1,"planeBounds":{"left":0.0049166666666666768,"top":-0.78125,"right":0.98408333333333331,"bottom":0.03125},"atlasBounds":{"left":705.5,"top":645.5,"right":752.5,"bottom":684.5}},{"unicode":12410,"advance":1,"planeBounds":{"left":0.0049166666666666768,"top":-0.76041666666666663,"right":0.98408333333333331,"bottom":0.03125},"atlasBounds":{"left":267.5,"top":686.5,"right":314.5,"bottom":724.5}},{"unicode":12411,"advance":1,"planeBounds":{"left":0.072166666666666657,"top":-0.82291666666666663,"right":0.98883333333333323,"bottom":0.09375},"atlasBounds":{"left":373.5,"top":384.5,"right":417.5,"bottom":428.5}},{"unicode":12412,"advance":1,"planeBounds":{"left":0.043416666666666659,"top":-0.86458333333333337,"right":1.0225833333333332,"bottom":0.09375},"atlasBounds":{"left":258.5,"top":244.5,"right":305.5,"bottom":290.5}},{"unicode":12413,"advance":1,"planeBounds":{"left":0.038500000000000048,"top":-0.90625,"right":1.0385,"bottom":0.09375},"atlasBounds":{"left":175.5,"top":146.5,"right":223.5,"bottom":194.5}},{"unicode":12414,"advance":1,"planeBounds":{"left":0.13858333333333325,"top":-0.84375,"right":0.90941666666666665,"bottom":0.09375},"atlasBounds":{"left":530.5,"top":292.5,"right":567.5,"bottom":337.5}},{"unicode":12415,"advance":1,"planeBounds":{"left":0.020416666666666614,"top":-0.80208333333333326,"right":0.99958333333333327,"bottom":0.11458333333333333},"atlasBounds":{"left":418.5,"top":384.5,"right":465.5,"bottom":428.5}},{"unicode":12416,"advance":1,"planeBounds":{"left":0.048666666666666629,"top":-0.84375,"right":0.96533333333333327,"bottom":0.09375},"atlasBounds":{"left":568.5,"top":292.5,"right":612.5,"bottom":337.5}},{"unicode":12417,"advance":1,"planeBounds":{"left":0.037666666666666668,"top":-0.84375,"right":0.95433333333333326,"bottom":0.09375},"atlasBounds":{"left":613.5,"top":292.5,"right":657.5,"bottom":337.5}},{"unicode":12418,"advance":1,"planeBounds":{"left":0.044499999999999977,"top":-0.84375,"right":0.91949999999999998,"bottom":0.09375},"atlasBounds":{"left":658.5,"top":292.5,"right":700.5,"bottom":337.5}},{"unicode":12419,"advance":1,"planeBounds":{"left":0.070249999999999951,"top":-0.67708333333333326,"right":0.88274999999999992,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":686.5,"right":39.5,"bottom":725.5}},{"unicode":12420,"advance":1,"planeBounds":{"left":0.017749999999999957,"top":-0.86458333333333337,"right":0.95524999999999993,"bottom":0.09375},"atlasBounds":{"left":306.5,"top":244.5,"right":351.5,"bottom":290.5}},{"unicode":12421,"advance":1,"planeBounds":{"left":0.151,"top":-0.67708333333333326,"right":0.90100000000000002,"bottom":0.13541666666666669},"atlasBounds":{"left":753.5,"top":645.5,"right":789.5,"bottom":684.5}},{"unicode":12422,"advance":1,"planeBounds":{"left":0.081999999999999976,"top":-0.84375,"right":0.95699999999999985,"bottom":0.11458333333333333},"atlasBounds":{"left":352.5,"top":244.5,"right":394.5,"bottom":290.5}},{"unicode":12423,"advance":1,"planeBounds":{"left":0.14774999999999999,"top":-0.67708333333333326,"right":0.83525000000000005,"bottom":0.11458333333333333},"atlasBounds":{"left":511.5,"top":686.5,"right":544.5,"bottom":724.5}},{"unicode":12424,"advance":1,"planeBounds":{"left":0.084249999999999992,"top":-0.84375,"right":0.89675000000000005,"bottom":0.09375},"atlasBounds":{"left":701.5,"top":292.5,"right":740.5,"bottom":337.5}},{"unicode":12425,"advance":1,"planeBounds":{"left":0.12958333333333333,"top":-0.84375,"right":0.90041666666666664,"bottom":0.09375},"atlasBounds":{"left":741.5,"top":292.5,"right":778.5,"bottom":337.5}},{"unicode":12426,"advance":1,"planeBounds":{"left":0.16866666666666663,"top":-0.84375,"right":0.83533333333333326,"bottom":0.09375},"atlasBounds":{"left":779.5,"top":292.5,"right":811.5,"bottom":337.5}},{"unicode":12427,"advance":1,"planeBounds":{"left":0.067333333333333301,"top":-0.80208333333333337,"right":0.90066666666666662,"bottom":0.09375},"atlasBounds":{"left":225.5,"top":474.5,"right":265.5,"bottom":517.5}},{"unicode":12428,"advance":1,"planeBounds":{"left":0.0035833333333332986,"top":-0.84375,"right":1.0244166666666668,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":338.5,"right":49.5,"bottom":383.5}},{"unicode":12429,"advance":1,"planeBounds":{"left":0.063333333333333297,"top":-0.80208333333333337,"right":0.89666666666666661,"bottom":0.09375},"atlasBounds":{"left":286.5,"top":474.5,"right":326.5,"bottom":517.5}},{"unicode":12430,"advance":1,"planeBounds":{"left":0.10024999999999999,"top":-0.69791666666666663,"right":0.91275000000000006,"bottom":0.09375},"atlasBounds":{"left":141.5,"top":686.5,"right":180.5,"bottom":724.5}},{"unicode":12431,"advance":1,"planeBounds":{"left":0.012833333333333316,"top":-0.84375,"right":0.97116666666666662,"bottom":0.09375},"atlasBounds":{"left":50.5,"top":338.5,"right":96.5,"bottom":383.5}},{"unicode":12432,"advance":1,"planeBounds":{"left":0.041583333333333299,"top":-0.82291666666666663,"right":0.93741666666666668,"bottom":0.09375},"atlasBounds":{"left":466.5,"top":384.5,"right":509.5,"bottom":428.5}},{"unicode":12433,"advance":1,"planeBounds":{"left":0.015333333333333334,"top":-0.82291666666666663,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":510.5,"top":384.5,"right":556.5,"bottom":428.5}},{"unicode":12434,"advance":1,"planeBounds":{"left":0.050999999999999969,"top":-0.84375,"right":0.92599999999999993,"bottom":0.09375},"atlasBounds":{"left":97.5,"top":338.5,"right":139.5,"bottom":383.5}},{"unicode":12435,"advance":1,"planeBounds":{"left":0.032333333333333339,"top":-0.82291666666666663,"right":0.9906666666666667,"bottom":0.09375},"atlasBounds":{"left":557.5,"top":384.5,"right":603.5,"bottom":428.5}},{"unicode":12436,"advance":1,"planeBounds":{"left":0.088666666666666644,"top":-0.84375,"right":1.0053333333333332,"bottom":0.09375},"atlasBounds":{"left":140.5,"top":338.5,"right":184.5,"bottom":383.5}},{"unicode":12437,"advance":1,"planeBounds":{"left":0.1183333333333333,"top":-0.67708333333333326,"right":0.95166666666666666,"bottom":0.11458333333333333},"atlasBounds":{"left":315.5,"top":686.5,"right":355.5,"bottom":724.5}},{"unicode":12438,"advance":1,"planeBounds":{"left":0.14008333333333331,"top":-0.65624999999999989,"right":0.9109166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":356.5,"top":686.5,"right":393.5,"bottom":724.5}},{"unicode":12441,"advance":0,"planeBounds":{"left":-0.31016666666666665,"top":-0.92708333333333326,"right":0.023166666666666686,"bottom":-0.61458333333333326},"atlasBounds":{"left":839.5,"top":113.5,"right":855.5,"bottom":128.5}},{"unicode":12442,"advance":0,"planeBounds":{"left":-0.27775,"top":-0.92708333333333337,"right":0.034749999999999941,"bottom":-0.59375},"atlasBounds":{"left":832.5,"top":225.5,"right":847.5,"bottom":241.5}},{"unicode":12443,"advance":1,"planeBounds":{"left":-0.033999999999999968,"top":-0.88541666666666663,"right":0.34100000000000003,"bottom":-0.55208333333333326},"atlasBounds":{"left":825.5,"top":707.5,"right":843.5,"bottom":723.5}},{"unicode":12444,"advance":1,"planeBounds":{"left":-0.016249999999999973,"top":-0.86458333333333337,"right":0.29625000000000001,"bottom":-0.53125},"atlasBounds":{"left":809.5,"top":707.5,"right":824.5,"bottom":723.5}},{"unicode":12445,"advance":1,"planeBounds":{"left":0.27758333333333329,"top":-0.69791666666666663,"right":0.79841666666666666,"bottom":-0.010416666666666666},"atlasBounds":{"left":559.5,"top":726.5,"right":584.5,"bottom":759.5}},{"unicode":12446,"advance":1,"planeBounds":{"left":0.23299999999999996,"top":-0.80208333333333326,"right":0.85799999999999987,"bottom":0.010416666666666666},"atlasBounds":{"left":790.5,"top":645.5,"right":820.5,"bottom":684.5}},{"unicode":12447,"advance":1,"planeBounds":{"left":0.029499999999999978,"top":-0.88541666666666663,"right":0.90449999999999997,"bottom":0.11458333333333333},"atlasBounds":{"left":266.5,"top":146.5,"right":308.5,"bottom":194.5}},{"unicode":12448,"advance":1,"planeBounds":{"left":0.23958333333333331,"top":-0.53125,"right":0.76041666666666663,"bottom":-0.23958333333333331},"atlasBounds":{"left":430.5,"top":797.5,"right":455.5,"bottom":811.5}},{"unicode":12449,"advance":1,"planeBounds":{"left":0.12516666666666662,"top":-0.59374999999999989,"right":0.91683333333333328,"bottom":0.13541666666666669},"atlasBounds":{"left":398.5,"top":59.5,"right":436.5,"bottom":94.5}},{"unicode":12450,"advance":1,"planeBounds":{"left":0.079583333333333311,"top":-0.78125,"right":0.9754166666666666,"bottom":0.09375},"atlasBounds":{"left":614.5,"top":518.5,"right":657.5,"bottom":560.5}},{"unicode":12451,"advance":1,"planeBounds":{"left":0.077000000000000013,"top":-0.65624999999999989,"right":0.82700000000000007,"bottom":0.13541666666666669},"atlasBounds":{"left":596.5,"top":686.5,"right":632.5,"bottom":724.5}},{"unicode":12452,"advance":1,"planeBounds":{"left":0.036999999999999991,"top":-0.84374999999999989,"right":0.91200000000000003,"bottom":0.072916666666666657},"atlasBounds":{"left":629.5,"top":384.5,"right":671.5,"bottom":428.5}},{"unicode":12453,"advance":1,"planeBounds":{"left":0.14491666666666664,"top":-0.67708333333333326,"right":0.87408333333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":600.5,"top":645.5,"right":635.5,"bottom":684.5}},{"unicode":12454,"advance":1,"planeBounds":{"left":0.092333333333333351,"top":-0.84375,"right":0.92566666666666664,"bottom":0.09375},"atlasBounds":{"left":185.5,"top":338.5,"right":225.5,"bottom":383.5}},{"unicode":12455,"advance":1,"planeBounds":{"left":0.10516666666666662,"top":-0.57291666666666663,"right":0.89683333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":368.5,"top":764.5,"right":406.5,"bottom":794.5}},{"unicode":12456,"advance":1,"planeBounds":{"left":0.041666666666666664,"top":-0.71874999999999989,"right":0.95833333333333326,"bottom":0.010416666666666666},"atlasBounds":{"left":437.5,"top":59.5,"right":481.5,"bottom":94.5}},{"unicode":12457,"advance":1,"planeBounds":{"left":0.13158333333333333,"top":-0.65625,"right":0.90241666666666664,"bottom":0.11458333333333333},"atlasBounds":{"left":74.5,"top":726.5,"right":111.5,"bottom":763.5}},{"unicode":12458,"advance":1,"planeBounds":{"left":0.044083333333333322,"top":-0.84374999999999989,"right":0.93991666666666662,"bottom":0.072916666666666657},"atlasBounds":{"left":672.5,"top":384.5,"right":715.5,"bottom":428.5}},{"unicode":12459,"advance":1,"planeBounds":{"left":0.06533333333333334,"top":-0.84374999999999989,"right":0.89866666666666661,"bottom":0.072916666666666657},"atlasBounds":{"left":716.5,"top":384.5,"right":756.5,"bottom":428.5}},{"unicode":12460,"advance":1,"planeBounds":{"left":0.038333333333333296,"top":-0.88541666666666663,"right":0.9966666666666667,"bottom":0.09375},"atlasBounds":{"left":696.5,"top":195.5,"right":742.5,"bottom":242.5}},{"unicode":12461,"advance":1,"planeBounds":{"left":0.064999999999999974,"top":-0.84375,"right":0.93999999999999995,"bottom":0.09375},"atlasBounds":{"left":226.5,"top":338.5,"right":268.5,"bottom":383.5}},{"unicode":12462,"advance":1,"planeBounds":{"left":0.037333333333333295,"top":-0.90625,"right":0.9956666666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":211.5,"top":96.5,"right":257.5,"bottom":145.5}},{"unicode":12463,"advance":1,"planeBounds":{"left":0.047999999999999994,"top":-0.86458333333333337,"right":0.92300000000000004,"bottom":0.09375},"atlasBounds":{"left":395.5,"top":244.5,"right":437.5,"bottom":290.5}},{"unicode":12464,"advance":1,"planeBounds":{"left":0.0084999999999999763,"top":-0.88541666666666663,"right":1.0085,"bottom":0.11458333333333333},"atlasBounds":{"left":309.5,"top":146.5,"right":357.5,"bottom":194.5}},{"unicode":12465,"advance":1,"planeBounds":{"left":0.021749999999999953,"top":-0.84375,"right":0.95924999999999994,"bottom":0.09375},"atlasBounds":{"left":269.5,"top":338.5,"right":314.5,"bottom":383.5}},{"unicode":12466,"advance":1,"planeBounds":{"left":0.0029999999999999949,"top":-0.88541666666666663,"right":1.0030000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":358.5,"top":146.5,"right":406.5,"bottom":194.5}},{"unicode":12467,"advance":1,"planeBounds":{"left":0.10816666666666666,"top":-0.73958333333333326,"right":0.89983333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":80.5,"top":686.5,"right":118.5,"bottom":724.5}},{"unicode":12468,"advance":1,"planeBounds":{"left":0.09358333333333331,"top":-0.90625,"right":0.98941666666666661,"bottom":0.09375},"atlasBounds":{"left":407.5,"top":146.5,"right":450.5,"bottom":194.5}},{"unicode":12469,"advance":1,"planeBounds":{"left":0.015333333333333327,"top":-0.82291666666666663,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":782.5,"top":384.5,"right":828.5,"bottom":428.5}},{"unicode":12470,"advance":1,"planeBounds":{"left":0.0024999999999999432,"top":-0.84375,"right":1.0024999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":438.5,"top":244.5,"right":486.5,"bottom":290.5}},{"unicode":12471,"advance":1,"planeBounds":{"left":0.061583333333333309,"top":-0.82291666666666663,"right":0.95741666666666669,"bottom":0.072916666666666657},"atlasBounds":{"left":327.5,"top":474.5,"right":370.5,"bottom":517.5}},{"unicode":12472,"advance":1,"planeBounds":{"left":0.049249999999999974,"top":-0.84375,"right":0.9867499999999999,"bottom":0.09375},"atlasBounds":{"left":315.5,"top":338.5,"right":360.5,"bottom":383.5}},{"unicode":12473,"advance":1,"planeBounds":{"left":0.051583333333333301,"top":-0.76041666666666663,"right":0.94741666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":740.5,"top":561.5,"right":783.5,"bottom":601.5}},{"unicode":12474,"advance":1,"planeBounds":{"left":0.037833333333333316,"top":-0.90625,"right":0.99616666666666664,"bottom":0.09375},"atlasBounds":{"left":451.5,"top":146.5,"right":497.5,"bottom":194.5}},{"unicode":12475,"advance":1,"planeBounds":{"left":0.014666666666666628,"top":-0.82291666666666663,"right":0.93133333333333324,"bottom":0.072916666666666657},"atlasBounds":{"left":371.5,"top":474.5,"right":415.5,"bottom":517.5}},{"unicode":12476,"advance":1,"planeBounds":{"left":0.0014999999999999805,"top":-0.88541666666666663,"right":1.0015000000000001,"bottom":0.09375},"atlasBounds":{"left":768.5,"top":195.5,"right":816.5,"bottom":242.5}},{"unicode":12477,"advance":1,"planeBounds":{"left":0.080750000000000002,"top":-0.78124999999999989,"right":0.89324999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":554.5,"top":561.5,"right":593.5,"bottom":602.5}},{"unicode":12478,"advance":1,"planeBounds":{"left":0.058749999999999997,"top":-0.90625,"right":0.99624999999999997,"bottom":0.09375},"atlasBounds":{"left":498.5,"top":146.5,"right":543.5,"bottom":194.5}},{"unicode":12479,"advance":1,"planeBounds":{"left":0.044999999999999991,"top":-0.86458333333333337,"right":0.92000000000000004,"bottom":0.09375},"atlasBounds":{"left":487.5,"top":244.5,"right":529.5,"bottom":290.5}},{"unicode":12480,"advance":1,"planeBounds":{"left":0.0094999999999999373,"top":-0.90625,"right":1.0094999999999998,"bottom":0.11458333333333333},"atlasBounds":{"left":258.5,"top":96.5,"right":306.5,"bottom":145.5}},{"unicode":12481,"advance":1,"planeBounds":{"left":0.042166666666666609,"top":-0.82291666666666663,"right":0.95883333333333332,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":429.5,"right":44.5,"bottom":473.5}},{"unicode":12482,"advance":1,"planeBounds":{"left":0.013916666666666673,"top":-0.80208333333333326,"right":0.99308333333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":45.5,"top":429.5,"right":92.5,"bottom":473.5}},{"unicode":12483,"advance":1,"planeBounds":{"left":0.12558333333333335,"top":-0.63541666666666663,"right":0.89641666666666675,"bottom":0.13541666666666669},"atlasBounds":{"left":112.5,"top":726.5,"right":149.5,"bottom":763.5}},{"unicode":12484,"advance":1,"planeBounds":{"left":0.051083333333333279,"top":-0.80208333333333337,"right":0.94691666666666663,"bottom":0.09375},"atlasBounds":{"left":416.5,"top":474.5,"right":459.5,"bottom":517.5}},{"unicode":12485,"advance":1,"planeBounds":{"left":0.028916666666666636,"top":-0.90625,"right":1.0080833333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":307.5,"top":96.5,"right":354.5,"bottom":145.5}},{"unicode":12486,"advance":1,"planeBounds":{"left":0.046666666666666662,"top":-0.80208333333333337,"right":0.96333333333333326,"bottom":0.09375},"atlasBounds":{"left":460.5,"top":474.5,"right":504.5,"bottom":517.5}},{"unicode":12487,"advance":1,"planeBounds":{"left":0.042416666666666658,"top":-0.90625,"right":1.0215833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":355.5,"top":96.5,"right":402.5,"bottom":145.5}},{"unicode":12488,"advance":1,"planeBounds":{"left":0.28591666666666665,"top":-0.82291666666666663,"right":0.89008333333333323,"bottom":0.072916666666666657},"atlasBounds":{"left":531.5,"top":474.5,"right":560.5,"bottom":517.5}},{"unicode":12489,"advance":1,"planeBounds":{"left":0.25066666666666665,"top":-0.82291666666666663,"right":0.91733333333333322,"bottom":0.09375},"atlasBounds":{"left":93.5,"top":429.5,"right":125.5,"bottom":473.5}},{"unicode":12490,"advance":1,"planeBounds":{"left":0.05116666666666661,"top":-0.82291666666666663,"right":0.96783333333333332,"bottom":0.09375},"atlasBounds":{"left":126.5,"top":429.5,"right":170.5,"bottom":473.5}},{"unicode":12491,"advance":1,"planeBounds":{"left":0.041583333333333299,"top":-0.69791666666666663,"right":0.93741666666666668,"bottom":-0.010416666666666666},"atlasBounds":{"left":456.5,"top":726.5,"right":499.5,"bottom":759.5}},{"unicode":12492,"advance":1,"planeBounds":{"left":0.082166666666666666,"top":-0.76041666666666663,"right":0.87383333333333324,"bottom":0.09375},"atlasBounds":{"left":366.5,"top":561.5,"right":404.5,"bottom":602.5}},{"unicode":12493,"advance":1,"planeBounds":{"left":0.033749999999999988,"top":-0.84375,"right":0.97125000000000006,"bottom":0.09375},"atlasBounds":{"left":361.5,"top":338.5,"right":406.5,"bottom":383.5}},{"unicode":12494,"advance":1,"planeBounds":{"left":0.095999999999999974,"top":-0.80208333333333326,"right":0.84599999999999986,"bottom":0.072916666666666657},"atlasBounds":{"left":46.5,"top":561.5,"right":82.5,"bottom":603.5}},{"unicode":12495,"advance":1,"planeBounds":{"left":0.030749999999999951,"top":-0.76041666666666663,"right":0.96824999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":636.5,"top":645.5,"right":681.5,"bottom":684.5}},{"unicode":12496,"advance":1,"planeBounds":{"left":0.021916666666666678,"top":-0.86458333333333326,"right":1.0010833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":171.5,"top":429.5,"right":218.5,"bottom":473.5}},{"unicode":12497,"advance":1,"planeBounds":{"left":0.012499999999999973,"top":-0.86458333333333326,"right":1.0125,"bottom":0.052083333333333336},"atlasBounds":{"left":219.5,"top":429.5,"right":267.5,"bottom":473.5}},{"unicode":12498,"advance":1,"planeBounds":{"left":0.17741666666666667,"top":-0.82291666666666663,"right":0.9065833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":785.5,"top":518.5,"right":820.5,"bottom":560.5}},{"unicode":12499,"advance":1,"planeBounds":{"left":0.13733333333333328,"top":-0.88541666666666663,"right":0.97066666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":530.5,"top":244.5,"right":570.5,"bottom":290.5}},{"unicode":12500,"advance":1,"planeBounds":{"left":0.13441666666666663,"top":-0.86458333333333326,"right":0.98858333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":407.5,"top":338.5,"right":448.5,"bottom":383.5}},{"unicode":12501,"advance":1,"planeBounds":{"left":0.096749999999999961,"top":-0.76041666666666663,"right":0.90924999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":640.5,"top":604.5,"right":679.5,"bottom":644.5}},{"unicode":12502,"advance":1,"planeBounds":{"left":0.079250000000000015,"top":-0.90625,"right":1.01675,"bottom":0.09375},"atlasBounds":{"left":544.5,"top":146.5,"right":589.5,"bottom":194.5}},{"unicode":12503,"advance":1,"planeBounds":{"left":0.088250000000000065,"top":-0.88541666666666663,"right":1.0257499999999999,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":244.5,"right":45.5,"bottom":291.5}},{"unicode":12504,"advance":1,"planeBounds":{"left":0.015416666666666655,"top":-0.67708333333333326,"right":0.99458333333333326,"bottom":0.010416666666666666},"atlasBounds":{"left":408.5,"top":726.5,"right":455.5,"bottom":759.5}},{"unicode":12505,"advance":1,"planeBounds":{"left":0.0064166666666666556,"top":-0.78125,"right":0.98558333333333326,"bottom":0.03125},"atlasBounds":{"left":329.5,"top":645.5,"right":376.5,"bottom":684.5}},{"unicode":12506,"advance":1,"planeBounds":{"left":0.0064166666666666556,"top":-0.78125,"right":0.98558333333333326,"bottom":0.03125},"atlasBounds":{"left":377.5,"top":645.5,"right":424.5,"bottom":684.5}},{"unicode":12507,"advance":1,"planeBounds":{"left":0.038250000000000006,"top":-0.84374999999999989,"right":0.97575000000000001,"bottom":0.072916666666666657},"atlasBounds":{"left":292.5,"top":429.5,"right":337.5,"bottom":473.5}},{"unicode":12508,"advance":1,"planeBounds":{"left":0.016916666666666674,"top":-0.86458333333333337,"right":0.99608333333333332,"bottom":0.09375},"atlasBounds":{"left":571.5,"top":244.5,"right":618.5,"bottom":290.5}},{"unicode":12509,"advance":1,"planeBounds":{"left":0.015333333333333332,"top":-0.90625,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":590.5,"top":146.5,"right":636.5,"bottom":194.5}},{"unicode":12510,"advance":1,"planeBounds":{"left":0.05766666666666663,"top":-0.73958333333333337,"right":0.97433333333333327,"bottom":0.09375},"atlasBounds":{"left":468.5,"top":604.5,"right":512.5,"bottom":644.5}},{"unicode":12511,"advance":1,"planeBounds":{"left":0.10900000000000001,"top":-0.80208333333333326,"right":0.85899999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":354.5,"top":518.5,"right":390.5,"bottom":560.5}},{"unicode":12512,"advance":1,"planeBounds":{"left":0.024833333333333273,"top":-0.82291666666666663,"right":0.98316666666666663,"bottom":0.09375},"atlasBounds":{"left":338.5,"top":429.5,"right":384.5,"bottom":473.5}},{"unicode":12513,"advance":1,"planeBounds":{"left":0.066916666666666638,"top":-0.80208333333333326,"right":0.92108333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":281.5,"top":518.5,"right":322.5,"bottom":560.5}},{"unicode":12514,"advance":1,"planeBounds":{"left":0.063083333333333325,"top":-0.76041666666666663,"right":0.95891666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":812.5,"top":561.5,"right":855.5,"bottom":601.5}},{"unicode":12515,"advance":1,"planeBounds":{"left":0.10175000000000001,"top":-0.65624999999999989,"right":0.91425000000000001,"bottom":0.13541666666666669},"atlasBounds":{"left":471.5,"top":686.5,"right":510.5,"bottom":724.5}},{"unicode":12516,"advance":1,"planeBounds":{"left":0.021250000000000012,"top":-0.84375,"right":0.95874999999999999,"bottom":0.09375},"atlasBounds":{"left":449.5,"top":338.5,"right":494.5,"bottom":383.5}},{"unicode":12517,"advance":1,"planeBounds":{"left":0.10666666666666669,"top":-0.55208333333333326,"right":0.89833333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":573.5,"top":764.5,"right":611.5,"bottom":793.5}},{"unicode":12518,"advance":1,"planeBounds":{"left":0.033249999999999974,"top":-0.71875,"right":0.97074999999999989,"bottom":-0.010416666666666666},"atlasBounds":{"left":231.5,"top":726.5,"right":276.5,"bottom":760.5}},{"unicode":12519,"advance":1,"planeBounds":{"left":0.15916666666666662,"top":-0.59375,"right":0.82583333333333331,"bottom":0.11458333333333333},"atlasBounds":{"left":176.5,"top":726.5,"right":208.5,"bottom":760.5}},{"unicode":12520,"advance":1,"planeBounds":{"left":0.10658333333333332,"top":-0.76041666666666663,"right":0.87741666666666662,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":604.5,"right":37.5,"bottom":644.5}},{"unicode":12521,"advance":1,"planeBounds":{"left":0.092833333333333337,"top":-0.80208333333333337,"right":0.92616666666666669,"bottom":0.09375},"atlasBounds":{"left":648.5,"top":474.5,"right":688.5,"bottom":517.5}},{"unicode":12522,"advance":1,"planeBounds":{"left":0.17508333333333329,"top":-0.80208333333333337,"right":0.82091666666666663,"bottom":0.09375},"atlasBounds":{"left":689.5,"top":474.5,"right":720.5,"bottom":517.5}},{"unicode":12523,"advance":1,"planeBounds":{"left":0.019416666666666658,"top":-0.80208333333333326,"right":0.99858333333333327,"bottom":0.072916666666666657},"atlasBounds":{"left":782.5,"top":474.5,"right":829.5,"bottom":516.5}},{"unicode":12524,"advance":1,"planeBounds":{"left":0.17908333333333329,"top":-0.80208333333333326,"right":0.94991666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":744.5,"top":474.5,"right":781.5,"bottom":516.5}},{"unicode":12525,"advance":1,"planeBounds":{"left":0.096250000000000016,"top":-0.73958333333333326,"right":0.90875000000000006,"bottom":0.052083333333333336},"atlasBounds":{"left":40.5,"top":686.5,"right":79.5,"bottom":724.5}},{"unicode":12526,"advance":1,"planeBounds":{"left":0.14241666666666664,"top":-0.59374999999999989,"right":0.87158333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":482.5,"top":59.5,"right":517.5,"bottom":94.5}},{"unicode":12527,"advance":1,"planeBounds":{"left":0.087333333333333305,"top":-0.76041666666666663,"right":0.92066666666666663,"bottom":0.09375},"atlasBounds":{"left":405.5,"top":561.5,"right":445.5,"bottom":602.5}},{"unicode":12528,"advance":1,"planeBounds":{"left":0.045666666666666668,"top":-0.82291666666666663,"right":0.96233333333333326,"bottom":0.09375},"atlasBounds":{"left":385.5,"top":429.5,"right":429.5,"bottom":473.5}},{"unicode":12529,"advance":1,"planeBounds":{"left":0.036250000000000004,"top":-0.73958333333333326,"right":0.97375,"bottom":0.010416666666666666},"atlasBounds":{"left":112.5,"top":59.5,"right":157.5,"bottom":95.5}},{"unicode":12530,"advance":1,"planeBounds":{"left":0.11475,"top":-0.76041666666666663,"right":0.92725000000000002,"bottom":0.09375},"atlasBounds":{"left":204.5,"top":561.5,"right":243.5,"bottom":602.5}},{"unicode":12531,"advance":1,"planeBounds":{"left":0.094499999999999959,"top":-0.78124999999999989,"right":0.96949999999999992,"bottom":0.072916666666666657},"atlasBounds":{"left":267.5,"top":561.5,"right":309.5,"bottom":602.5}},{"unicode":12532,"advance":1,"planeBounds":{"left":0.07166666666666667,"top":-0.90625,"right":0.98833333333333329,"bottom":0.09375},"atlasBounds":{"left":637.5,"top":146.5,"right":681.5,"bottom":194.5}},{"unicode":12533,"advance":1,"planeBounds":{"left":0.11791666666666668,"top":-0.65625,"right":0.8470833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":38.5,"top":726.5,"right":73.5,"bottom":763.5}},{"unicode":12534,"advance":1,"planeBounds":{"left":0.083249999999999991,"top":-0.65624999999999989,"right":0.89575000000000005,"bottom":0.13541666666666669},"atlasBounds":{"left":181.5,"top":686.5,"right":220.5,"bottom":724.5}},{"unicode":12535,"advance":1,"planeBounds":{"left":0.065666666666666651,"top":-0.90625,"right":0.98233333333333328,"bottom":0.11458333333333333},"atlasBounds":{"left":403.5,"top":96.5,"right":447.5,"bottom":145.5}},{"unicode":12536,"advance":1,"planeBounds":{"left":0.037333333333333336,"top":-0.88541666666666663,"right":0.9956666666666667,"bottom":0.09375},"atlasBounds":{"left":46.5,"top":244.5,"right":92.5,"bottom":291.5}},{"unicode":12537,"advance":1,"planeBounds":{"left":0.024333333333333294,"top":-0.90625,"right":0.98266666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":619.5,"top":244.5,"right":665.5,"bottom":290.5}},{"unicode":12538,"advance":1,"planeBounds":{"left":0.085083333333333344,"top":-0.90625,"right":0.98091666666666666,"bottom":0.11458333333333333},"atlasBounds":{"left":448.5,"top":96.5,"right":491.5,"bottom":145.5}},{"unicode":12539,"advance":1,"planeBounds":{"left":0.34375,"top":-0.53125,"right":0.65625,"bottom":-0.21875},"atlasBounds":{"left":839.5,"top":129.5,"right":854.5,"bottom":144.5}},{"unicode":12540,"advance":1,"planeBounds":{"left":0.051583333333333294,"top":-0.48958333333333331,"right":0.94741666666666668,"bottom":-0.28125},"atlasBounds":{"left":514.5,"top":797.5,"right":557.5,"bottom":807.5}},{"unicode":12541,"advance":1,"planeBounds":{"left":0.24766666666666665,"top":-0.63541666666666663,"right":0.78933333333333322,"bottom":-0.052083333333333329},"atlasBounds":{"left":40.5,"top":827.5,"right":66.5,"bottom":855.5}},{"unicode":12542,"advance":1,"planeBounds":{"left":0.22349999999999998,"top":-0.78124999999999989,"right":0.84850000000000003,"bottom":-0.052083333333333329},"atlasBounds":{"left":518.5,"top":59.5,"right":548.5,"bottom":94.5}},{"unicode":12543,"advance":1,"planeBounds":{"left":0.14924999999999999,"top":-0.82291666666666663,"right":0.83674999999999999,"bottom":0.09375},"atlasBounds":{"left":430.5,"top":429.5,"right":463.5,"bottom":473.5}},{"unicode":12784,"advance":1,"planeBounds":{"left":0.10549999999999998,"top":-0.69791666666666663,"right":0.85550000000000004,"bottom":0.11458333333333333},"atlasBounds":{"left":563.5,"top":645.5,"right":599.5,"bottom":684.5}},{"unicode":12785,"advance":1,"planeBounds":{"left":0.1330833333333333,"top":-0.67708333333333337,"right":0.9039166666666667,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":726.5,"right":37.5,"bottom":763.5}},{"unicode":12786,"advance":1,"planeBounds":{"left":0.12199999999999997,"top":-0.61458333333333337,"right":0.87199999999999989,"bottom":0.09375},"atlasBounds":{"left":311.5,"top":726.5,"right":347.5,"bottom":760.5}},{"unicode":12787,"advance":1,"planeBounds":{"left":0.32058333333333333,"top":-0.65625,"right":0.8414166666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":150.5,"top":726.5,"right":175.5,"bottom":763.5}},{"unicode":12788,"advance":1,"planeBounds":{"left":0.14016666666666666,"top":-0.61458333333333326,"right":0.80683333333333329,"bottom":0.11458333333333333},"atlasBounds":{"left":549.5,"top":59.5,"right":581.5,"bottom":94.5}},{"unicode":12789,"advance":1,"planeBounds":{"left":0.10966666666666668,"top":-0.61458333333333326,"right":0.90133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":717.5,"top":726.5,"right":755.5,"bottom":759.5}},{"unicode":12790,"advance":1,"planeBounds":{"left":0.21599999999999994,"top":-0.67708333333333326,"right":0.84099999999999986,"bottom":0.072916666666666657},"atlasBounds":{"left":158.5,"top":59.5,"right":188.5,"bottom":95.5}},{"unicode":12791,"advance":1,"planeBounds":{"left":0.15974999999999995,"top":-0.63541666666666663,"right":0.84724999999999995,"bottom":0.072916666666666657},"atlasBounds":{"left":277.5,"top":726.5,"right":310.5,"bottom":760.5}},{"unicode":12792,"advance":1,"planeBounds":{"left":0.099749999999999978,"top":-0.55208333333333337,"right":0.91224999999999989,"bottom":0.03125},"atlasBounds":{"left":0.5,"top":827.5,"right":39.5,"bottom":855.5}},{"unicode":12793,"advance":1,"planeBounds":{"left":0.082749999999999962,"top":-0.69791666666666663,"right":0.89524999999999988,"bottom":0.072916666666666657},"atlasBounds":{"left":747.5,"top":686.5,"right":786.5,"bottom":723.5}},{"unicode":12794,"advance":1,"planeBounds":{"left":0.11975000000000001,"top":-0.65625,"right":0.93225000000000002,"bottom":0.11458333333333333},"atlasBounds":{"left":707.5,"top":686.5,"right":746.5,"bottom":723.5}},{"unicode":12795,"advance":1,"planeBounds":{"left":0.13483333333333328,"top":-0.63541666666666663,"right":0.84316666666666662,"bottom":0.11458333333333333},"atlasBounds":{"left":189.5,"top":59.5,"right":223.5,"bottom":95.5}},{"unicode":12796,"advance":1,"planeBounds":{"left":0.20774999999999996,"top":-0.65625,"right":0.77024999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":679.5,"top":686.5,"right":706.5,"bottom":723.5}},{"unicode":12797,"advance":1,"planeBounds":{"left":0.094333333333333297,"top":-0.65624999999999989,"right":0.92766666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":582.5,"top":59.5,"right":622.5,"bottom":94.5}},{"unicode":12798,"advance":1,"planeBounds":{"left":0.25766666666666665,"top":-0.63541666666666663,"right":0.92433333333333323,"bottom":0.09375},"atlasBounds":{"left":623.5,"top":59.5,"right":655.5,"bottom":94.5}},{"unicode":12799,"advance":1,"planeBounds":{"left":0.15725,"top":-0.59374999999999989,"right":0.84475,"bottom":0.072916666666666657},"atlasBounds":{"left":785.5,"top":726.5,"right":818.5,"bottom":758.5}},{"unicode":65281,"advance":1,"planeBounds":{"left":0.38541666666666669,"top":-0.80208333333333326,"right":0.61458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":354.5,"top":561.5,"right":365.5,"bottom":602.5}},{"unicode":65282,"advance":1,"planeBounds":{"left":0.28125,"top":-0.92708333333333326,"right":0.71875,"bottom":-0.48958333333333331},"atlasBounds":{"left":68.5,"top":797.5,"right":89.5,"bottom":818.5}},{"unicode":65283,"advance":1,"planeBounds":{"left":0.11658333333333336,"top":-0.80208333333333326,"right":0.88741666666666674,"bottom":0.052083333333333336},"atlasBounds":{"left":144.5,"top":561.5,"right":181.5,"bottom":602.5}},{"unicode":65284,"advance":1,"planeBounds":{"left":0.18299999999999997,"top":-0.92708333333333326,"right":0.80800000000000005,"bottom":0.17708333333333334},"atlasBounds":{"left":104.5,"top":0.5,"right":134.5,"bottom":53.5}},{"unicode":65285,"advance":1,"planeBounds":{"left":0.031749999999999952,"top":-0.82291666666666663,"right":0.96924999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":561.5,"right":45.5,"bottom":603.5}},{"unicode":65286,"advance":1,"planeBounds":{"left":0.14891666666666661,"top":-0.80208333333333326,"right":0.87808333333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":749.5,"top":518.5,"right":784.5,"bottom":560.5}},{"unicode":65287,"advance":1,"planeBounds":{"left":0.39583333333333331,"top":-0.92708333333333326,"right":0.60416666666666663,"bottom":-0.48958333333333331},"atlasBounds":{"left":844.5,"top":72.5,"right":854.5,"bottom":93.5}},{"unicode":65288,"advance":1,"planeBounds":{"left":0.64741666666666664,"top":-0.90624999999999989,"right":1.0015833333333333,"bottom":0.15625},"atlasBounds":{"left":287.5,"top":0.5,"right":304.5,"bottom":51.5}},{"unicode":65289,"advance":1,"planeBounds":{"left":-0.001583333333333309,"top":-0.90624999999999989,"right":0.3525833333333333,"bottom":0.15625},"atlasBounds":{"left":305.5,"top":0.5,"right":322.5,"bottom":51.5}},{"unicode":65290,"advance":1,"planeBounds":{"left":0.10516666666666666,"top":-0.82291666666666663,"right":0.89683333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":65.5,"top":518.5,"right":103.5,"bottom":560.5}},{"unicode":65291,"advance":1,"planeBounds":{"left":0.09375,"top":-0.78125,"right":0.90625,"bottom":0.03125},"atlasBounds":{"left":258.5,"top":645.5,"right":297.5,"bottom":684.5}},{"unicode":65292,"advance":1,"planeBounds":{"left":0.086666666666666711,"top":-0.28125,"right":0.37833333333333335,"bottom":0.15625},"atlasBounds":{"left":90.5,"top":797.5,"right":104.5,"bottom":818.5}},{"unicode":65293,"advance":1,"planeBounds":{"left":0.09375,"top":-0.46875,"right":0.90625,"bottom":-0.28125},"atlasBounds":{"left":558.5,"top":797.5,"right":597.5,"bottom":806.5}},{"unicode":65294,"advance":1,"planeBounds":{"left":0.11400000000000003,"top":-0.21875,"right":0.36399999999999999,"bottom":0.052083333333333336},"atlasBounds":{"left":843.5,"top":827.5,"right":855.5,"bottom":840.5}},{"unicode":65295,"advance":1,"planeBounds":{"left":-0.010416666666666696,"top":-0.90624999999999989,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":673.5,"top":0.5,"right":722.5,"bottom":50.5}},{"unicode":65296,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.80208333333333326,"right":0.82291666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":563.5,"top":518.5,"right":594.5,"bottom":560.5}},{"unicode":65297,"advance":1,"planeBounds":{"left":0.19599999999999995,"top":-0.78125,"right":0.82099999999999995,"bottom":0.052083333333333336},"atlasBounds":{"left":227.5,"top":645.5,"right":257.5,"bottom":685.5}},{"unicode":65298,"advance":1,"planeBounds":{"left":0.18599999999999997,"top":-0.80208333333333326,"right":0.81100000000000005,"bottom":0.052083333333333336},"atlasBounds":{"left":523.5,"top":561.5,"right":553.5,"bottom":602.5}},{"unicode":65299,"advance":1,"planeBounds":{"left":0.17199999999999999,"top":-0.80208333333333326,"right":0.79700000000000004,"bottom":0.072916666666666657},"atlasBounds":{"left":532.5,"top":518.5,"right":562.5,"bottom":560.5}},{"unicode":65300,"advance":1,"planeBounds":{"left":0.17316666666666666,"top":-0.78125,"right":0.83983333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":735.5,"top":604.5,"right":767.5,"bottom":644.5}},{"unicode":65301,"advance":1,"planeBounds":{"left":0.17449999999999999,"top":-0.78124999999999989,"right":0.79949999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":492.5,"top":561.5,"right":522.5,"bottom":602.5}},{"unicode":65302,"advance":1,"planeBounds":{"left":0.18949999999999997,"top":-0.80208333333333326,"right":0.8145,"bottom":0.072916666666666657},"atlasBounds":{"left":468.5,"top":518.5,"right":498.5,"bottom":560.5}},{"unicode":65303,"advance":1,"planeBounds":{"left":0.18899999999999995,"top":-0.78125,"right":0.81399999999999995,"bottom":0.052083333333333336},"atlasBounds":{"left":680.5,"top":604.5,"right":710.5,"bottom":644.5}},{"unicode":65304,"advance":1,"planeBounds":{"left":0.18649999999999997,"top":-0.80208333333333326,"right":0.8115,"bottom":0.072916666666666657},"atlasBounds":{"left":391.5,"top":518.5,"right":421.5,"bottom":560.5}},{"unicode":65305,"advance":1,"planeBounds":{"left":0.18549999999999997,"top":-0.80208333333333326,"right":0.8105,"bottom":0.072916666666666657},"atlasBounds":{"left":323.5,"top":518.5,"right":353.5,"bottom":560.5}},{"unicode":65306,"advance":1,"planeBounds":{"left":0.375,"top":-0.73958333333333326,"right":0.625,"bottom":-0.010416666666666666},"atlasBounds":{"left":656.5,"top":59.5,"right":668.5,"bottom":94.5}},{"unicode":65307,"advance":1,"planeBounds":{"left":0.34816666666666668,"top":-0.73958333333333337,"right":0.63983333333333325,"bottom":0.15625},"atlasBounds":{"left":91.5,"top":474.5,"right":105.5,"bottom":517.5}},{"unicode":65308,"advance":1,"planeBounds":{"left":0.05699999999999996,"top":-0.82291666666666663,"right":0.93199999999999994,"bottom":0.072916666666666657},"atlasBounds":{"left":48.5,"top":474.5,"right":90.5,"bottom":517.5}},{"unicode":65309,"advance":1,"planeBounds":{"left":0.09375,"top":-0.57291666666666663,"right":0.90625,"bottom":-0.17708333333333331},"atlasBounds":{"left":205.5,"top":797.5,"right":244.5,"bottom":816.5}},{"unicode":65310,"advance":1,"planeBounds":{"left":0.067999999999999977,"top":-0.82291666666666663,"right":0.94299999999999995,"bottom":0.072916666666666657},"atlasBounds":{"left":777.5,"top":429.5,"right":819.5,"bottom":472.5}},{"unicode":65311,"advance":1,"planeBounds":{"left":0.20533333333333328,"top":-0.82291666666666663,"right":0.78866666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":518.5,"right":28.5,"bottom":560.5}},{"unicode":65312,"advance":1,"planeBounds":{"left":0.031249999999999965,"top":-0.88541666666666663,"right":0.96874999999999989,"bottom":0.11458333333333333},"atlasBounds":{"left":46.5,"top":195.5,"right":91.5,"bottom":243.5}},{"unicode":65313,"advance":1,"planeBounds":{"left":0.10958333333333337,"top":-0.78125,"right":0.88041666666666674,"bottom":0.052083333333333336},"atlasBounds":{"left":393.5,"top":604.5,"right":430.5,"bottom":644.5}},{"unicode":65314,"advance":1,"planeBounds":{"left":0.19416666666666665,"top":-0.78125,"right":0.86083333333333323,"bottom":0.052083333333333336},"atlasBounds":{"left":360.5,"top":604.5,"right":392.5,"bottom":644.5}},{"unicode":65315,"advance":1,"planeBounds":{"left":0.1588333333333333,"top":-0.80208333333333326,"right":0.86716666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":147.5,"top":518.5,"right":181.5,"bottom":560.5}},{"unicode":65316,"advance":1,"planeBounds":{"left":0.17774999999999999,"top":-0.78125,"right":0.86524999999999996,"bottom":0.052083333333333336},"atlasBounds":{"left":294.5,"top":604.5,"right":327.5,"bottom":644.5}},{"unicode":65317,"advance":1,"planeBounds":{"left":0.23433333333333328,"top":-0.78125,"right":0.81766666666666665,"bottom":0.052083333333333336},"atlasBounds":{"left":230.5,"top":604.5,"right":258.5,"bottom":644.5}},{"unicode":65318,"advance":1,"planeBounds":{"left":0.24733333333333327,"top":-0.78125,"right":0.83066666666666666,"bottom":0.052083333333333336},"atlasBounds":{"left":156.5,"top":604.5,"right":184.5,"bottom":644.5}},{"unicode":65319,"advance":1,"planeBounds":{"left":0.12741666666666662,"top":-0.80208333333333326,"right":0.85658333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":29.5,"top":518.5,"right":64.5,"bottom":560.5}},{"unicode":65320,"advance":1,"planeBounds":{"left":0.15624999999999997,"top":-0.78125,"right":0.84375,"bottom":0.052083333333333336},"atlasBounds":{"left":38.5,"top":604.5,"right":71.5,"bottom":644.5}},{"unicode":65321,"advance":1,"planeBounds":{"left":0.19891666666666663,"top":-0.78125,"right":0.80308333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":671.5,"top":561.5,"right":700.5,"bottom":601.5}},{"unicode":65322,"advance":1,"planeBounds":{"left":0.19766666666666663,"top":-0.78124999999999989,"right":0.73933333333333329,"bottom":0.072916666666666657},"atlasBounds":{"left":117.5,"top":561.5,"right":143.5,"bottom":602.5}},{"unicode":65323,"advance":1,"planeBounds":{"left":0.1938333333333333,"top":-0.78125,"right":0.90216666666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":259.5,"top":604.5,"right":293.5,"bottom":644.5}},{"unicode":65324,"advance":1,"planeBounds":{"left":0.25774999999999992,"top":-0.78125,"right":0.82024999999999992,"bottom":0.052083333333333336},"atlasBounds":{"left":784.5,"top":561.5,"right":811.5,"bottom":601.5}},{"unicode":65325,"advance":1,"planeBounds":{"left":0.10416666666666662,"top":-0.78125,"right":0.89583333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":701.5,"top":561.5,"right":739.5,"bottom":601.5}},{"unicode":65326,"advance":1,"planeBounds":{"left":0.15724999999999997,"top":-0.78125,"right":0.84475,"bottom":0.052083333333333336},"atlasBounds":{"left":122.5,"top":604.5,"right":155.5,"bottom":644.5}},{"unicode":65327,"advance":1,"planeBounds":{"left":0.09375,"top":-0.80208333333333326,"right":0.90625,"bottom":0.072916666666666657},"atlasBounds":{"left":684.5,"top":518.5,"right":723.5,"bottom":560.5}},{"unicode":65328,"advance":1,"planeBounds":{"left":0.20258333333333328,"top":-0.78125,"right":0.8484166666666666,"bottom":0.052083333333333336},"atlasBounds":{"left":513.5,"top":604.5,"right":544.5,"bottom":644.5}},{"unicode":65329,"advance":1,"planeBounds":{"left":0.094333333333333297,"top":-0.80208333333333326,"right":0.92766666666666664,"bottom":0.23958333333333334},"atlasBounds":{"left":723.5,"top":0.5,"right":763.5,"bottom":50.5}},{"unicode":65330,"advance":1,"planeBounds":{"left":0.20516666666666666,"top":-0.78125,"right":0.87183333333333324,"bottom":0.052083333333333336},"atlasBounds":{"left":194.5,"top":645.5,"right":226.5,"bottom":685.5}},{"unicode":65331,"advance":1,"planeBounds":{"left":0.16766666666666663,"top":-0.80208333333333326,"right":0.83433333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":499.5,"top":518.5,"right":531.5,"bottom":560.5}},{"unicode":65332,"advance":1,"planeBounds":{"left":0.14583333333333331,"top":-0.78125,"right":0.85416666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":123.5,"top":645.5,"right":157.5,"bottom":685.5}},{"unicode":65333,"advance":1,"planeBounds":{"left":0.15875,"top":-0.78124999999999989,"right":0.84625000000000006,"bottom":0.072916666666666657},"atlasBounds":{"left":83.5,"top":561.5,"right":116.5,"bottom":602.5}},{"unicode":65334,"advance":1,"planeBounds":{"left":0.12549999999999997,"top":-0.78125,"right":0.87549999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":86.5,"top":645.5,"right":122.5,"bottom":685.5}},{"unicode":65335,"advance":1,"planeBounds":{"left":-0.021833333333333364,"top":-0.78125,"right":1.0198333333333334,"bottom":0.052083333333333336},"atlasBounds":{"left":35.5,"top":645.5,"right":85.5,"bottom":685.5}},{"unicode":65336,"advance":1,"planeBounds":{"left":0.14633333333333329,"top":-0.78125,"right":0.85466666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":645.5,"right":34.5,"bottom":685.5}},{"unicode":65337,"advance":1,"planeBounds":{"left":0.15624999999999994,"top":-0.78125,"right":0.84374999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":801.5,"top":604.5,"right":834.5,"bottom":644.5}},{"unicode":65338,"advance":1,"planeBounds":{"left":0.17116666666666666,"top":-0.78125,"right":0.83783333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":768.5,"top":604.5,"right":800.5,"bottom":644.5}},{"unicode":65339,"advance":1,"planeBounds":{"left":0.6619166666666666,"top":-0.90624999999999989,"right":1.0160833333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":764.5,"top":0.5,"right":781.5,"bottom":50.5}},{"unicode":65340,"advance":1,"planeBounds":{"left":-0.010416666666666696,"top":-0.90624999999999989,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":782.5,"top":0.5,"right":831.5,"bottom":50.5}},{"unicode":65341,"advance":1,"planeBounds":{"left":-0.016083333333333293,"top":-0.90624999999999989,"right":0.3380833333333334,"bottom":0.13541666666666669},"atlasBounds":{"left":832.5,"top":0.5,"right":849.5,"bottom":50.5}},{"unicode":65342,"advance":1,"planeBounds":{"left":0.30066666666666669,"top":-0.98958333333333326,"right":0.71733333333333327,"bottom":-0.76041666666666663},"atlasBounds":{"left":835.5,"top":547.5,"right":855.5,"bottom":558.5}},{"unicode":65343,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":0.03125,"right":1.0520833333333333,"bottom":0.17708333333333334},"atlasBounds":{"left":643.5,"top":797.5,"right":696.5,"bottom":804.5}},{"unicode":65344,"advance":1,"planeBounds":{"left":0.29325000000000001,"top":-0.98958333333333326,"right":0.60575000000000001,"bottom":-0.76041666666666663},"atlasBounds":{"left":498.5,"top":797.5,"right":513.5,"bottom":808.5}},{"unicode":65345,"advance":1,"planeBounds":{"left":0.20633333333333329,"top":-0.61458333333333326,"right":0.78966666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":585.5,"top":726.5,"right":613.5,"bottom":759.5}},{"unicode":65346,"advance":1,"planeBounds":{"left":0.20699999999999999,"top":-0.84374999999999989,"right":0.83200000000000007,"bottom":0.072916666666666657},"atlasBounds":{"left":120.5,"top":384.5,"right":150.5,"bottom":428.5}},{"unicode":65347,"advance":1,"planeBounds":{"left":0.20841666666666664,"top":-0.61458333333333326,"right":0.81258333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":378.5,"top":726.5,"right":407.5,"bottom":759.5}},{"unicode":65348,"advance":1,"planeBounds":{"left":0.16749999999999998,"top":-0.84374999999999989,"right":0.79249999999999998,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":384.5,"right":30.5,"bottom":428.5}},{"unicode":65349,"advance":1,"planeBounds":{"left":0.18058333333333329,"top":-0.61458333333333326,"right":0.82641666666666669,"bottom":0.072916666666666657},"atlasBounds":{"left":614.5,"top":726.5,"right":645.5,"bottom":759.5}},{"unicode":65350,"advance":1,"planeBounds":{"left":0.23299999999999998,"top":-0.86458333333333326,"right":0.85799999999999998,"bottom":0.052083333333333336},"atlasBounds":{"left":750.5,"top":338.5,"right":780.5,"bottom":382.5}},{"unicode":65351,"advance":1,"planeBounds":{"left":0.18366666666666664,"top":-0.61458333333333326,"right":0.85033333333333327,"bottom":0.30208333333333331},"atlasBounds":{"left":717.5,"top":338.5,"right":749.5,"bottom":382.5}},{"unicode":65352,"advance":1,"planeBounds":{"left":0.21333333333333329,"top":-0.84375,"right":0.79666666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":722.5,"top":429.5,"right":750.5,"bottom":472.5}},{"unicode":65353,"advance":1,"planeBounds":{"left":0.38541666666666663,"top":-0.84375,"right":0.61458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":689.5,"top":429.5,"right":700.5,"bottom":472.5}},{"unicode":65354,"advance":1,"planeBounds":{"left":0.18049999999999997,"top":-0.84375,"right":0.68049999999999999,"bottom":0.30208333333333331},"atlasBounds":{"left":79.5,"top":0.5,"right":103.5,"bottom":55.5}},{"unicode":65355,"advance":1,"planeBounds":{"left":0.23891666666666664,"top":-0.84375,"right":0.8430833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":659.5,"top":429.5,"right":688.5,"bottom":472.5}},{"unicode":65356,"advance":1,"planeBounds":{"left":0.39466666666666672,"top":-0.84374999999999989,"right":0.81133333333333324,"bottom":0.072916666666666657},"atlasBounds":{"left":617.5,"top":338.5,"right":637.5,"bottom":382.5}},{"unicode":65357,"advance":1,"planeBounds":{"left":0.03425000000000001,"top":-0.61458333333333326,"right":0.97175,"bottom":0.052083333333333336},"atlasBounds":{"left":81.5,"top":764.5,"right":126.5,"bottom":796.5}},{"unicode":65358,"advance":1,"planeBounds":{"left":0.21333333333333329,"top":-0.61458333333333326,"right":0.79666666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":756.5,"top":726.5,"right":784.5,"bottom":758.5}},{"unicode":65359,"advance":1,"planeBounds":{"left":0.16666666666666663,"top":-0.61458333333333326,"right":0.83333333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":684.5,"top":726.5,"right":716.5,"bottom":759.5}},{"unicode":65360,"advance":1,"planeBounds":{"left":0.20649999999999999,"top":-0.61458333333333326,"right":0.83150000000000002,"bottom":0.28125},"atlasBounds":{"left":617.5,"top":474.5,"right":647.5,"bottom":517.5}},{"unicode":65361,"advance":1,"planeBounds":{"left":0.16799999999999998,"top":-0.61458333333333326,"right":0.79300000000000004,"bottom":0.28125},"atlasBounds":{"left":586.5,"top":474.5,"right":616.5,"bottom":517.5}},{"unicode":65362,"advance":1,"planeBounds":{"left":0.27716666666666662,"top":-0.61458333333333326,"right":0.8188333333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":54.5,"top":764.5,"right":80.5,"bottom":796.5}},{"unicode":65363,"advance":1,"planeBounds":{"left":0.19641666666666666,"top":-0.61458333333333326,"right":0.80058333333333331,"bottom":0.072916666666666657},"atlasBounds":{"left":348.5,"top":726.5,"right":377.5,"bottom":759.5}},{"unicode":65364,"advance":1,"planeBounds":{"left":0.19549999999999998,"top":-0.73958333333333326,"right":0.82050000000000001,"bottom":0.072916666666666657},"atlasBounds":{"left":298.5,"top":645.5,"right":328.5,"bottom":684.5}},{"unicode":65365,"advance":1,"planeBounds":{"left":0.20433333333333328,"top":-0.59374999999999989,"right":0.78766666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":127.5,"top":764.5,"right":155.5,"bottom":796.5}},{"unicode":65366,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.59375,"right":0.82291666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":262.5,"top":764.5,"right":293.5,"bottom":795.5}},{"unicode":65367,"advance":1,"planeBounds":{"left":0.030249999999999971,"top":-0.59375,"right":0.96774999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":156.5,"top":764.5,"right":201.5,"bottom":795.5}},{"unicode":65368,"advance":1,"planeBounds":{"left":0.18749999999999997,"top":-0.59375,"right":0.8125,"bottom":0.052083333333333336},"atlasBounds":{"left":202.5,"top":764.5,"right":232.5,"bottom":795.5}},{"unicode":65369,"advance":1,"planeBounds":{"left":0.17708333333333329,"top":-0.59375,"right":0.82291666666666663,"bottom":0.28125},"atlasBounds":{"left":203.5,"top":518.5,"right":234.5,"bottom":560.5}},{"unicode":65370,"advance":1,"planeBounds":{"left":0.21033333333333329,"top":-0.59375,"right":0.79366666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":233.5,"top":764.5,"right":261.5,"bottom":795.5}},{"unicode":65371,"advance":1,"planeBounds":{"left":0.65691666666666659,"top":-0.90624999999999989,"right":1.0110833333333333,"bottom":0.15625},"atlasBounds":{"left":323.5,"top":0.5,"right":340.5,"bottom":51.5}},{"unicode":65372,"advance":1,"planeBounds":{"left":0.42708333333333331,"top":-0.92708333333333326,"right":0.57291666666666663,"bottom":0.17708333333333334},"atlasBounds":{"left":135.5,"top":0.5,"right":142.5,"bottom":53.5}},{"unicode":65373,"advance":1,"planeBounds":{"left":-0.011083333333333327,"top":-0.90624999999999989,"right":0.3430833333333333,"bottom":0.15625},"atlasBounds":{"left":341.5,"top":0.5,"right":358.5,"bottom":51.5}},{"unicode":65374,"advance":1,"planeBounds":{"left":-3.552713678800501e-17,"top":-0.57291666666666663,"right":0.99999999999999989,"bottom":-0.19791666666666666},"atlasBounds":{"left":367.5,"top":797.5,"right":415.5,"bottom":815.5}},{"unicode":65375,"advance":1,"planeBounds":{"left":0.52641666666666664,"top":-0.90624999999999989,"right":1.0055833333333333,"bottom":0.15625},"atlasBounds":{"left":359.5,"top":0.5,"right":382.5,"bottom":51.5}},{"unicode":65376,"advance":1,"planeBounds":{"left":-0.0055833333333333056,"top":-0.90624999999999989,"right":0.47358333333333336,"bottom":0.15625},"atlasBounds":{"left":383.5,"top":0.5,"right":406.5,"bottom":51.5}},{"unicode":65377,"advance":0.5,"planeBounds":{"left":-0.01141666666666666,"top":-0.30208333333333331,"right":0.38441666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":125.5,"top":797.5,"right":144.5,"bottom":817.5}},{"unicode":65378,"advance":0.5,"planeBounds":{"left":0.099666666666666723,"top":-0.90625,"right":0.51633333333333342,"bottom":-0.15625},"atlasBounds":{"left":224.5,"top":59.5,"right":244.5,"bottom":95.5}},{"unicode":65379,"advance":0.5,"planeBounds":{"left":-0.0163333333333333,"top":-0.61458333333333326,"right":0.40033333333333337,"bottom":0.13541666666666669},"atlasBounds":{"left":245.5,"top":59.5,"right":265.5,"bottom":95.5}},{"unicode":65380,"advance":0.5,"planeBounds":{"left":-0.017999999999999964,"top":-0.28125,"right":0.35699999999999998,"bottom":0.11458333333333333},"atlasBounds":{"left":245.5,"top":797.5,"right":263.5,"bottom":816.5}},{"unicode":65381,"advance":0.5,"planeBounds":{"left":0.11408333333333334,"top":-0.51041666666666663,"right":0.38491666666666668,"bottom":-0.23958333333333331},"atlasBounds":{"left":821.5,"top":547.5,"right":834.5,"bottom":560.5}},{"unicode":65382,"advance":0.5,"planeBounds":{"left":0.032333333333333367,"top":-0.76041666666666663,"right":0.4906666666666667,"bottom":0.09375},"atlasBounds":{"left":446.5,"top":561.5,"right":468.5,"bottom":602.5}},{"unicode":65383,"advance":0.5,"planeBounds":{"left":0.056166666666666684,"top":-0.61458333333333337,"right":0.47283333333333333,"bottom":0.09375},"atlasBounds":{"left":835.5,"top":604.5,"right":855.5,"bottom":638.5}},{"unicode":65384,"advance":0.5,"planeBounds":{"left":0.022583333333333344,"top":-0.65625,"right":0.41841666666666671,"bottom":0.09375},"atlasBounds":{"left":266.5,"top":59.5,"right":285.5,"bottom":95.5}},{"unicode":65385,"advance":0.5,"planeBounds":{"left":0.04766666666666669,"top":-0.65625,"right":0.46433333333333338,"bottom":0.09375},"atlasBounds":{"left":286.5,"top":59.5,"right":306.5,"bottom":95.5}},{"unicode":65386,"advance":0.5,"planeBounds":{"left":0.02675000000000002,"top":-0.55208333333333326,"right":0.46425,"bottom":0.052083333333333336},"atlasBounds":{"left":723.5,"top":764.5,"right":744.5,"bottom":793.5}},{"unicode":65387,"advance":0.5,"planeBounds":{"left":0.025750000000000023,"top":-0.65625,"right":0.46325,"bottom":0.09375},"atlasBounds":{"left":307.5,"top":59.5,"right":328.5,"bottom":95.5}},{"unicode":65388,"advance":0.5,"planeBounds":{"left":0.016333333333333363,"top":-0.65625,"right":0.47466666666666668,"bottom":0.09375},"atlasBounds":{"left":329.5,"top":59.5,"right":351.5,"bottom":95.5}},{"unicode":65389,"advance":0.5,"planeBounds":{"left":0.030750000000000031,"top":-0.55208333333333326,"right":0.46825,"bottom":0.052083333333333336},"atlasBounds":{"left":551.5,"top":764.5,"right":572.5,"bottom":793.5}},{"unicode":65390,"advance":0.5,"planeBounds":{"left":0.060500000000000026,"top":-0.57291666666666663,"right":0.4355,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":764.5,"right":18.5,"bottom":796.5}},{"unicode":65391,"advance":0.5,"planeBounds":{"left":0.026250000000000013,"top":-0.59375,"right":0.46375,"bottom":0.11458333333333333},"atlasBounds":{"left":209.5,"top":726.5,"right":230.5,"bottom":760.5}},{"unicode":65392,"advance":0.5,"planeBounds":{"left":0.0045000000000000127,"top":-0.46875,"right":0.50450000000000006,"bottom":-0.26041666666666669},"atlasBounds":{"left":821.5,"top":674.5,"right":845.5,"bottom":684.5}},{"unicode":65393,"advance":0.5,"planeBounds":{"left":0.01649999999999998,"top":-0.76041666666666663,"right":0.51649999999999996,"bottom":0.09375},"atlasBounds":{"left":646.5,"top":561.5,"right":670.5,"bottom":602.5}},{"unicode":65394,"advance":0.5,"planeBounds":{"left":-0.016999999999999967,"top":-0.82291666666666663,"right":0.48299999999999998,"bottom":0.09375},"atlasBounds":{"left":563.5,"top":338.5,"right":587.5,"bottom":382.5}},{"unicode":65395,"advance":0.5,"planeBounds":{"left":0.016416666666666694,"top":-0.84375,"right":0.49558333333333332,"bottom":0.09375},"atlasBounds":{"left":129.5,"top":292.5,"right":152.5,"bottom":337.5}},{"unicode":65396,"advance":0.5,"planeBounds":{"left":-0.010916666666666642,"top":-0.71874999999999989,"right":0.50991666666666668,"bottom":0.010416666666666666},"atlasBounds":{"left":669.5,"top":59.5,"right":694.5,"bottom":94.5}},{"unicode":65397,"advance":0.5,"planeBounds":{"left":0.0050833333333333199,"top":-0.82291666666666663,"right":0.52591666666666659,"bottom":0.072916666666666657},"atlasBounds":{"left":751.5,"top":429.5,"right":776.5,"bottom":472.5}},{"unicode":65398,"advance":0.5,"planeBounds":{"left":-0.015499999999999984,"top":-0.82291666666666663,"right":0.48449999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":561.5,"top":474.5,"right":585.5,"bottom":517.5}},{"unicode":65399,"advance":0.5,"planeBounds":{"left":0.012916666666666677,"top":-0.82291666666666663,"right":0.49208333333333332,"bottom":0.09375},"atlasBounds":{"left":268.5,"top":429.5,"right":291.5,"bottom":473.5}},{"unicode":65400,"advance":0.5,"planeBounds":{"left":0.01091666666666671,"top":-0.84375,"right":0.49008333333333337,"bottom":0.09375},"atlasBounds":{"left":229.5,"top":292.5,"right":252.5,"bottom":337.5}},{"unicode":65401,"advance":0.5,"planeBounds":{"left":-0.019916666666666645,"top":-0.82291666666666663,"right":0.50091666666666668,"bottom":0.09375},"atlasBounds":{"left":464.5,"top":429.5,"right":489.5,"bottom":473.5}},{"unicode":65402,"advance":0.5,"planeBounds":{"left":0.030750000000000031,"top":-0.71875,"right":0.46825,"bottom":0.052083333333333336},"atlasBounds":{"left":787.5,"top":686.5,"right":808.5,"bottom":723.5}},{"unicode":65403,"advance":0.5,"planeBounds":{"left":-0.012916666666666646,"top":-0.80208333333333337,"right":0.50791666666666668,"bottom":0.09375},"atlasBounds":{"left":106.5,"top":474.5,"right":131.5,"bottom":517.5}},{"unicode":65404,"advance":0.5,"planeBounds":{"left":-0.0044166666666666269,"top":-0.82291666666666663,"right":0.51641666666666675,"bottom":0.072916666666666657},"atlasBounds":{"left":505.5,"top":474.5,"right":530.5,"bottom":517.5}},{"unicode":65405,"advance":0.5,"planeBounds":{"left":-0.005416666666666627,"top":-0.76041666666666663,"right":0.51541666666666675,"bottom":0.072916666666666657},"atlasBounds":{"left":590.5,"top":604.5,"right":615.5,"bottom":644.5}},{"unicode":65406,"advance":0.5,"planeBounds":{"left":-0.026416666666666665,"top":-0.82291666666666663,"right":0.49441666666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":658.5,"top":518.5,"right":683.5,"bottom":560.5}},{"unicode":65407,"advance":0.5,"planeBounds":{"left":0.010916666666666677,"top":-0.76041666666666663,"right":0.49008333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":711.5,"top":604.5,"right":734.5,"bottom":644.5}},{"unicode":65408,"advance":0.5,"planeBounds":{"left":-0.012999999999999966,"top":-0.86458333333333326,"right":0.48699999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":743.5,"top":195.5,"right":767.5,"bottom":242.5}},{"unicode":65409,"advance":0.5,"planeBounds":{"left":0.0010000000000000391,"top":-0.82291666666666663,"right":0.501,"bottom":0.09375},"atlasBounds":{"left":757.5,"top":384.5,"right":781.5,"bottom":428.5}},{"unicode":65410,"advance":0.5,"planeBounds":{"left":-0.016416666666666621,"top":-0.78125,"right":0.50441666666666674,"bottom":0.09375},"atlasBounds":{"left":830.5,"top":474.5,"right":855.5,"bottom":516.5}},{"unicode":65411,"advance":0.5,"planeBounds":{"left":0.0010000000000000356,"top":-0.78125,"right":0.501,"bottom":0.09375},"atlasBounds":{"left":724.5,"top":518.5,"right":748.5,"bottom":560.5}},{"unicode":65412,"advance":0.5,"planeBounds":{"left":0.095500000000000029,"top":-0.80208333333333326,"right":0.47050000000000003,"bottom":0.072916666666666657},"atlasBounds":{"left":595.5,"top":518.5,"right":613.5,"bottom":560.5}},{"unicode":65413,"advance":0.5,"planeBounds":{"left":0.0020000000000000391,"top":-0.82291666666666663,"right":0.502,"bottom":0.09375},"atlasBounds":{"left":604.5,"top":384.5,"right":628.5,"bottom":428.5}},{"unicode":65414,"advance":0.5,"planeBounds":{"left":0.0010000000000000338,"top":-0.69791666666666663,"right":0.501,"bottom":-0.010416666666666666},"atlasBounds":{"left":534.5,"top":726.5,"right":558.5,"bottom":759.5}},{"unicode":65415,"advance":0.5,"planeBounds":{"left":0.0053333333333333661,"top":-0.76041666666666663,"right":0.46366666666666667,"bottom":0.09375},"atlasBounds":{"left":469.5,"top":561.5,"right":491.5,"bottom":602.5}},{"unicode":65416,"advance":0.5,"planeBounds":{"left":-0.0034166666666666946,"top":-0.82291666666666663,"right":0.51741666666666664,"bottom":0.09375},"atlasBounds":{"left":151.5,"top":384.5,"right":176.5,"bottom":428.5}},{"unicode":65417,"advance":0.5,"planeBounds":{"left":0.014250000000000009,"top":-0.78124999999999989,"right":0.45174999999999998,"bottom":0.072916666666666657},"atlasBounds":{"left":182.5,"top":561.5,"right":203.5,"bottom":602.5}},{"unicode":65418,"advance":0.5,"planeBounds":{"left":-0.037750000000000034,"top":-0.73958333333333326,"right":0.52474999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":545.5,"top":686.5,"right":572.5,"bottom":724.5}},{"unicode":65419,"advance":0.5,"planeBounds":{"left":0.046166666666666682,"top":-0.80208333333333326,"right":0.46283333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":182.5,"top":518.5,"right":202.5,"bottom":560.5}},{"unicode":65420,"advance":0.5,"planeBounds":{"left":0.031833333333333352,"top":-0.71875,"right":0.49016666666666669,"bottom":0.09375},"atlasBounds":{"left":682.5,"top":645.5,"right":704.5,"bottom":684.5}},{"unicode":65421,"advance":0.5,"planeBounds":{"left":-0.034750000000000038,"top":-0.69791666666666663,"right":0.52774999999999994,"bottom":0.03125},"atlasBounds":{"left":695.5,"top":59.5,"right":722.5,"bottom":94.5}},{"unicode":65422,"advance":0.5,"planeBounds":{"left":-0.020333333333333301,"top":-0.80208333333333326,"right":0.52133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":829.5,"top":384.5,"right":855.5,"bottom":426.5}},{"unicode":65423,"advance":0.5,"planeBounds":{"left":-0.00091666666666671403,"top":-0.71875,"right":0.51991666666666669,"bottom":0.09375},"atlasBounds":{"left":830.5,"top":244.5,"right":855.5,"bottom":283.5}},{"unicode":65424,"advance":0.5,"planeBounds":{"left":0.037166666666666688,"top":-0.80208333333333337,"right":0.45383333333333331,"bottom":0.09375},"atlasBounds":{"left":701.5,"top":429.5,"right":721.5,"bottom":472.5}},{"unicode":65425,"advance":0.5,"planeBounds":{"left":-0.021333333333333371,"top":-0.80208333333333337,"right":0.52033333333333331,"bottom":0.09375},"atlasBounds":{"left":602.5,"top":429.5,"right":628.5,"bottom":472.5}},{"unicode":65426,"advance":0.5,"planeBounds":{"left":-8.3333333333289515e-05,"top":-0.80208333333333337,"right":0.47908333333333336,"bottom":0.09375},"atlasBounds":{"left":578.5,"top":429.5,"right":601.5,"bottom":472.5}},{"unicode":65427,"advance":0.5,"planeBounds":{"left":3.7303493627405262e-17,"top":-0.73958333333333326,"right":0.5,"bottom":0.072916666666666657},"atlasBounds":{"left":425.5,"top":645.5,"right":449.5,"bottom":684.5}},{"unicode":65428,"advance":0.5,"planeBounds":{"left":-0.023833333333333321,"top":-0.82291666666666663,"right":0.51783333333333326,"bottom":0.09375},"atlasBounds":{"left":536.5,"top":338.5,"right":562.5,"bottom":382.5}},{"unicode":65429,"advance":0.5,"planeBounds":{"left":-0.010416666666666656,"top":-0.71874999999999989,"right":0.51041666666666674,"bottom":0.010416666666666666},"atlasBounds":{"left":723.5,"top":59.5,"right":748.5,"bottom":94.5}},{"unicode":65430,"advance":0.5,"planeBounds":{"left":0.035750000000000032,"top":-0.73958333333333326,"right":0.47325,"bottom":0.052083333333333336},"atlasBounds":{"left":119.5,"top":686.5,"right":140.5,"bottom":724.5}},{"unicode":65431,"advance":0.5,"planeBounds":{"left":0.024333333333333373,"top":-0.78125,"right":0.48266666666666669,"bottom":0.09375},"atlasBounds":{"left":721.5,"top":474.5,"right":743.5,"bottom":516.5}},{"unicode":65432,"advance":0.5,"planeBounds":{"left":0.046083333333333351,"top":-0.80208333333333337,"right":0.44191666666666668,"bottom":0.09375},"atlasBounds":{"left":266.5,"top":474.5,"right":285.5,"bottom":517.5}},{"unicode":65433,"advance":0.5,"planeBounds":{"left":-0.031250000000000049,"top":-0.78124999999999989,"right":0.53124999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":618.5,"top":561.5,"right":645.5,"bottom":602.5}},{"unicode":65434,"advance":0.5,"planeBounds":{"left":0.042333333333333362,"top":-0.78124999999999989,"right":0.5006666666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":244.5,"top":561.5,"right":266.5,"bottom":602.5}},{"unicode":65435,"advance":0.5,"planeBounds":{"left":0.023833333333333349,"top":-0.73958333333333326,"right":0.48216666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":573.5,"top":686.5,"right":595.5,"bottom":724.5}},{"unicode":65436,"advance":0.5,"planeBounds":{"left":0.018416666666666685,"top":-0.73958333333333337,"right":0.49758333333333332,"bottom":0.09375},"atlasBounds":{"left":616.5,"top":604.5,"right":639.5,"bottom":644.5}},{"unicode":65437,"advance":0.5,"planeBounds":{"left":0.017916666666666674,"top":-0.78124999999999989,"right":0.49708333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":594.5,"top":561.5,"right":617.5,"bottom":602.5}},{"unicode":65438,"advance":0.5,"planeBounds":{"left":-0.02566666666666665,"top":-0.90625,"right":0.3076666666666667,"bottom":-0.57291666666666663},"atlasBounds":{"left":839.5,"top":96.5,"right":855.5,"bottom":112.5}},{"unicode":65439,"advance":0.5,"planeBounds":{"left":-0.022333333333333309,"top":-0.90625,"right":0.26933333333333337,"bottom":-0.55208333333333326},"atlasBounds":{"left":817.5,"top":225.5,"right":831.5,"bottom":242.5}},{"unicode":65441,"advance":0.92000000000000004,"planeBounds":{"left":0.13708333333333328,"top":-0.65625,"right":0.78291666666666659,"bottom":-0.09375},"atlasBounds":{"left":617.5,"top":827.5,"right":648.5,"bottom":854.5}},{"unicode":65442,"advance":0.92000000000000004,"planeBounds":{"left":0.089583333333333362,"top":-0.65625,"right":0.86041666666666672,"bottom":-0.09375},"atlasBounds":{"left":579.5,"top":827.5,"right":616.5,"bottom":854.5}},{"unicode":65443,"advance":0.92000000000000004,"planeBounds":{"left":0.090666666666666687,"top":-0.67708333333333326,"right":0.8823333333333333,"bottom":-0.072916666666666671},"atlasBounds":{"left":817.5,"top":195.5,"right":855.5,"bottom":224.5}},{"unicode":65444,"advance":0.92000000000000004,"planeBounds":{"left":0.13716666666666666,"top":-0.65625,"right":0.80383333333333329,"bottom":-0.09375},"atlasBounds":{"left":546.5,"top":827.5,"right":578.5,"bottom":854.5}},{"unicode":65445,"advance":0.92000000000000004,"planeBounds":{"left":0.10749999999999998,"top":-0.65625,"right":0.85750000000000004,"bottom":-0.072916666666666671},"atlasBounds":{"left":141.5,"top":827.5,"right":177.5,"bottom":855.5}},{"unicode":65446,"advance":0.92000000000000004,"planeBounds":{"left":0.11383333333333333,"top":-0.67708333333333326,"right":0.8221666666666666,"bottom":-0.052083333333333329},"atlasBounds":{"left":294.5,"top":764.5,"right":328.5,"bottom":794.5}},{"unicode":65447,"advance":0.92000000000000004,"planeBounds":{"left":0.14358333333333331,"top":-0.65625,"right":0.78941666666666666,"bottom":-0.09375},"atlasBounds":{"left":328.5,"top":827.5,"right":359.5,"bottom":854.5}},{"unicode":65448,"advance":0.92000000000000004,"planeBounds":{"left":0.1103333333333333,"top":-0.65625,"right":0.81866666666666665,"bottom":-0.09375},"atlasBounds":{"left":466.5,"top":827.5,"right":500.5,"bottom":854.5}},{"unicode":65449,"advance":0.92000000000000004,"planeBounds":{"left":0.13366666666666663,"top":-0.67708333333333326,"right":0.80033333333333323,"bottom":-0.072916666666666671},"atlasBounds":{"left":781.5,"top":764.5,"right":813.5,"bottom":793.5}},{"unicode":65450,"advance":0.92000000000000004,"planeBounds":{"left":0.098916666666666639,"top":-0.67708333333333337,"right":0.82808333333333328,"bottom":-0.09375},"atlasBounds":{"left":292.5,"top":827.5,"right":327.5,"bottom":855.5}},{"unicode":65451,"advance":0.92000000000000004,"planeBounds":{"left":0.10391666666666662,"top":-0.67708333333333337,"right":0.83308333333333329,"bottom":-0.09375},"atlasBounds":{"left":256.5,"top":827.5,"right":291.5,"bottom":855.5}},{"unicode":65452,"advance":0.92000000000000004,"planeBounds":{"left":0.094499999999999987,"top":-0.67708333333333326,"right":0.84450000000000003,"bottom":-0.072916666666666671},"atlasBounds":{"left":651.5,"top":764.5,"right":687.5,"bottom":793.5}},{"unicode":65453,"advance":0.92000000000000004,"planeBounds":{"left":0.098166666666666666,"top":-0.67708333333333326,"right":0.88983333333333325,"bottom":-0.072916666666666671},"atlasBounds":{"left":612.5,"top":764.5,"right":650.5,"bottom":793.5}},{"unicode":65454,"advance":0.92000000000000004,"planeBounds":{"left":0.098499999999999963,"top":-0.67708333333333337,"right":0.84849999999999992,"bottom":-0.09375},"atlasBounds":{"left":104.5,"top":827.5,"right":140.5,"bottom":855.5}},{"unicode":65455,"advance":0.92000000000000004,"planeBounds":{"left":0.10249999999999997,"top":-0.67708333333333337,"right":0.85249999999999992,"bottom":-0.09375},"atlasBounds":{"left":67.5,"top":827.5,"right":103.5,"bottom":855.5}},{"unicode":65456,"advance":0.92000000000000004,"planeBounds":{"left":0.10100000000000002,"top":-0.69791666666666663,"right":0.85099999999999998,"bottom":-0.072916666666666671},"atlasBounds":{"left":819.5,"top":726.5,"right":855.5,"bottom":756.5}},{"unicode":65457,"advance":0.92000000000000004,"planeBounds":{"left":0.13116666666666665,"top":-0.65625,"right":0.79783333333333328,"bottom":-0.09375},"atlasBounds":{"left":360.5,"top":827.5,"right":392.5,"bottom":854.5}},{"unicode":65458,"advance":0.92000000000000004,"planeBounds":{"left":0.14108333333333328,"top":-0.67708333333333326,"right":0.7869166666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":519.5,"top":764.5,"right":550.5,"bottom":793.5}},{"unicode":65459,"advance":0.92000000000000004,"planeBounds":{"left":0.1153333333333333,"top":-0.67708333333333337,"right":0.82366666666666666,"bottom":-0.09375},"atlasBounds":{"left":821.5,"top":518.5,"right":855.5,"bottom":546.5}},{"unicode":65460,"advance":0.92000000000000004,"planeBounds":{"left":0.11016666666666662,"top":-0.67708333333333326,"right":0.90183333333333326,"bottom":-0.072916666666666671},"atlasBounds":{"left":480.5,"top":764.5,"right":518.5,"bottom":793.5}},{"unicode":65461,"advance":0.92000000000000004,"planeBounds":{"left":0.10041666666666667,"top":-0.67708333333333326,"right":0.82958333333333323,"bottom":-0.072916666666666671},"atlasBounds":{"left":745.5,"top":764.5,"right":780.5,"bottom":793.5}},{"unicode":65462,"advance":0.92000000000000004,"planeBounds":{"left":0.051333333333333328,"top":-0.65625,"right":0.8846666666666666,"bottom":-0.09375},"atlasBounds":{"left":649.5,"top":827.5,"right":689.5,"bottom":854.5}},{"unicode":65463,"advance":0.92000000000000004,"planeBounds":{"left":0.12716666666666665,"top":-0.67708333333333326,"right":0.79383333333333328,"bottom":-0.072916666666666671},"atlasBounds":{"left":814.5,"top":764.5,"right":846.5,"bottom":793.5}},{"unicode":65464,"advance":0.92000000000000004,"planeBounds":{"left":0.11333333333333334,"top":-0.67708333333333326,"right":0.82166666666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":688.5,"top":764.5,"right":722.5,"bottom":793.5}},{"unicode":65465,"advance":0.92000000000000004,"planeBounds":{"left":0.048833333333333312,"top":-0.67708333333333326,"right":0.88216666666666665,"bottom":-0.072916666666666671},"atlasBounds":{"left":439.5,"top":764.5,"right":479.5,"bottom":793.5}},{"unicode":65466,"advance":0.92000000000000004,"planeBounds":{"left":0.10733333333333331,"top":-0.71874999999999989,"right":0.81566666666666665,"bottom":-0.052083333333333329},"atlasBounds":{"left":19.5,"top":764.5,"right":53.5,"bottom":796.5}},{"unicode":65467,"advance":0.92000000000000004,"planeBounds":{"left":0.12516666666666665,"top":-0.67708333333333326,"right":0.79183333333333328,"bottom":-0.072916666666666671},"atlasBounds":{"left":0.5,"top":797.5,"right":32.5,"bottom":826.5}},{"unicode":65468,"advance":0.92000000000000004,"planeBounds":{"left":0.13616666666666666,"top":-0.67708333333333337,"right":0.80283333333333329,"bottom":-0.09375},"atlasBounds":{"left":178.5,"top":827.5,"right":210.5,"bottom":855.5}},{"unicode":65469,"advance":0.92000000000000004,"planeBounds":{"left":0.11083333333333328,"top":-0.65625,"right":0.8191666666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":821.5,"top":645.5,"right":855.5,"bottom":673.5}},{"unicode":65470,"advance":0.92000000000000004,"planeBounds":{"left":0.12124999999999998,"top":-0.71875,"right":0.80874999999999997,"bottom":-0.03125},"atlasBounds":{"left":500.5,"top":726.5,"right":533.5,"bottom":759.5}},{"unicode":65474,"advance":0.92000000000000004,"planeBounds":{"left":0.34008333333333335,"top":-0.86458333333333326,"right":0.73591666666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":562.5,"top":96.5,"right":581.5,"bottom":144.5}},{"unicode":65475,"advance":0.92000000000000004,"planeBounds":{"left":0.25916666666666671,"top":-0.86458333333333326,"right":0.67583333333333329,"bottom":0.13541666666666669},"atlasBounds":{"left":541.5,"top":96.5,"right":561.5,"bottom":144.5}},{"unicode":65476,"advance":0.92000000000000004,"planeBounds":{"left":0.32000000000000001,"top":-0.86458333333333326,"right":0.69500000000000006,"bottom":0.13541666666666669},"atlasBounds":{"left":151.5,"top":195.5,"right":169.5,"bottom":243.5}},{"unicode":65477,"advance":0.92000000000000004,"planeBounds":{"left":0.26808333333333334,"top":-0.86458333333333326,"right":0.6639166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":195.5,"right":19.5,"bottom":243.5}},{"unicode":65478,"advance":0.92000000000000004,"planeBounds":{"left":0.24658333333333329,"top":-0.86458333333333326,"right":0.64241666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":92.5,"top":195.5,"right":111.5,"bottom":243.5}},{"unicode":65479,"advance":0.92000000000000004,"planeBounds":{"left":0.18408333333333332,"top":-0.86458333333333326,"right":0.70491666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":20.5,"top":195.5,"right":45.5,"bottom":243.5}},{"unicode":65482,"advance":0.92000000000000004,"planeBounds":{"left":0.24808333333333329,"top":-0.86458333333333326,"right":0.64391666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":627.5,"top":96.5,"right":646.5,"bottom":144.5}},{"unicode":65483,"advance":0.92000000000000004,"planeBounds":{"left":0.18058333333333329,"top":-0.86458333333333326,"right":0.70141666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":149.5,"top":146.5,"right":174.5,"bottom":194.5}},{"unicode":65484,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.69791666666666663,"right":0.91733333333333322,"bottom":-0.13541666666666666},"atlasBounds":{"left":421.5,"top":827.5,"right":465.5,"bottom":854.5}},{"unicode":65485,"advance":0.92000000000000004,"planeBounds":{"left":0.0081666666666666051,"top":-0.86458333333333326,"right":0.92483333333333317,"bottom":0.13541666666666669},"atlasBounds":{"left":582.5,"top":96.5,"right":626.5,"bottom":144.5}},{"unicode":65486,"advance":0.92000000000000004,"planeBounds":{"left":0.013916666666666635,"top":-0.86458333333333326,"right":0.86808333333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":224.5,"top":146.5,"right":265.5,"bottom":194.5}},{"unicode":65487,"advance":0.92000000000000004,"planeBounds":{"left":0.048666666666666622,"top":-0.86458333333333326,"right":0.84033333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":112.5,"top":195.5,"right":150.5,"bottom":243.5}},{"unicode":65490,"advance":0.92000000000000004,"planeBounds":{"left":0.007166666666666606,"top":-0.69791666666666663,"right":0.92383333333333317,"bottom":-0.11458333333333334},"atlasBounds":{"left":211.5,"top":827.5,"right":255.5,"bottom":855.5}},{"unicode":65491,"advance":0.92000000000000004,"planeBounds":{"left":0.0066666666666666237,"top":-0.57291666666666663,"right":0.92333333333333323,"bottom":-0.010416666666666666},"atlasBounds":{"left":501.5,"top":827.5,"right":545.5,"bottom":854.5}},{"unicode":65492,"advance":0.92000000000000004,"planeBounds":{"left":0.032833333333333277,"top":-0.86458333333333326,"right":0.86616666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":764.5,"top":146.5,"right":804.5,"bottom":194.5}},{"unicode":65493,"advance":0.92000000000000004,"planeBounds":{"left":0.026416666666666658,"top":-0.86458333333333326,"right":0.88058333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":722.5,"top":146.5,"right":763.5,"bottom":194.5}},{"unicode":65494,"advance":0.92000000000000004,"planeBounds":{"left":0.049749999999999996,"top":-0.86458333333333326,"right":0.86225000000000007,"bottom":0.13541666666666669},"atlasBounds":{"left":682.5,"top":146.5,"right":721.5,"bottom":194.5}},{"unicode":65495,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.57291666666666663,"right":0.91733333333333322,"bottom":-0.010416666666666666},"atlasBounds":{"left":718.5,"top":827.5,"right":762.5,"bottom":854.5}},{"unicode":65498,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.46875,"right":0.91733333333333322,"bottom":-0.30208333333333337},"atlasBounds":{"left":598.5,"top":797.5,"right":642.5,"bottom":805.5}},{"unicode":65499,"advance":0.92000000000000004,"planeBounds":{"left":0.032249999999999973,"top":-0.84374999999999989,"right":0.84474999999999989,"bottom":0.13541666666666669},"atlasBounds":{"left":656.5,"top":195.5,"right":695.5,"bottom":242.5}},{"unicode":65500,"advance":0.92000000000000004,"planeBounds":{"left":0.38116666666666671,"top":-0.86458333333333326,"right":0.54783333333333328,"bottom":0.13541666666666669},"atlasBounds":{"left":140.5,"top":146.5,"right":148.5,"bottom":194.5}},{"unicode":65504,"advance":1,"planeBounds":{"left":0.19149999999999998,"top":-0.82291666666666663,"right":0.8165,"bottom":0.09375},"atlasBounds":{"left":686.5,"top":338.5,"right":716.5,"bottom":382.5}},{"unicode":65505,"advance":1,"planeBounds":{"left":0.18308333333333332,"top":-0.78125,"right":0.82891666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":328.5,"top":604.5,"right":359.5,"bottom":644.5}},{"unicode":65506,"advance":1,"planeBounds":{"left":0.072833333333333292,"top":-0.57291666666666663,"right":0.90616666666666668,"bottom":-0.15625},"atlasBounds":{"left":145.5,"top":797.5,"right":185.5,"bottom":817.5}},{"unicode":65507,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":-0.92708333333333337,"right":1.0520833333333333,"bottom":-0.78125},"atlasBounds":{"left":697.5,"top":797.5,"right":750.5,"bottom":804.5}},{"unicode":65508,"advance":1,"planeBounds":{"left":0.42708333333333331,"top":-0.88541666666666663,"right":0.57291666666666663,"bottom":0.11458333333333333},"atlasBounds":{"left":170.5,"top":195.5,"right":177.5,"bottom":243.5}},{"unicode":65509,"advance":1,"planeBounds":{"left":0.13491666666666663,"top":-0.78125,"right":0.86408333333333331,"bottom":0.052083333333333336},"atlasBounds":{"left":158.5,"top":645.5,"right":193.5,"bottom":685.5}},{"unicode":65510,"advance":1,"planeBounds":{"left":0.041666666666666664,"top":-0.78125,"right":0.95833333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":185.5,"top":604.5,"right":229.5,"bottom":644.5}},{"unicode":65512,"advance":0.5,"planeBounds":{"left":0.16666666666666669,"top":-0.88541666666666674,"right":0.33333333333333337,"bottom":0.32291666666666663},"atlasBounds":{"left":70.5,"top":0.5,"right":78.5,"bottom":58.5}},{"unicode":65513,"advance":0.5,"planeBounds":{"left":-0.036250000000000018,"top":-0.65625,"right":0.52625,"bottom":-0.09375},"atlasBounds":{"left":690.5,"top":827.5,"right":717.5,"bottom":854.5}},{"unicode":65514,"advance":0.5,"planeBounds":{"left":0.02083333333333335,"top":-0.90624999999999989,"right":0.47916666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":505.5,"top":0.5,"right":527.5,"bottom":50.5}},{"unicode":65515,"advance":0.5,"planeBounds":{"left":-0.026250000000000016,"top":-0.65625,"right":0.53625,"bottom":-0.09375},"atlasBounds":{"left":393.5,"top":827.5,"right":420.5,"bottom":854.5}},{"unicode":65516,"advance":0.5,"planeBounds":{"left":0.02083333333333335,"top":-0.90624999999999989,"right":0.47916666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":650.5,"top":0.5,"right":672.5,"bottom":50.5}},{"unicode":65517,"advance":0.5,"planeBounds":{"left":0.020833333333333343,"top":-0.61458333333333337,"right":0.47916666666666669,"bottom":-0.15625},"atlasBounds":{"left":33.5,"top":797.5,"right":55.5,"bottom":819.5}},{"unicode":65518,"advance":0.5,"planeBounds":{"left":0.020333333333333373,"top":-0.61458333333333326,"right":0.47866666666666668,"bottom":-0.13541666666666666},"atlasBounds":{"left":820.5,"top":827.5,"right":842.5,"bottom":850.5}}],"kerning":[]} diff --git a/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png new file mode 100644 index 0000000..d865662 Binary files /dev/null and b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png differ diff --git a/tests/Resources/Typefaces/WorkSans/WorkSans-Italic.json b/tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.json similarity index 100% rename from tests/Resources/Typefaces/WorkSans/WorkSans-Italic.json rename to tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.json diff --git a/tests/Resources/Typefaces/WorkSans/WorkSans-Italic.png b/tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.png similarity index 100% rename from tests/Resources/Typefaces/WorkSans/WorkSans-Italic.png rename to tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.png diff --git a/tests/SampleGameBase.cs b/tests/SampleGameBase.cs index b3f1606..f23a9d6 100644 --- a/tests/SampleGameBase.cs +++ b/tests/SampleGameBase.cs @@ -1,3 +1,4 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.IO.Stores; @@ -7,11 +8,28 @@ namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests; public abstract partial class SampleGameBase : Game { private DependencyContainer? dependencies; + private MsdfFontStore? msdfFonts; [BackgroundDependencyLoader] private void load() { Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(SampleGameBase).Assembly), "Resources")); + MsdfTextRendering.CreateCapabilities(Resources); + + msdfFonts = new MsdfFontStore(Host.Renderer, Shaders); + + foreach (var weight in new[] { "Thin", "ExtraLight", "Light", "Regular", "Medium", "SemiBold", "Bold", "Black" }) + { + AddFont(Resources, $"Fonts/WorkSans/WorkSans-{weight}"); + AddFont(Resources, $"Fonts/WorkSans/WorkSans-{weight}Italic"); + + msdfFonts.AddFont(Resources, $"WorkSans", weight); + msdfFonts.AddFont(Resources, $"WorkSans", $"{weight}Italic"); + } + + msdfFonts.AddFont(Resources, $"NotoSansJP", "Regular"); + + dependencies?.CacheAs(msdfFonts); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) diff --git a/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs b/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs new file mode 100644 index 0000000..f7db8ba --- /dev/null +++ b/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs @@ -0,0 +1,108 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual; +using NUnit.Framework; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Sprites; + +[HeadlessTest] +public partial class TestSceneMsdfSpriteTextPresence : SampleTestScene +{ + [Test] + public void TestNormalSpriteText() + { + Container container = null!; + MsdfSpriteText text = null!; + + AddStep("reset", () => + { + Child = container = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red.Opacity(0.3f) + }, + text = new MsdfSpriteText + { + Text = "Hello world!", + Font = new FontUsage("WorkSans", 12, "Regular"), + } + ] + }; + }); + + AddAssert("is present", () => text.IsPresent); + AddAssert("height == 12", () => Precision.AlmostEquals(12, container.Height)); + AddStep("empty text", () => text.Text = string.Empty); + AddAssert("not present", () => !text.IsPresent); + AddAssert("height == 0", () => Precision.AlmostEquals(0, container.Height)); + } + + [Test] + public void TestAlwaysPresentSpriteText() + { + Container container = null!; + MsdfSpriteText text = null!; + + AddStep("reset", () => + { + Child = container = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red.Opacity(0.3f) + }, + text = new AlwaysPresentSpriteText + { + Text = "Hello world!", + Font = new FontUsage(size: 12), + } + ] + }; + }); + + AddAssert("is present", () => text.IsPresent); + AddAssert("height == 12", () => Precision.AlmostEquals(12, container.Height)); + AddStep("empty text", () => text.Text = string.Empty); + AddAssert("is present", () => text.IsPresent); + AddAssert("height == 0", () => Precision.AlmostEquals(0, container.Height)); + } + + [Test] + public void TestPresenceRemainsTheSameDuringFlow() + { + AddStep("reset", () => + { + Child = new FillFlowContainer + { + Child = new MsdfSpriteText() + }; + }); + + AddWaitStep("wait for some update frames", 2); + } + + private partial class AlwaysPresentSpriteText : MsdfSpriteText + { + public override bool IsPresent => true; + } +} diff --git a/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs b/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs new file mode 100644 index 0000000..8f015b3 --- /dev/null +++ b/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs @@ -0,0 +1,215 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers; +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; +using osuTK; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Containers; + +public partial class TestSceneMsdfTextFlowContainer : MsdfComparisonTestScene +{ + private const float demo_font_size = 20.0f; + private const float flow_width = 380.0f; + + private const string multi_paragraph_text = + "The quick brown fox jumps over the lazy dog. A swift movement across the tranquil meadow.\n\n" + + "Second paragraph with more descriptive text to demonstrate multi-line wrapping and paragraph spacing.\n" + + "This line follows a single newline break."; + + private const string cjk_and_urls_text = + "Visit https://osu.ppy.sh/home or browse \\local\\storage\\path. " + + "We also support hyphen-based and anti-aliased text splitting. " + + "CJK support: こんにちは世界!(Hello World) and 日本語の文章です。"; + + private static readonly FontUsage default_fu = FontUsage.Default.With(family: "WorkSans", size: demo_font_size, weight: "Regular"); + + private TextFlowContainer leftFlow = null!; + private MsdfTextFlowContainer rightFlow = null!; + + protected override string LeftLabel => "TextFlowContainer (without MSDF, bitmap WorkSans)"; + protected override string RightLabel => "MsdfTextFlowContainer (with MSDF)"; + + protected override Drawable CreateLeftSample() => new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = leftFlow = new TextFlowContainer(s => s.Font = default_fu) + { + Width = flow_width, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + + protected override Drawable CreateRightSample() => new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = rightFlow = new MsdfTextFlowContainer(s => s.Font = default_fu) + { + Width = flow_width, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("Default multi-paragraph", () => + { + resetProperties(); + leftFlow.Text = multi_paragraph_text; + rightFlow.Text = multi_paragraph_text; + }); + + AddStep("AddText (\\n is paragraph)", () => + { + resetProperties(); + leftFlow.Clear(); + rightFlow.Clear(); + + leftFlow.AddText("Line 1\nLine 2\nLine 3"); + rightFlow.AddText("Line 1\nLine 2\nLine 3"); + + Schedule(logFlowMetrics); + }); + + AddStep("AddParagraph (\\n is line break)", () => + { + resetProperties(); + leftFlow.Clear(); + rightFlow.Clear(); + + leftFlow.AddParagraph("Line 1\nLine 2\nLine 3"); + rightFlow.AddParagraph("Line 1\nLine 2\nLine 3"); + }); + + foreach (var anchor in new[] + { + Anchor.TopLeft, Anchor.TopCentre, Anchor.TopRight, + Anchor.CentreLeft, Anchor.Centre, Anchor.CentreRight, + Anchor.BottomLeft, Anchor.BottomCentre, Anchor.BottomRight + }) + { + AddStep($"Anchor: {anchor}", () => + { + leftFlow.TextAnchor = anchor; + rightFlow.TextAnchor = anchor; + }); + } + + AddStep("Indents: FirstLine=40, Content=20", () => + { + resetProperties(); + leftFlow.FirstLineIndent = 40; + leftFlow.ContentIndent = 20; + rightFlow.FirstLineIndent = 40; + rightFlow.ContentIndent = 20; + leftFlow.Text = multi_paragraph_text; + rightFlow.Text = multi_paragraph_text; + }); + + AddStep("Spacing: Para=2.0, Line=10", () => + { + resetProperties(); + leftFlow.ParagraphSpacing = 2.0f; + leftFlow.LineSpacing = 10f; + rightFlow.ParagraphSpacing = 2.0f; + rightFlow.LineSpacing = 10f; + leftFlow.Text = multi_paragraph_text; + rightFlow.Text = multi_paragraph_text; + }); + + AddStep("URLs, Hyphens & CJK", () => + { + resetProperties(); + leftFlow.Text = cjk_and_urls_text; + rightFlow.Text = cjk_and_urls_text; + }); + + AddStep("Mixed manual instances", () => + { + resetProperties(); + leftFlow.Clear(); + rightFlow.Clear(); + + leftFlow.AddText("Before manual: "); + leftFlow.AddText(new SpriteText + { + Text = "[MANUAL INSTANCE]", + Colour = Color4.Gold, + Font = default_fu + }); + leftFlow.AddText(" - After manual text continuation."); + + rightFlow.AddText("Before manual: "); + rightFlow.AddText(new MsdfSpriteText + { + Text = "[MANUAL INSTANCE]", + Colour = Color4.Gold, + Font = default_fu + }); + rightFlow.AddText(" - After manual text continuation."); + }); + + AddStep("Baseline alignment (mixed sizes)", () => + { + resetProperties(); + leftFlow.Clear(); + rightFlow.Clear(); + + leftFlow.AddText("Normal (20) ", s => s.Font = default_fu.With(size: 20)); + leftFlow.AddText("Huge (36) ", s => s.Font = default_fu.With(size: 36)); + leftFlow.AddText("Small (14) ", s => s.Font = default_fu.With(size: 14)); + leftFlow.AddText("on same baseline."); + + rightFlow.AddText("Normal (20) ", s => s.Font = default_fu.With(size: 20)); + rightFlow.AddText("Huge (36) ", s => s.Font = default_fu.With(size: 36)); + rightFlow.AddText("Small (14) ", s => s.Font = default_fu.With(size: 14)); + rightFlow.AddText("on same baseline."); + }); + } + + private void logFlowMetrics() + { + Logger.Log($"--- left (native) flow.Height={leftFlow.DrawHeight:F3} ---"); + foreach (var c in leftFlow.Children) + Logger.Log($" {c.GetType().Name,-20} '{(c as SpriteText)?.Text}' Y={c.Y,8:F3} H={c.DrawHeight,8:F3}"); + + Logger.Log($"--- right (msdf) flow.Height={rightFlow.DrawHeight:F3} ---"); + foreach (var c in rightFlow.Children) + Logger.Log($" {c.GetType().Name,-20} '{(c as MsdfSpriteText)?.Text}' Y={c.Y,8:F3} H={c.DrawHeight,8:F3}"); + } + + private void resetProperties() + { + leftFlow.TextAnchor = Anchor.TopLeft; + rightFlow.TextAnchor = Anchor.TopLeft; + + leftFlow.FirstLineIndent = 0; + rightFlow.FirstLineIndent = 0; + + leftFlow.ContentIndent = 0; + rightFlow.ContentIndent = 0; + + leftFlow.ParagraphSpacing = 0.5f; + rightFlow.ParagraphSpacing = 0.5f; + + leftFlow.LineSpacing = 0; + rightFlow.LineSpacing = 0; + + leftFlow.Spacing = Vector2.Zero; + rightFlow.Spacing = Vector2.Zero; + } +} + diff --git a/tests/VIsual/MsdfComparisonTestScene.cs b/tests/VIsual/MsdfComparisonTestScene.cs new file mode 100644 index 0000000..4ecc662 --- /dev/null +++ b/tests/VIsual/MsdfComparisonTestScene.cs @@ -0,0 +1,200 @@ +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual; + +public abstract partial class SampleComparisonTestScene : SampleTestScene +{ + private const float top_bar_height = 40; + private const float min_zoom = 0.5f; + private const float max_zoom = 32f; + + private readonly BindableNumber zoom = new(1f) + { + MinValue = min_zoom, + MaxValue = max_zoom, + Precision = 0.01f, + }; + + private readonly Bindable pan = new(); + + private SpriteText zoomValueText = null!; + + protected abstract string LeftLabel { get; } + protected abstract string RightLabel { get; } + + protected abstract Drawable CreateLeftSample(); + protected abstract Drawable CreateRightSample(); + + [BackgroundDependencyLoader] + private void load() + { + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.MidnightBlue, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = top_bar_height }, + Children = + [ + new ComparisonColumn(LeftLabel, CreateLeftSample(), zoom, pan) + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + Width = 0.5f, + Padding = new MarginPadding { Top = 8 }, + }, + new ComparisonColumn(RightLabel, CreateRightSample(), zoom, pan) + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + Width = 0.5f, + X = 0.5f, + Padding = new MarginPadding { Top = 8 }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 2, + X = 0.5f, + Origin = Anchor.TopCentre, + Colour = Color4.White, + Alpha = 0.25f, + }, + ], + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = top_bar_height, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, + }, + new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 16, + Text = "Scroll to zoom, drag to pan", + Font = FontUsage.Default.With( size: 16, weight: "Regular"), + Colour = Color4.LightGray, + }, + zoomValueText = new SpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -84, + Font = FontUsage.Default.With(size: 18, weight: "Regular"), + }, + new BasicButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -8, + Width = 64, + Height = 28, + Text = "Reset", + Action = reset, + }, + ], + }, + ]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + zoom.BindValueChanged(e => zoomValueText.Text = $"{e.NewValue:0.00}x", true); + } + + protected override bool OnScroll(ScrollEvent e) + { + var factor = MathF.Pow(1.2f, e.ScrollDelta.Y); + zoom.Value = Math.Clamp(zoom.Value * factor, min_zoom, max_zoom); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + pan.Value += e.Delta; + base.OnDrag(e); + } + + private void reset() + { + zoom.Value = 1f; + pan.Value = Vector2.Zero; + } + + private partial class ComparisonColumn : Container + { + private readonly string label; + private readonly Drawable sample; + private readonly Bindable zoom; + private readonly Bindable pan; + + private Container zoomArea = null!; + + public ComparisonColumn(string label, Drawable sample, Bindable zoom, Bindable pan) + { + this.label = label; + this.sample = sample; + this.zoom = zoom.GetBoundCopy(); + this.pan = pan.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + + InternalChildren = + [ + new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = label, + Font = FontUsage.Default.With(size: 20), + }, + zoomArea = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = sample, + }, + ]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + zoom.BindValueChanged(e => zoomArea.Scale = new Vector2(e.NewValue), true); + pan.BindValueChanged(e => zoomArea.Position = e.NewValue, true); + } + } +} diff --git a/tests/VIsual/Sprites/MsdfComparisonTestScene.cs b/tests/VIsual/Sprites/MsdfComparisonTestScene.cs new file mode 100644 index 0000000..a8af452 --- /dev/null +++ b/tests/VIsual/Sprites/MsdfComparisonTestScene.cs @@ -0,0 +1,200 @@ +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites; + +public abstract partial class MsdfComparisonTestScene : SampleTestScene +{ + private const float top_bar_height = 40; + private const float min_zoom = 0.5f; + private const float max_zoom = 32f; + + private readonly BindableNumber zoom = new(1f) + { + MinValue = min_zoom, + MaxValue = max_zoom, + Precision = 0.01f, + }; + + private readonly Bindable pan = new(); + + private SpriteText zoomValueText = null!; + + protected abstract string LeftLabel { get; } + protected abstract string RightLabel { get; } + + protected abstract Drawable CreateLeftSample(); + protected abstract Drawable CreateRightSample(); + + [BackgroundDependencyLoader] + private void load() + { + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.MidnightBlue, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = top_bar_height }, + Children = + [ + new ComparisonColumn(LeftLabel, CreateLeftSample(), zoom, pan) + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + Width = 0.5f, + Padding = new MarginPadding { Top = 8 }, + }, + new ComparisonColumn(RightLabel, CreateRightSample(), zoom, pan) + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + Width = 0.5f, + X = 0.5f, + Padding = new MarginPadding { Top = 8 }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 2, + X = 0.5f, + Origin = Anchor.TopCentre, + Colour = Color4.White, + Alpha = 0.25f, + }, + ], + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = top_bar_height, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, + }, + new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 16, + Text = "Scroll to zoom, drag to pan", + Font = FontUsage.Default.With(size: 16), + Colour = Color4.LightGray, + }, + zoomValueText = new SpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -84, + Font = FontUsage.Default.With(size: 18), + }, + new BasicButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -8, + Width = 64, + Height = 28, + Text = "Reset", + Action = reset, + }, + ], + }, + ]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + zoom.BindValueChanged(e => zoomValueText.Text = $"{e.NewValue:0.00}x", true); + } + + protected override bool OnScroll(ScrollEvent e) + { + var factor = MathF.Pow(1.2f, e.ScrollDelta.Y); + zoom.Value = Math.Clamp(zoom.Value * factor, min_zoom, max_zoom); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + pan.Value += e.Delta; + base.OnDrag(e); + } + + private void reset() + { + zoom.Value = 1f; + pan.Value = Vector2.Zero; + } + + private partial class ComparisonColumn : Container + { + private readonly string label; + private readonly Drawable sample; + private readonly Bindable zoom; + private readonly Bindable pan; + + private Container zoomArea = null!; + + public ComparisonColumn(string label, Drawable sample, Bindable zoom, Bindable pan) + { + this.label = label; + this.sample = sample; + this.zoom = zoom.GetBoundCopy(); + this.pan = pan.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + + InternalChildren = + [ + new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = label, + Font = FontUsage.Default.With(size: 20), + }, + zoomArea = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = sample, + }, + ]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + zoom.BindValueChanged(e => zoomArea.Scale = new Vector2(e.NewValue), true); + pan.BindValueChanged(e => zoomArea.Position = e.NewValue, true); + } + } +} diff --git a/tests/VIsual/Sprites/TestSceneMsdfItalic.cs b/tests/VIsual/Sprites/TestSceneMsdfItalic.cs new file mode 100644 index 0000000..f2ef13d --- /dev/null +++ b/tests/VIsual/Sprites/TestSceneMsdfItalic.cs @@ -0,0 +1,43 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites; + +public partial class TestSceneMsdfItalic : MsdfComparisonTestScene +{ + private const float demo_font_size = 28.0f; + private const string demo_text = "MSDF Sample Aa Oo 123"; + + protected override string LeftLabel => "SpriteText (without MSDF, bitmap WorkSans)"; + protected override string RightLabel => "MsdfSpriteText (with MSDF)"; + + protected override Drawable CreateLeftSample() => createFlow( + text => new SpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = text }, + (drawable, italics) => ((SpriteText)drawable).Font = new FontUsage("WorkSans", demo_font_size, "Regular", italics)); + + protected override Drawable CreateRightSample() => createFlow( + text => new MsdfSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = text }, + (drawable, italics) => ((MsdfSpriteText)drawable).Font = new FontUsage("WorkSans", demo_font_size, "Regular", italics)); + + private static FillFlowContainer createFlow(Func create, Action setFont) + { + var upright = create($"Regular: {demo_text}"); + var italic = create($"Italic: {demo_text}"); + + setFont(upright, false); + setFont(italic, true); + + return new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 16), + Children = [upright, italic], + }; + } +} diff --git a/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs b/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs new file mode 100644 index 0000000..d205fa1 --- /dev/null +++ b/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs @@ -0,0 +1,106 @@ +using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK.Graphics; + +namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites; + +public partial class TestSceneMsdfSpriteText : SampleTestScene +{ + public TestSceneMsdfSpriteText() + { + FillFlowContainer flow; + + Children = + [ + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Children = + [ + flow = new FillFlowContainer + { + Anchor = Anchor.TopLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + } + ] + } + ]; + + flow.Add(new MsdfSpriteText + { + Text = @"the quick red fox jumps over the lazy brown 子犬🐶" + }); + flow.Add(new MsdfSpriteText + { + Text = @"THE QUICK RED FOX JUMPS OVER THE LAZY BROWN 子犬🐶" + }); + flow.Add(new MsdfSpriteText + { + Text = @"0123456789!@#$%^&*()_-+-[]{}.,<>;'\" + }); + + flow.Add(new Container + { + Margin = new MarginPadding { Vertical = 5 }, + AutoSizeAxes = Axes.Both, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new MsdfSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Black, + UseFullGlyphHeight = true, + Text = "UseFullGlyphHeight = true", + }, + ] + }); + + flow.Add(new Container + { + Margin = new MarginPadding { Vertical = 5 }, + AutoSizeAxes = Axes.Both, + Children = + [ + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new MsdfSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Black, + UseFullGlyphHeight = false, + Text = "UseFullGlyphHeight = false", + }, + ] + }); + + for (var i = 1; i <= 200; i++) + { + var text = new MsdfSpriteText + { + Text = $@"Font testy at size {i}", + Font = new FontUsage( + "WorkSans", + i, + i % 4 > 1 ? "Bold" : "Regular", + i % 2 == 1), + AllowMultiline = true, + RelativeSizeAxes = Axes.X, + }; + + flow.Add(text); + } + } +}