From 3b074a2684bcf5410fb25f84915441316055172c Mon Sep 17 00:00:00 2001 From: Speiger Date: Thu, 6 Aug 2026 13:56:16 +0200 Subject: [PATCH] Font renderer ported --- .../coreengine/assets/api/AssetFilter.java | 35 ++ .../assets/api/IResourceListener.java | 10 +- .../coreengine/assets/api/ReloadContext.java | 2 +- .../assets/api/SteppedResourceListener.java | 13 + .../assets/manager/AssetManager.java | 2 +- .../assets/manager/ReloadBuilder.java | 8 +- .../coreengine/assets/manager/ReloadTask.java | 8 +- .../api/buffer/GrowableVertexBuffer.java | 39 +++ .../api/buffer/MultiVertexBuffer.java | 75 +++++ .../graphics/api/core/GraphicsDevice.java | 2 + .../opengl/core/GLGraphicsDevice.java | 2 + .../src/coreengine/ui/gui/font/Font.java | 109 +++++++ .../src/coreengine/ui/gui/font/FontGroup.java | 30 ++ .../coreengine/ui/gui/font/FontManager.java | 118 +++++++ .../coreengine/ui/gui/font/FontSplitter.java | 106 +++++++ .../src/coreengine/ui/gui/font/FontStyle.java | 59 ++++ .../coreengine/ui/gui/font/FontTexture.java | 107 +++++++ .../coreengine/ui/gui/font/GlythCache.java | 77 +++++ .../src/coreengine/ui/gui/font/TextStyle.java | 140 ++++++++ .../coreengine/ui/gui/font/glyth/Glyth.java | 17 + .../ui/gui/font/glyth/GlythData.java | 19 ++ .../ui/gui/font/glyth/IGlythSheetInfo.java | 19 ++ .../ui/gui/font/glyth/MissingGlyth.java | 70 ++++ .../ui/gui/font/glyth/UnbakedGlyth.java | 20 ++ .../gui/font/providers/FreeTypeProvider.java | 298 ++++++++++++++++++ .../ui/gui/font/providers/IFontProvider.java | 14 + .../font/providers/STBTrueTypeProvider.java | 215 +++++++++++++ 27 files changed, 1605 insertions(+), 9 deletions(-) create mode 100644 src/assets/java/speiger/src/coreengine/assets/api/AssetFilter.java create mode 100644 src/assets/java/speiger/src/coreengine/assets/api/SteppedResourceListener.java create mode 100644 src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/GrowableVertexBuffer.java create mode 100644 src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/MultiVertexBuffer.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/Font.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/FontGroup.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/FontManager.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/FontSplitter.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/FontStyle.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/FontTexture.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/GlythCache.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/TextStyle.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/glyth/Glyth.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/glyth/GlythData.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/glyth/IGlythSheetInfo.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/glyth/MissingGlyth.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/glyth/UnbakedGlyth.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/providers/FreeTypeProvider.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/providers/IFontProvider.java create mode 100644 src/gui/java/speiger/src/coreengine/ui/gui/font/providers/STBTrueTypeProvider.java diff --git a/src/assets/java/speiger/src/coreengine/assets/api/AssetFilter.java b/src/assets/java/speiger/src/coreengine/assets/api/AssetFilter.java new file mode 100644 index 0000000..38b2741 --- /dev/null +++ b/src/assets/java/speiger/src/coreengine/assets/api/AssetFilter.java @@ -0,0 +1,35 @@ +package speiger.src.coreengine.assets.api; + +import java.util.Map; + +public record AssetFilter(String prefix, String extension) { + public static AssetFilter json(String prefix) { + return new AssetFilter(prefix, ".json"); + } + + public ID id(ID file) { + String location = file.fileLocation(); + return ID.of(file.domain(), location.substring(prefix().length()+1, location.length()-extension().length())); + } + + public ID file(ID id) { + return ID.of(id.domain(), prefix()+"/"+id.fileLocation()+extension()); + } + + public Map list(IAssetProvider provider) { + return provider.list(prefix, T -> T.endsWith(extension)); + } + + public Map listRoot(IAssetProvider provider) { + return provider.list(prefix, T -> T.endsWith(extension) && T.isRootFolder(prefix)); + } + + public Map multi(IAssetProvider provider) { + return provider.listAll(prefix, T -> T.endsWith(extension)); + } + + public Map multiRoot(IAssetProvider provider) { + return provider.listAll(prefix, T -> T.endsWith(extension) && T.isRootFolder(prefix)); + } + +} diff --git a/src/assets/java/speiger/src/coreengine/assets/api/IResourceListener.java b/src/assets/java/speiger/src/coreengine/assets/api/IResourceListener.java index d0b3858..1b73279 100644 --- a/src/assets/java/speiger/src/coreengine/assets/api/IResourceListener.java +++ b/src/assets/java/speiger/src/coreengine/assets/api/IResourceListener.java @@ -3,11 +3,17 @@ package speiger.src.coreengine.assets.api; import java.util.concurrent.CompletableFuture; public interface IResourceListener { - void name(); CompletableFuture reload(ReloadContext context, ISyncer sync); - void release(); + + public static interface ISimpleResourceListener extends IResourceListener { + @Override + default CompletableFuture reload(ReloadContext context, ISyncer sync) { return CompletableFuture.runAsync(() -> reload(context), context.tasks()); } + void reload(ReloadContext context); + } public static interface ISyncer { CompletableFuture sync(T value); } + + } diff --git a/src/assets/java/speiger/src/coreengine/assets/api/ReloadContext.java b/src/assets/java/speiger/src/coreengine/assets/api/ReloadContext.java index 0611965..f579a4c 100644 --- a/src/assets/java/speiger/src/coreengine/assets/api/ReloadContext.java +++ b/src/assets/java/speiger/src/coreengine/assets/api/ReloadContext.java @@ -3,7 +3,7 @@ package speiger.src.coreengine.assets.api; import java.util.Set; import java.util.concurrent.Executor; -public record ReloadContext(IAssetProvider provider, Set filter, Executor main, Executor background) { +public record ReloadContext(IAssetProvider provider, Set filter, Executor mainThread, Executor tasks, Executor background) { public boolean hasChanged(ID id) { return filter.isEmpty() || filter.contains(id); diff --git a/src/assets/java/speiger/src/coreengine/assets/api/SteppedResourceListener.java b/src/assets/java/speiger/src/coreengine/assets/api/SteppedResourceListener.java new file mode 100644 index 0000000..d59222b --- /dev/null +++ b/src/assets/java/speiger/src/coreengine/assets/api/SteppedResourceListener.java @@ -0,0 +1,13 @@ +package speiger.src.coreengine.assets.api; + +import java.util.concurrent.CompletableFuture; + +public abstract class SteppedResourceListener implements IResourceListener { + @Override + public CompletableFuture reload(ReloadContext context, ISyncer sync) { + return CompletableFuture.supplyAsync(() -> prepare(context), context.tasks()).thenCompose(sync::sync).thenAccept(T -> apply(T, context)); + } + + protected abstract T prepare(ReloadContext context); + protected abstract void apply(T value, ReloadContext context); +} diff --git a/src/assets/java/speiger/src/coreengine/assets/manager/AssetManager.java b/src/assets/java/speiger/src/coreengine/assets/manager/AssetManager.java index 8107bdd..4e044d1 100644 --- a/src/assets/java/speiger/src/coreengine/assets/manager/AssetManager.java +++ b/src/assets/java/speiger/src/coreengine/assets/manager/AssetManager.java @@ -76,7 +76,7 @@ public class AssetManager { if(builder.listeners.isEmpty()) builder.withListener(tracker.listeners()); return new ReloadTask(get(), builder); } - + public boolean isReloadable() { return manager != null; } diff --git a/src/assets/java/speiger/src/coreengine/assets/manager/ReloadBuilder.java b/src/assets/java/speiger/src/coreengine/assets/manager/ReloadBuilder.java index 3670bdb..8e93ca2 100644 --- a/src/assets/java/speiger/src/coreengine/assets/manager/ReloadBuilder.java +++ b/src/assets/java/speiger/src/coreengine/assets/manager/ReloadBuilder.java @@ -16,6 +16,7 @@ import speiger.src.coreengine.assets.providers.SingleProvider; public class ReloadBuilder { Executor mainRunner = Runnable::run; + Executor taskRunner = Runnable::run; Executor syncRunner = Runnable::run; Set filter = ObjectSets.empty(); Optional packages = Optional.empty(); @@ -26,11 +27,16 @@ public class ReloadBuilder { return this; } - public ReloadBuilder withMain(Executor main) { + public ReloadBuilder withMainThread(Executor main) { mainRunner = Objects.requireNonNull(main); return this; } + public ReloadBuilder withTasks(Executor tasks) { + taskRunner = Objects.requireNonNull(tasks); + return this; + } + public ReloadBuilder withSync(Executor sync) { syncRunner = Objects.requireNonNull(sync); return this; diff --git a/src/assets/java/speiger/src/coreengine/assets/manager/ReloadTask.java b/src/assets/java/speiger/src/coreengine/assets/manager/ReloadTask.java index 15ea6b8..48648a7 100644 --- a/src/assets/java/speiger/src/coreengine/assets/manager/ReloadTask.java +++ b/src/assets/java/speiger/src/coreengine/assets/manager/ReloadTask.java @@ -13,13 +13,13 @@ import speiger.src.coreengine.assets.api.IResourceListener; import speiger.src.coreengine.assets.api.IResourceListener.ISyncer; import speiger.src.coreengine.assets.api.ReloadContext; -public class ReloadTask { +public class ReloadTask { AtomicInteger started = new AtomicInteger(); AtomicInteger finished = new AtomicInteger(); CompletableFuture future; public ReloadTask(IAssetProvider provider, ReloadBuilder builder) { - future = reload(new ReloadContext(provider, builder.filter, new CountingExecutor(builder.mainRunner, started, finished), new CountingExecutor(builder.syncRunner, started, finished)), new ObjectArrayList<>(builder.listeners.get())); + future = reload(new ReloadContext(provider, builder.filter, new CountingExecutor(builder.mainRunner, started, finished), new CountingExecutor(builder.taskRunner, started, finished), new CountingExecutor(builder.syncRunner, started, finished)), new ObjectArrayList<>(builder.listeners.get())); } public int started() { return started.get(); } @@ -36,14 +36,14 @@ public class ReloadTask { List> tasks = new ObjectArrayList<>(); for(IResourceListener listener : listeners) { CompletableFuture current = chainedTask; - chainedTask = listener.reload(context, new Syncer(context.main(), current, syncFuture, listener, todo)); + chainedTask = listener.reload(context, new Syncer(context.tasks(), current, syncFuture, listener, todo)); tasks.add(chainedTask); } return CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenRunAsync(() -> { finished.getAndIncrement(); try { context.provider().close(); } catch(Exception e) { e.printStackTrace(); } - }, context.main()); + }, context.tasks()); } diff --git a/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/GrowableVertexBuffer.java b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/GrowableVertexBuffer.java new file mode 100644 index 0000000..f09a0aa --- /dev/null +++ b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/GrowableVertexBuffer.java @@ -0,0 +1,39 @@ +package speiger.src.coreengine.platform.graphics.api.buffer; + +public class GrowableVertexBuffer +{ + VertexBuffer buffer; + final int bytesPerIndex; + final int limit; + final int startSize; + int currentCapacity; + + public GrowableVertexBuffer(VertexBuffer buffer, int bytesPerIndex, int limit, int startSize) + { + this.buffer = buffer; + this.bytesPerIndex = bytesPerIndex; + this.limit = limit; + this.startSize = startSize; + currentCapacity = startSize; + buffer.bind().allocate(startSize * bytesPerIndex).unbind(); + } + + public VertexBuffer ensureSize(int capacity) + { + if(capacity > currentCapacity) + { + if(currentCapacity == limit) + { + throw new RuntimeException("THIS SHOULD NEVER HAPPEN!: "+currentCapacity+":"+limit); + } + currentCapacity = Math.min(Math.max(currentCapacity + (currentCapacity / 2), capacity), limit); + buffer.bind().grow(currentCapacity * bytesPerIndex).unbind(); + } + else if(capacity < currentCapacity / 2 && currentCapacity != 10) + { + currentCapacity = Math.max(10, capacity); + buffer.bind().shrink(currentCapacity * bytesPerIndex).unbind(); + } + return buffer; + } +} diff --git a/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/MultiVertexBuffer.java b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/MultiVertexBuffer.java new file mode 100644 index 0000000..a107051 --- /dev/null +++ b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/buffer/MultiVertexBuffer.java @@ -0,0 +1,75 @@ +package speiger.src.coreengine.platform.graphics.api.buffer; + +public class MultiVertexBuffer { + VertexBuffer buffer; + final int bytesPerIndex; + final int limit; + final int startSize; + final float growRate; + final int padding; + final int[][] offsets; + int currentCapacity; + + public MultiVertexBuffer(VertexBuffer buffer, int bytesPerIndex, int limit, int startSize, int padding, float growRate, int[][] offsets) { + super(); + this.buffer = buffer; + this.bytesPerIndex = bytesPerIndex; + this.limit = limit; + this.startSize = startSize; + this.padding = padding; + this.growRate = growRate; + this.offsets = offsets; + currentCapacity = startSize; + buffer.bind().allocate(startSize * bytesPerIndex).unbind(); + } + + public boolean ensureSize(int index, int capacity) { + int[] offset = offsets[index]; + int room = offset[2] - offset[0]; + if(capacity >= room) { + int newCap = Math.max((int)(room * growRate), capacity + 1); + offset[2] = offset[0] + newCap; + grow(index + 1, newCap - room); + return true; + } + return false; + } + + protected void grow(int index, int extra) { + if(index == offsets.length) { + int newCap = offsets[offsets.length - 1][2] + extra; + if(newCap > currentCapacity) { + if(currentCapacity == limit) { throw new RuntimeException("THIS SHOULD NEVER HAPPEN!: "+currentCapacity+":"+limit); } + currentCapacity = Math.min(Math.max(currentCapacity + (currentCapacity / 2), newCap), limit); + buffer.bind().grow(currentCapacity * bytesPerIndex).unbind(); + } + return; + } + int startBytes = offsets[index][0] * bytesPerIndex; + int endBytes = offsets[offsets.length - 1][2] * bytesPerIndex; + for(int i = index;i < offsets.length;i++) { + offsets[i][0] += extra; + offsets[i][2] += extra; + } + grow(offsets.length, extra); + buffer.bind().shift(startBytes, endBytes - startBytes, extra * bytesPerIndex).unbind(); + } + + protected void shrink(int index, int extra) { + if(index == offsets.length) { + int newCap = offsets[offsets.length - 1][2] - extra; + if(newCap < currentCapacity / 2 && currentCapacity != 10) { + currentCapacity = Math.max(10, newCap); + buffer.bind().shrink(currentCapacity * bytesPerIndex).unbind(); + } + return; + } + int startBytes = offsets[index][0] * bytesPerIndex; + buffer.bind().shift(startBytes, (offsets[offsets.length - 1][2] * bytesPerIndex) - startBytes, -(extra * bytesPerIndex)).unbind(); + for(int i = index;i < offsets.length;i++) { + offsets[i][0] -= extra; + offsets[i][2] -= extra; + } + shrink(offsets.length, extra); + } +} diff --git a/src/graphics/java/speiger/src/coreengine/platform/graphics/api/core/GraphicsDevice.java b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/core/GraphicsDevice.java index c726f3b..993e2ab 100644 --- a/src/graphics/java/speiger/src/coreengine/platform/graphics/api/core/GraphicsDevice.java +++ b/src/graphics/java/speiger/src/coreengine/platform/graphics/api/core/GraphicsDevice.java @@ -17,6 +17,8 @@ import speiger.src.coreengine.platform.input.window.Window; public interface GraphicsDevice { public static final ScopedValue SCOPE = ScopedValue.newInstance(); + public static GraphicsDevice get() { return SCOPE.get(); } + Window window(); GraphicsSurface createSurface(); GraphicsCommandBuffer createCommandBuffer(ExecutionType type); diff --git a/src/graphics/java/speiger/src/coreengine/platform/graphics/opengl/core/GLGraphicsDevice.java b/src/graphics/java/speiger/src/coreengine/platform/graphics/opengl/core/GLGraphicsDevice.java index 5aeecbc..2fc548c 100644 --- a/src/graphics/java/speiger/src/coreengine/platform/graphics/opengl/core/GLGraphicsDevice.java +++ b/src/graphics/java/speiger/src/coreengine/platform/graphics/opengl/core/GLGraphicsDevice.java @@ -10,6 +10,7 @@ import java.util.OptionalInt; import java.util.function.IntPredicate; import java.util.function.Supplier; +import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL20; @@ -88,6 +89,7 @@ public class GLGraphicsDevice implements GraphicsDevice { this.owner = owner; this.manager = manager; this.capabilities = capbilities; + GL.setCapabilities(capbilities); this.queue = new GLCommandQueue(this); } diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/Font.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/Font.java new file mode 100644 index 0000000..5b065eb --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/Font.java @@ -0,0 +1,109 @@ +package speiger.src.coreengine.ui.gui.font; + +import java.util.Map; +import java.util.function.Consumer; + +import speiger.src.coreengine.assets.api.ID; +import speiger.src.coreengine.platform.graphics.api.texture.Texture; +import speiger.src.coreengine.platform.graphics.api.vertex.builder.IVertexBuilder; +import speiger.src.coreengine.ui.gui.font.glyth.Glyth; +import speiger.src.coreengine.ui.gui.font.glyth.GlythData; +import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.GlythBaker; +import speiger.src.coreengine.utils.helpers.TextUtil; +import speiger.src.coreengine.utils.misc.CodepointSequence; + +public class Font { + public static final int FORMAT_CODE_POINT = '§'; + FontSplitter splitter; + Map fonts; + GlythBaker baker; + float oversample; + GlythCache[] styledCache = new GlythCache[] {new GlythCache(this, 0), new GlythCache(this, 1), new GlythCache(this, 2), new GlythCache(this, 3)}; + + protected Font(Map fonts, GlythBaker baker, float oversample, Consumer clearing) { + this.fonts = fonts; + this.baker = baker; + this.oversample = oversample; + clearing.accept(this::reset); + splitter = new FontSplitter(this); + } + + private void reset() { + for(int i = 0;i<4;i++) { + styledCache[i].reset(); + } + } + + public GlythData data(FontStyle font, int codepoint) { + return styledCache[font.style() & 0x3].data(font, codepoint); + } + + public Glyth glyth(FontStyle font, int codepoint) { + return styledCache[font.style() & 0x3].glyth(font, codepoint); + } + + public float width(FontStyle font, int codepoint) { + return styledCache[font.style() & 0x3].data(font, codepoint).advance(); + } + + protected FontGroup font(ID font) { + return fonts.get(font); + } + + public float drawText(String text, float x, float y, int color, TexturedBuffer buffer, boolean end) { + return drawText(TextStyle.DEFAULT, text, x, y, color, buffer, 1F, end); + } + + public float drawText(TextStyle style, String text, float x, float y, int color, TexturedBuffer buffer, float scale, boolean end) { + TextStyle currentStyle = style; + int currentColor = currentStyle.hasColor() ? currentStyle.color() : color; + float xStart = x; + int previousCodepoint = -1; + for(int i = 0,m=text.length();i"); + if(end == -1) return null; + return input.subSequence(2, end-1); + } + + + public static interface TexturedBuffer { + public IVertexBuilder builderForTexture(Texture texture); + public void end(); + } +} diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/FontGroup.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontGroup.java new file mode 100644 index 0000000..14fb4a3 --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontGroup.java @@ -0,0 +1,30 @@ +package speiger.src.coreengine.ui.gui.font; + +import java.util.List; + +import speiger.src.coreengine.assets.api.ID; +import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth; +import speiger.src.coreengine.ui.gui.font.providers.IFontProvider; + +public class FontGroup { + ID locations; + List providers; + + public FontGroup(ID locations, List providers) { + this.locations = locations; + this.providers = providers; + } + + public UnbakedGlyth data(int codepoint, int style, float size, float oversample) { + for(int i = 0,m=providers.size();i>> { + private static final AssetFilter FILTER = AssetFilter.json("font"); + Float2ObjectMap cachedFonts = Float2ObjectMap.builder().map(); + Map fonts = Object2ObjectMap.builder().linkedMap(); + Map> fontParsers = Object2ObjectMap.builder().map(); + List listeners = new ObjectArrayList<>(); + List textures = new ObjectArrayList<>(); + + public FontManager() { + registerParser("stb-ttf", STBTrueTypeProvider::create); + registerParser("free-ttf", FreeTypeProvider::load); + } + + public void registerParser(String id, BiFunction parser) { + fontParsers.putIfAbsent(id, parser); + } + + @Override + protected Map> prepare(ReloadContext context) { + Map loadingCache = Object2ObjectMap.builder().linkedMap(); + Object2ObjectMap> providers = Object2ObjectMap.builder().linkedMap(); + for(Entry entry : FILTER.multiRoot(context.provider()).entrySet()) { + ID id = entry.getKey(); + for(JsonObject obj : entry.getValue().map(Serialized.JSON_OBJ, JsonObject::new)) { + JsonUtil.iterateValues(obj.get("providers"), T -> { + ID location = ID.tryOf(T.getAsString()); + if(location == null || location.isRoot()) return; + IFontProvider font = loadingCache.computeIfAbsent(location.prefix("font"), E -> getFont(E, context.provider())); + if(font == null) return; + providers.supplyIfAbsent(FILTER.id(id), ObjectArrayList::new).add(font); + }); + } + } + return providers; + } + + @Override + protected void apply(Map> value, ReloadContext context) { + reset(context.mainThread()); + value.forEach((K, V) -> fonts.put(K, new FontGroup(K, V))); + } + + public void destroy() { + reset(Runnable::run); + listeners.clear(); + } + + private void reset(Executor execute) { + listeners.forEach(Runnable::run); + execute.execute(() -> { + textures.forEach(FontTexture::delete); + textures.clear(); + }); + fonts.values().forEach(FontGroup::close); + fonts.clear(); + cachedFonts.clear(); + } + + public Font createFont() { + return createFont(1F); + } + + public Font createFont(float oversample) { + return cachedFonts.computeIfAbsent(oversample, T -> new Font(fonts, this::stitch, T, listeners::add)); + } + + private Glyth stitch(IGlythSheetInfo info) { + for(int i = 0,m=textures.size();i builder = fontParsers.get(obj.get("type").getAsString()); + return builder == null ? null : builder.apply(obj, provider); + } + catch(Exception e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/FontSplitter.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontSplitter.java new file mode 100644 index 0000000..151cfba --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontSplitter.java @@ -0,0 +1,106 @@ +package speiger.src.coreengine.ui.gui.font; + +import speiger.src.coreengine.utils.misc.CodepointSequence; + +public class FontSplitter { + Font font; + + public FontSplitter(Font font) { + this.font = font; + } + + public float width(String text) { + return width(TextStyle.DEFAULT, text, true); + } + + public float width(TextStyle style, String text) { + return width(style, text, true); + } + + public float width(TextStyle style, String text, boolean applyStyleChanges) { + TextStyle currentStyle = style; + float total = 0F; + for(int i = 0,m=text.length();i=0;i--) { + totalWidth += widths[i]; + if(totalWidth >= maxWidth) return text.substring(i, text.length()); + } + } + else { + float totalWidth = 0F; + for(int i = text.length()-1;i>=0;i--) { + int codepoint = text.codePointAt(i); + totalWidth += font.width(style.font(), codepoint); + if(totalWidth >= maxWidth) return text.substring(i, text.length()); + } + } + return text; + } + + public String splitHeadByWidth(TextStyle style, String text, float maxWidth, boolean applyStyleChanges) { + TextStyle current = style; + float width = 0F; + for(int i = 0,m=text.length();i= maxWidth) return text.substring(0, i); + } + return text; + } +} diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/FontStyle.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontStyle.java new file mode 100644 index 0000000..bf51a40 --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontStyle.java @@ -0,0 +1,59 @@ +package speiger.src.coreengine.ui.gui.font; + +import speiger.src.coreengine.assets.api.ID; + +public record FontStyle(ID font, int style, float size) { + public static final FontStyle DEFAULT = new FontStyle(ID.of("default"), 0, 16F); + public static final int REGULAR = 0; + public static final int BOLD = 1; + public static final int ITALIC = 2; + public static final int BOLD_ITCALIC = 3; + + public FontStyle { + if(style < 0 || style > 3) throw new IllegalArgumentException("Style can only be between 0-3. Style found: "+style); + } + + public FontStyle(ID font, float size) { + this(font, REGULAR, size); + } + + public FontStyle(ID font, boolean bold, boolean italic, float size) { + this(font, (bold ? BOLD : 0) | (italic ? ITALIC : 0), size); + } + + public FontStyle with(ID font, int style, float size) { + return new FontStyle(font, style, size); + } + + public FontStyle with(ID font, float size) { + return new FontStyle(font, style, size); + } + + public FontStyle with(ID font) { + return new FontStyle(font, style, size); + } + + public FontStyle with(float size) { + return new FontStyle(font, style, size); + } + + public FontStyle with(int style) { + return new FontStyle(font, style, size); + } + + public FontStyle withBold(boolean bold) { + return new FontStyle(font, bold ? (style | BOLD) : (style & ~BOLD), size); + } + + public FontStyle withItalic(boolean italic) { + return new FontStyle(font, italic ? (style | ITALIC) : (style & ~ITALIC), size); + } + + public boolean bold() { + return (style() & BOLD) != 0; + } + + public boolean italic() { + return (style() & ITALIC) != 0; + } +} \ No newline at end of file diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/FontTexture.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontTexture.java new file mode 100644 index 0000000..cab761e --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/FontTexture.java @@ -0,0 +1,107 @@ +package speiger.src.coreengine.ui.gui.font; + +import speiger.src.coreengine.math.MathUtils; +import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice; +import speiger.src.coreengine.platform.graphics.api.texture.Texture; +import speiger.src.coreengine.platform.graphics.api.texture.TextureSettings; +import speiger.src.coreengine.platform.graphics.api.texture.states.SwizzleMask; +import speiger.src.coreengine.platform.graphics.api.texture.states.TextureFormat; +import speiger.src.coreengine.ui.gui.font.glyth.Glyth; +import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo; + +public class FontTexture { + private static final TextureSettings COLOR = TextureSettings.builder().build(); + private static final TextureSettings NO_COLOR = TextureSettings.builder() + .format(TextureFormat.LUMINANCE) + .swizzle(SwizzleMask.RED, SwizzleMask.RED, SwizzleMask.RED, SwizzleMask.RED) + .build(); + + Texture texture; + boolean color; + int bounds; + Slot slot; + + public FontTexture(int bounds, boolean color) { + this.texture = GraphicsDevice.get().createTexture(color ? COLOR : NO_COLOR, bounds, bounds); + this.bounds = bounds; + this.color = color; + this.slot = new Slot(0, 0, bounds, bounds); + } + + public static TextureFormat formatByColor(boolean colored) { + return colored ? TextureFormat.RGBA : TextureFormat.R; + } + + public Glyth build(IGlythSheetInfo info) { + if(color != info.isColored()) return null; + Slot result = slot.insert(info); + if(result != null) { + info.upload(texture, result.x, result.y); + float minU = (float)(result.x) / (float)bounds; + float maxU = (float)((result.x) + info.width()) / (float)bounds; + float minV = (float)(result.y) / (float)bounds; + float maxV = (float)((result.y) + info.height()) / (float)bounds; + return new Glyth(texture, minU, minV, maxU, maxV, info.left(), info.right(), info.top(), info.bottom()); + } + return null; + } + + public void delete() { + texture.remove(); + } + + public boolean colored() { return color; } + + public static class Slot { + final int x; + final int y; + final int width; + final int height; + Slot[] children = null; + boolean occupied; + + public Slot(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public Slot insert(IGlythSheetInfo info) { + if(children == null) { + if(occupied) return null; + int iw = info.width(); + int ih = info.height(); + if(iw > width || ih > height) return null; + if(iw == width && ih == height) { + occupied = true; + return this; + } + int dw = width - iw; + int dh = height - ih; + children = new Slot[2]; + if(dw > dh) { + int offset = MathUtils.ceil(iw / 50F); + children[0] = new Slot(x, y, iw, height); + children[1] = new Slot(x + iw + offset, y, dw - offset, height); + } + else { + int offset = MathUtils.ceil(ih / 50F); + children[0] = new Slot(x, y, width, ih); + children[1] = new Slot(x, y + ih + offset, width, dh - offset); + } + return children[0].insert(info); + } + for(int i = 0,m=children.length;i cache = new Object2ObjectOpenHashMap<>(); + + public GlythCache(Font font, int style) { + this.font = font; + this.style = style; + } + + public void reset() { + cache.values().forEach(FontCache::reset); + cache.clear(); + } + + private FontCache cache(FontStyle font) { + return cache.computeIfAbsent(font, FontCache::new); + } + + public GlythData data(FontStyle fontId, int codepoint) { + return cache(fontId).data(codepoint); + } + + public Glyth glyth(FontStyle fontId, int codepoint) { + return cache(fontId).glyth(codepoint); + } + + public class FontCache { + Int2ObjectMap data = new Int2ObjectOpenHashMap<>(); + Int2ObjectMap glyth = new Int2ObjectOpenHashMap<>(); + FontStyle fontStyle; + MissingGlyth missingGlyth; + GlythData missingData; + + + public FontCache(FontStyle fontStyle) { + this.fontStyle = fontStyle; + this.missingGlyth = new MissingGlyth(fontStyle.size(), font.oversample * 2F); + missingData = new GlythData(missingGlyth); + } + + private void reset() { + missingGlyth.cleanCache(); + missingData.cleanCache(); + } + + public GlythData data(int codepoint) { + return data.computeIfAbsent(codepoint, this::compute); + } + + public Glyth glyth(int codepoint) { + return glyth.computeIfAbsent(codepoint, this::bake); + } + + private GlythData compute(int codepoint) { + UnbakedGlyth data = font.font(fontStyle.font()).data(codepoint, style, fontStyle.size(), font.oversample); + return data == null ? missingData : new GlythData(data); + } + + private Glyth bake(int codepoint) { + return data(codepoint).unbaked().bake(font.baker); + } + } +} \ No newline at end of file diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/TextStyle.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/TextStyle.java new file mode 100644 index 0000000..9684086 --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/TextStyle.java @@ -0,0 +1,140 @@ +package speiger.src.coreengine.ui.gui.font; + +import speiger.src.coreengine.assets.api.ID; +import speiger.src.coreengine.math.color.ColorUtils; +import speiger.src.coreengine.utils.helpers.TextUtil; + +public record TextStyle(FontStyle font, int styleFlags, int color) { + public static final TextStyle DEFAULT = new TextStyle(FontStyle.DEFAULT); + public static final int NONE = 0; + public static final int UNDERLINE = 1; + public static final int STRIKETHROUGH = 2; + public static final int HAS_COLOR = 4; + + public TextStyle(FontStyle font) { + this(font, 0, 0xFFFFFFFF); + } + + public TextStyle(FontStyle font, int color) { + this(font, 0, color); + } + + public TextStyle color(int color) { + return new TextStyle(font, styleFlags | HAS_COLOR, color); + } + + public TextStyle removeColor() { + return new TextStyle(font, styleFlags & ~HAS_COLOR, color); + } + + public TextStyle font(FontStyle font) { + return new TextStyle(font, styleFlags, color); + } + + public TextStyle font(ID location) { + return new TextStyle(font.with(location), styleFlags, color); + } + + public TextStyle size(float size) { + return new TextStyle(font.with(size), styleFlags, color); + } + + public TextStyle bold(boolean value) { + return new TextStyle(font.withBold(value), styleFlags, color); + } + + public TextStyle italic(boolean value) { + return new TextStyle(font.withItalic(value), styleFlags, color); + } + + public TextStyle underline(boolean value) { + return new TextStyle(font, value ? styleFlags | UNDERLINE : styleFlags & ~UNDERLINE, color); + } + + public TextStyle strikethrough(boolean value) { + return new TextStyle(font, value ? styleFlags | STRIKETHROUGH : styleFlags & ~STRIKETHROUGH, color); + } + + public TextStyle parseArguments(TextStyle original, String... args) { + if(args == null || args.length <= 0) return this; + ID font = font().font(); + boolean bold = font().bold(); + boolean italic = font().italic(); + boolean underline = underline(); + boolean strikethrough = strikethrough(); + int color = color(); + boolean hasColor = hasColor(); + float size = font().size(); + for(int i = 0,m=args.length;i builder.append(T.getAsString())); + LongObjectPair value = parse(location, provider, library); + if(value == null) return null; + return new FreeTypeInstance(style, value.getLongKey(), value.getValue(), oversample, xOff, yOff, shadowOffset, builder.toString()); + } + + private static LongObjectPair parse(ID location, IAssetProvider provider, long library) { + try(MemoryStack stack = MemoryStack.stackPush()) { + ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT); + PointerBuffer facePointer = stack.mallocPointer(1); + if(FreeTypeLibrary.parseError(FreeType.FT_New_Memory_Face(library, buffer, 0L, facePointer), "Creating Font Face")) { + MemoryUtil.memFree(buffer); + return null; + } + FT_Face face = FT_Face.create(facePointer.get()); + String s = FreeType.FT_Get_Font_Format(face); + if(!"TrueType".equals(s)) { + MemoryUtil.memFree(buffer); + throw new IllegalStateException("Font type ["+s+"] is not true type"); + } + if(FreeTypeLibrary.parseError(FreeType.FT_Select_Charmap(face, FreeType.FT_ENCODING_UNICODE), "Applying Unicode Encoding")) { + MemoryUtil.memFree(buffer); + return null; + } + return LongObjectPair.of(MemoryUtil.memAddress(buffer), face); + } + catch(Exception exception) { + exception.printStackTrace(); + return null; + } + } + + @Override + public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) { + FreeTypeInstance instance = this.instance[style & 0x3]; + return instance != null ? instance.gylthData(codepoint, size, oversample) : null; + } + + @Override + public void close() { + for(int i = 0;i<4;i++) { + if(instance[i] != null) { + instance[i].free(); + } + } + instance = null; + } + + public static class FreeTypeGlyth implements UnbakedGlyth { + final FT_Face face; + final int width; + final int height; + final float xOff; + final float yOff; + final float oversample; + private final float advance; + final int glyth; + final int glythCodepoint; + final long harfBuzzFont; + + public FreeTypeGlyth(FT_Face face, long harfBuzzFont, float xOff, float yOff, int width, int height, float advance, float oversample, int glyth, int glythCodepoint) { + this.face = face; + this.harfBuzzFont = harfBuzzFont; + this.width = width; + this.height = height; + this.oversample = oversample; + this.advance = advance / oversample; + this.xOff = xOff / oversample; + this.yOff = yOff / oversample; + this.glyth = glyth; + this.glythCodepoint = glythCodepoint; + } + + @Override + public float advance() { + return advance; + } + + @Override + public float kerning(int codepoint) { + int index = FreeType.FT_Get_Char_Index(face, codepoint); + if(index == 0) return 0; + StringBuilder builder = new StringBuilder(); + builder.append(Character.toChars(codepoint)); + builder.append(Character.toChars(glythCodepoint)); + float hbresult = (getAdvance(Character.toString(codepoint)) - getAdvance(builder.toString())) / 64F; + return hbresult; + } + + private float getAdvance(String adv) { + long id = HarfBuzz.hb_buffer_create(); + try { + HarfBuzz.hb_buffer_add_utf8(id, adv, 0, adv.length()); + HarfBuzz.hb_buffer_guess_segment_properties(id); + HarfBuzz.hb_shape(harfBuzzFont, id, null); + hb_glyph_position_t.Buffer positions = HarfBuzz.hb_buffer_get_glyph_positions(id); + float result = positions.hasRemaining() ? positions.x_advance() : 0F; + HarfBuzz.hb_buffer_destroy(positions.address()); + return result; + } + finally { + HarfBuzz.hb_buffer_destroy(id); + } + } + + @Override + public Glyth bake(GlythBaker baker) { + return baker.bake(new IGlythSheetInfo() { + @Override + public float xOffset() { return xOff; } + @Override + public float yOffset() { return yOff; } + @Override + public int width() { return width; } + @Override + public int height() { return height; } + @Override + public float oversample() { return oversample; } + @Override + public void upload(Texture texture, int x, int y) { + Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height); + if(drawable.drawFont(face, glyth)) { + drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height); + } + drawable.close(); + } + + @Override + public boolean isColored() { return false; } + }); + } + + } + + public static class FreeTypeInstance { + final int style; + long data; + final FT_Face face; + IntSet skip = new IntOpenHashSet(); + final float oversample; + final float xOff; + final float yOff; + final float shadowOffset; + final long harfBuzzFont; + + public FreeTypeInstance(int style, long data, FT_Face face, float oversample, float xOff, float yOff, float shadowOffset, String skip) { + this.style = style; + this.data = data; + this.face = face; + this.harfBuzzFont = HarfBuzz.hb_ft_font_create_referenced(face.address()); + skip.codePoints().forEach(this.skip::add); + this.oversample = oversample; + this.xOff = xOff; + this.yOff = yOff; + this.shadowOffset = shadowOffset; + try(MemoryStack stack = MemoryStack.stackPush()) { + FT_Vector ft_vector = FT_Vector.malloc(stack).set(Math.round(oversample * xOff * 64F), Math.round(oversample * -yOff * 64F)); + FreeType.FT_Set_Transform(face, null, ft_vector); + } + } + + public void free() { + if(data == 0L) return; + FreeType.FT_Done_Face(face); + MemoryUtil.nmemFree(data); + data = 0L; + } + + public UnbakedGlyth gylthData(int codepoint, float size, float oversample) { + if(skip.contains(codepoint)) return null; + int index = FreeType.FT_Get_Char_Index(face, codepoint); + if(index == 0) return null; + oversample *= this.oversample; + int pixels = Math.round(size * oversample); + if(FreeTypeLibrary.parseError(FreeType.FT_Set_Pixel_Sizes(face, 0, pixels), "Set Pixel Size")) return null; + if(FreeTypeLibrary.parseError(FreeType.FT_Load_Glyph(face, index, FreeType.FT_LOAD_NO_BITMAP | FreeType.FT_LOAD_BITMAP_METRICS_ONLY), "Loading Glyth")) return null; + FT_GlyphSlot slot = face.glyph(); + if(slot == null) { + System.out.println("Glyth didn't load for some reason"); + return null; + } + float advance = slot.advance().x() / 64F; + FT_Bitmap bitmap = slot.bitmap(); + + int left = slot.bitmap_left(); + int top = slot.bitmap_top(); + int width = bitmap.width(); + int height = bitmap.rows(); + if(width > 0 && height > 0) return new FreeTypeGlyth(face, harfBuzzFont, left, -top, width, height, advance, oversample, index, codepoint); + return new EmptyGlythData(advance / oversample); + } + } + + public static class FreeTypeLibrary { + private static long pointer = 0L; + + public static long get() { + if(pointer == 0L) { + try(MemoryStack stack = MemoryStack.stackPush()) { + PointerBuffer pointBuffer = stack.callocPointer(1); + int result = FreeType.FT_Init_FreeType(pointBuffer); + if(result != 0) { + throw new IllegalStateException(FreeType.FT_Error_String(result)); + } + pointer = pointBuffer.get(); + } + } + return pointer; + } + + public static boolean parseError(int result, String action) { + if(result == 0) return false; + String error = FreeType.FT_Error_String(result); + System.out.println("Couldn't do ["+action+"] because of "+error); + return true; + } + + public static void close() { + if(pointer == 0L) return; + FreeType.FT_Done_Library(pointer); + pointer = 0L; + } + } +} diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/IFontProvider.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/IFontProvider.java new file mode 100644 index 0000000..f8e1eca --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/IFontProvider.java @@ -0,0 +1,14 @@ +package speiger.src.coreengine.ui.gui.font.providers; + +import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth; + +public interface IFontProvider { + public static final int REGULAR = 0; + public static final int BOLD = 1; + public static final int ITALIC = 2; + public static final int ITALIC_BOLD = 3; + + + public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample); + public void close(); +} diff --git a/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/STBTrueTypeProvider.java b/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/STBTrueTypeProvider.java new file mode 100644 index 0000000..c905d65 --- /dev/null +++ b/src/gui/java/speiger/src/coreengine/ui/gui/font/providers/STBTrueTypeProvider.java @@ -0,0 +1,215 @@ +package speiger.src.coreengine.ui.gui.font.providers; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import org.lwjgl.stb.STBTTFontinfo; +import org.lwjgl.stb.STBTruetype; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import com.google.gson.JsonObject; + +import speiger.src.collections.ints.sets.IntOpenHashSet; +import speiger.src.collections.ints.sets.IntSet; +import speiger.src.coreengine.assets.api.IAssetProvider; +import speiger.src.coreengine.assets.api.ID; +import speiger.src.coreengine.assets.impl.parsers.Buffers; +import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice; +import speiger.src.coreengine.platform.graphics.api.texture.Texture; +import speiger.src.coreengine.platform.graphics.api.texture.drawable.Drawable; +import speiger.src.coreengine.ui.gui.font.FontTexture; +import speiger.src.coreengine.ui.gui.font.glyth.Glyth; +import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo; +import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth; +import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.EmptyGlythData; +import speiger.src.coreengine.utils.helpers.JsonUtil; + +public class STBTrueTypeProvider implements IFontProvider { + TrueTypeInstance[] instances; + + public STBTrueTypeProvider(TrueTypeInstance[] instances) { + this.instances = instances; + } + + public static IFontProvider create(ID location, IAssetProvider provider) { + try { + return create(provider.get(location).jsonObj(), provider); + } + catch(Exception e) { e.printStackTrace(); } + return null; + } + + public static IFontProvider create(JsonObject data, IAssetProvider provider) { + TrueTypeInstance[] instances = new TrueTypeInstance[4]; + instances[0] = create(0, data.getAsJsonObject("regular"), provider); + if(instances[0] == null) return null; + instances[1] = create(1, data.getAsJsonObject("bold"), provider); + instances[2] = create(2, data.getAsJsonObject("italic"), provider); + instances[3] = create(3, data.getAsJsonObject("bold_italic"), provider); + return new STBTrueTypeProvider(instances); + } + + private static TrueTypeInstance create(int style, JsonObject obj, IAssetProvider provider) { + if(obj == null || !obj.has("file")) return null; + ID location = ID.of(obj.get("file").getAsString()); + float oversample = JsonUtil.getOrDefault(obj, "oversample", 1F); + float shadowOffset = JsonUtil.getOrDefault(obj, "shadowOffset", 1F); + float xOff = 0; + float yOff = 0; + StringBuilder builder = new StringBuilder(); + JsonObject shift = obj.getAsJsonObject("offset"); + if(shift != null) { + xOff = JsonUtil.getOrDefault(shift, "x", 0); + yOff = JsonUtil.getOrDefault(shift, "y", 0); + } + JsonUtil.iterateValues(obj.get("skip"), T -> builder.append(T.getAsString())); + try { + ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT); + STBTTFontinfo info = STBTTFontinfo.create(); + if(!STBTruetype.stbtt_InitFont(info, buffer)) { + System.out.println("Couldn't load font"); + MemoryUtil.memFree(buffer); + info.free(); + return null; + } + return new TrueTypeInstance(style, MemoryUtil.memAddress(buffer), info, oversample, xOff, yOff, shadowOffset, builder.toString()); + } + catch(Exception e) { + e.printStackTrace(); + } + return null; + } + + public static class TrueTypeInstance { + final int style; + long data; + final STBTTFontinfo info; + IntSet skip = new IntOpenHashSet(); + final float oversample; + final float xOff; + final float yOff; + final float shadowOffset; + final float ascent; + + public TrueTypeInstance(int style, long data, STBTTFontinfo info, float oversample, float xOff, float yOff, float shadowOffset, String skip) { + this.style = style; + this.data = data; + this.info = info; + skip.codePoints().forEach(this.skip::add); + this.oversample = oversample; + this.xOff = xOff; + this.yOff = yOff; + this.shadowOffset = shadowOffset; + int[] ascent = new int[1]; + STBTruetype.stbtt_GetFontVMetrics(info, ascent, new int[1], new int[1]); + this.ascent = ascent[0]; + } + + public void free() { + if(data == 0L) return; + info.free(); + MemoryUtil.nmemFree(data); + data = 0L; + } + + public UnbakedGlyth glythData(int codepoint, float size, float oversample) { + if(skip.contains(codepoint)) return null; + int glyth = STBTruetype.nstbtt_FindGlyphIndex(info.address(), codepoint); + if(glyth == 0) return null; + oversample *= this.oversample; + float scale = STBTruetype.stbtt_ScaleForPixelHeight(info, size * oversample); + try(MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer left = stack.mallocInt(1); + IntBuffer bottom = stack.mallocInt(1); + IntBuffer right = stack.mallocInt(1); + IntBuffer top = stack.mallocInt(1); + IntBuffer advance = stack.mallocInt(1); + IntBuffer leftSideBearing = stack.mallocInt(1); + STBTruetype.stbtt_GetGlyphHMetrics(info, glyth, advance, leftSideBearing); + STBTruetype.stbtt_GetGlyphBitmapBoxSubpixel(info, glyth, scale, scale, xOff, yOff, left, bottom, right, top); + int minX = left.get(0); + int minY = -top.get(0); + int maxX = right.get(0); + int maxY = -bottom.get(0); + if(maxX - minX <= 0 || maxY - minY <= 0) return new EmptyGlythData(advance.get(0) * scale / oversample); + return new STBGlyth(this, minX, minY, maxX, maxY, advance.get(0), leftSideBearing.get(0), scale, oversample, glyth); + } + catch(Exception e) { + e.printStackTrace(); + return null; + } + } + } + + @Override + public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) { + TrueTypeInstance instance = instances[style & 0x3]; + return instance == null ? null : instance.glythData(codepoint, size, oversample); + } + + @Override + public void close() { + if(instances == null) return; + for(int i = 0;i<4;i++) { + if(instances[i] == null) continue; + instances[i].free(); + } + instances = null; + } + + private static class STBGlyth implements UnbakedGlyth { + final TrueTypeInstance owner; + final float xOffset; + final float yOffset; + final int width; + final int height; + final float oversample; + final float scale; + final float advance; + final int glyth; + + public STBGlyth(TrueTypeInstance owner, int minX, int minY, int maxX, int maxY, float advance, float leftPadding, float scale, float oversample, int glyth) { + this.owner = owner; + this.width = maxX - minX; + this.height = maxY - minY; + this.scale = scale; + this.oversample = oversample; + this.xOffset = ((leftPadding * scale) + minX + owner.xOff) / oversample; + this.yOffset = ((owner.ascent * scale) - maxY + owner.yOff) / oversample; + this.advance = advance * scale / oversample; + this.glyth = glyth; + } + + @Override + public float advance() { return advance; } + @Override + public float shadowOffset() { return owner.shadowOffset; } + @Override + public float kerning(int codepoint) { return STBTruetype.stbtt_GetCodepointKernAdvance(owner.info, codepoint, glyth) * scale / oversample; } + @Override + public Glyth bake(GlythBaker baker) { + return baker.bake(new IGlythSheetInfo() { + @Override + public float xOffset() { return xOffset; } + @Override + public float yOffset() { return yOffset; } + @Override + public int width() { return width; } + @Override + public int height() { return height; } + @Override + public float oversample() { return oversample; } + @Override + public boolean isColored() { return false; } + @Override + public void upload(Texture texture, int x, int y) { + Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height); + drawable.drawFont(owner.info, glyth, owner.xOff, owner.yOff, 0, 0, width, height, scale, scale); + drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height); + drawable.close(); + } + }); + } + } +}