Zum Inhalt

native/macos/KuroNativeSaver/Core/FontAtlas.swift

// FontAtlas — a monospace glyph atlas baked at runtime via CoreText into an
// r8Unorm coverage texture (no Xcode/metal compiler needed). CoreText + CoreGraphics
// only (no AppKit) so it also builds in the headless harness. The text pipeline
// tints coverage with the accent color, and because text is drawn into the HDR
// scene the CRT post (bloom/scanlines/glitch) treats it.
//
// A single narrow-cell grid (ASCII + katakana + symbols, half-width) plus one
// reserved solid-fill texel for rect fills.

import Metal
import CoreText
import CoreGraphics
import Foundation

final class FontAtlas {
    let texture: MTLTexture
    let cellW: Int, cellH: Int
    let atlasW: Int, atlasH: Int
    private let cols: Int
    private var index: [Character: Int] = [:]
    /// UV of a guaranteed fully-opaque texel (a baked solid cell) — for fillRect.
    let solidU: Float, solidV: Float

    init(device: MTLDevice, fontName: String = "Menlo", pointSize: CGFloat = 64) {
        // MAIN charset: printable ASCII + katakana + symbols (half-width).
        var chars: [Character] = (32...126).map { Character(UnicodeScalar($0)!) }
        let extra = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンー" +
                    "›↳«»█✛●○◢◣"
        chars += Array(extra)
        var seen = Set<Character>(); chars = chars.filter { seen.insert($0).inserted }

        let font = CTFontCreateWithName(fontName as CFString, pointSize, nil)
        let ascent = CTFontGetAscent(font), descent = CTFontGetDescent(font), leading = CTFontGetLeading(font)
        cellH = Int(ceil(ascent + descent + leading)) + 2
        var glyph = CGGlyph(0); var uni = UniChar(77) // 'M'
        CTFontGetGlyphsForCharacters(font, &uni, &glyph, 1)
        var adv = CGSize.zero; CTFontGetAdvancesForGlyphs(font, .horizontal, &glyph, &adv, 1)
        cellW = Int(ceil(adv.width)) + 2

        // Single grid (+1 cell reserved for the solid-fill texel).
        cols = Int(ceil(Double(chars.count + 1).squareRoot()))
        let mainRows = Int(ceil(Double(chars.count + 1) / Double(cols)))
        atlasW = cols * cellW; atlasH = mainRows * cellH

        let cs = CGColorSpaceCreateDeviceGray()
        let ctx = CGContext(data: nil, width: atlasW, height: atlasH, bitsPerComponent: 8,
                            bytesPerRow: atlasW, space: cs, bitmapInfo: CGImageAlphaInfo.none.rawValue)!
        ctx.setFillColor(gray: 0, alpha: 1); ctx.fill(CGRect(x: 0, y: 0, width: atlasW, height: atlasH))
        let white = CGColor(colorSpace: cs, components: [1, 1])!
        let attrs: [CFString: Any] = [kCTFontAttributeName: font, kCTForegroundColorAttributeName: white]

        // --- main glyphs (narrow cells, top region) ---
        for (i, c) in chars.enumerated() {
            index[c] = i
            let col = i % cols, rowTop = i / cols
            let cellBottom = atlasH - (rowTop + 1) * cellH      // bottom-up CGContext
            let astr = CFAttributedStringCreate(nil, String(c) as CFString, attrs as CFDictionary)!
            let line = CTLineCreateWithAttributedString(astr)
            ctx.textPosition = CGPoint(x: CGFloat(col * cellW) + 1, y: CGFloat(cellBottom) + descent + 1)
            CTLineDraw(line, ctx)
        }

        // --- solid-fill texel (reserved cell in the main grid) ---
        let si = chars.count, sc = si % cols, sr = si / cols
        ctx.setFillColor(gray: 1, alpha: 1)
        ctx.fill(CGRect(x: sc * cellW, y: atlasH - (sr + 1) * cellH, width: cellW, height: cellH))
        solidU = (Float(sc * cellW) + Float(cellW) * 0.5) / Float(atlasW)
        solidV = (Float(sr * cellH) + Float(cellH) * 0.5) / Float(atlasH)

        let td = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: atlasW, height: atlasH, mipmapped: false)
        td.usage = [.shaderRead]
        texture = device.makeTexture(descriptor: td)!
        texture.replace(region: MTLRegionMake2D(0, 0, atlasW, atlasH), mipmapLevel: 0,
                        withBytes: ctx.data!, bytesPerRow: atlasW)
    }

    /// Atlas UV rect for a (narrow) character. CGBitmapContext memory is TOP-DOWN
    /// (row 0 = image top); vTop is the glyph's visual top (smaller v).
    func uvRect(_ c: Character) -> (uLo: Float, uHi: Float, vTop: Float, vBottom: Float)? {
        guard let i = index[c] else { return nil }
        let col = i % cols, rowTop = i / cols
        let uLo = Float(col * cellW) / Float(atlasW)
        let uHi = Float((col + 1) * cellW) / Float(atlasW)
        let vTop = Float(rowTop * cellH) / Float(atlasH)
        let vBottom = Float((rowTop + 1) * cellH) / Float(atlasH)
        return (uLo, uHi, vTop, vBottom)
    }

    var cellAspect: Float { Float(cellW) / Float(cellH) }
}