mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-13 22:46:07 +09:00
justified typesetting wip
This commit is contained in:
303
src/net/torvald/terrarumsansbitmap/MovableType.kt
Normal file
303
src/net/torvald/terrarumsansbitmap/MovableType.kt
Normal file
@@ -0,0 +1,303 @@
|
||||
package net.torvald.terrarumsansbitmap
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.Batch
|
||||
import com.badlogic.gdx.utils.Disposable
|
||||
import net.torvald.terrarumsansbitmap.gdx.CodepointSequence
|
||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap
|
||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.getHash
|
||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.TextCacheObj
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2024-03-24.
|
||||
*/
|
||||
class MovableType(
|
||||
val font: TerrarumSansBitmap,
|
||||
val inputText: CodepointSequence,
|
||||
val width: Int,
|
||||
internal val isNull: Boolean = false
|
||||
): Disposable {
|
||||
|
||||
var height = 0; private set
|
||||
internal val hash: Long = inputText.getHash()
|
||||
private var disposed = false
|
||||
private val lines = ArrayList<List<Block>>()
|
||||
|
||||
override fun dispose() {
|
||||
if (!disposed) {
|
||||
disposed = true
|
||||
lines.forEach {
|
||||
it.forEach {
|
||||
it.block.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// perform typesetting
|
||||
init { if (inputText.isNotEmpty() && !isNull) {
|
||||
|
||||
if (width < 100) throw IllegalArgumentException("Width too narrow; width must be at least 100 pixels (got $width)")
|
||||
|
||||
val inputCharSeqsTokenised = inputText.tokenise()
|
||||
val inputWords = inputCharSeqsTokenised.map {
|
||||
val seq = if (it.isEmpty())
|
||||
CodepointSequence(listOf(0x20))
|
||||
else
|
||||
it
|
||||
|
||||
seq.add(0, 0)
|
||||
seq.add(0)
|
||||
|
||||
font.createTextCache(CodepointSequence(seq))
|
||||
}
|
||||
// list of [ word, word, \n, word, word, word, ... ]
|
||||
|
||||
println("Length of input text: ${inputText.size}")
|
||||
println("Token size: ${inputCharSeqsTokenised.size}")
|
||||
println("Paper width: $width")
|
||||
|
||||
var currentLine = ArrayList<Block>()
|
||||
var wordCount = 0
|
||||
|
||||
fun dequeue() {
|
||||
wordCount += 1
|
||||
}
|
||||
|
||||
fun flush() {
|
||||
// println("\n Anchors [$wordCount] =${" ".repeat(if (wordCount < 10) 3 else if (wordCount < 100) 2 else 1)}${currentLine.map { it.posX }.joinToString()}\n")
|
||||
|
||||
// flush the line
|
||||
lines.add(currentLine)
|
||||
currentLine = ArrayList()
|
||||
}
|
||||
|
||||
fun justifyAndFlush(lineWidthNow: Int, thisWordObj: TextCacheObj, thisWord: TerrarumSansBitmap.Companion.ShittyGlyphLayout) {
|
||||
val thislineEndsWithHangable = hangable.contains(currentLine.last().block.glyphLayout!!.textBuffer.penultimate())
|
||||
val nextWordEndsWithHangable = hangable.contains(thisWordObj.glyphLayout!!.textBuffer.penultimate())
|
||||
|
||||
val scoreForWidening = (width - (lineWidthNow - if (thislineEndsWithHangable) hangWidth else 0)).toFloat()
|
||||
val thisWordWidth = thisWord.width - if (nextWordEndsWithHangable) hangWidth else 0
|
||||
val scoreForAddingWordThenTightening0 = lineWidthNow + spaceWidth + thisWordWidth - width
|
||||
val scoreForAddingWordThenTightening = penaliseTightening(scoreForAddingWordThenTightening0)
|
||||
|
||||
// if adding word and contracting is better (has LOWER score), add the word
|
||||
if (scoreForAddingWordThenTightening < scoreForWidening) {
|
||||
currentLine.add(Block(lineWidthNow + spaceWidth, thisWordObj))
|
||||
// remove this word from the list of future words
|
||||
dequeue()
|
||||
}
|
||||
|
||||
val numberOfWords = currentLine.size
|
||||
|
||||
// continue with the widening/contraction
|
||||
val moveDeltas = IntArray(numberOfWords)
|
||||
val finalScore = minOf(scoreForWidening, scoreForAddingWordThenTightening0.toFloat())
|
||||
if (numberOfWords > 1) {
|
||||
val moveAmountsByWord = coalesceIndices(sortWordsByPriority(currentLine, round(finalScore).toInt()))
|
||||
for (i in 1 until moveDeltas.size) {
|
||||
moveDeltas[i] = moveDeltas[i - 1] + moveAmountsByWord.getOrElse(i) { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// widen: 1, tighten: -1
|
||||
val operation = if (scoreForWidening == 0f && scoreForAddingWordThenTightening == 0f) 0
|
||||
else if (scoreForWidening < scoreForAddingWordThenTightening) 1
|
||||
else -1
|
||||
|
||||
val widthOld = currentLine.last().let { it.posX + it.block.glyphLayout!!.width }
|
||||
|
||||
val anchorsOld = currentLine.map { it.posX }
|
||||
|
||||
// apply the operation
|
||||
moveDeltas.forEachIndexed { index, it ->
|
||||
val delta = operation * it
|
||||
currentLine[index].posX += delta
|
||||
}
|
||||
|
||||
val anchorsNew = currentLine.map { it.posX }
|
||||
|
||||
val widthNew = currentLine.last().let { it.posX + it.block.glyphLayout!!.width }
|
||||
|
||||
val lineHeader = "Strategy [L ${lines.size}]: "
|
||||
val lineHeader2 = " ".repeat(lineHeader.length)
|
||||
println(lineHeader + (if (operation == 0) "Nop" else if (operation == 1) "Widen" else "Tighten") +
|
||||
" (W $scoreForWidening, T $scoreForAddingWordThenTightening; $finalScore), " +
|
||||
"width: $widthOld -> $widthNew, wordCount: $numberOfWords, " +
|
||||
"thislineEndsWithHangable: $thislineEndsWithHangable, nextWordEndsWithHangable: $nextWordEndsWithHangable")
|
||||
println(lineHeader2 + "moveDelta: ${moveDeltas.map { it * operation }} (${moveDeltas.size})")
|
||||
println(lineHeader2 + "anchors old: $anchorsOld (${anchorsOld.size})")
|
||||
println(lineHeader2 + "anchors new: $anchorsNew (${anchorsNew.size})")
|
||||
println()
|
||||
|
||||
// flush the line
|
||||
flush()
|
||||
}
|
||||
|
||||
var thisWordObj = inputWords[wordCount]
|
||||
var thisWord = thisWordObj.glyphLayout!!
|
||||
var thisWordStr = thisWord.textBuffer // ALWAYS starts and ends with \0
|
||||
var lineWidthNow = if (currentLine.isEmpty()) -spaceWidth else currentLine.last().let { it.posX + it.block.glyphLayout!!.width }
|
||||
while (wordCount < inputWords.size) {
|
||||
thisWordObj = inputWords[wordCount]
|
||||
thisWord = thisWordObj.glyphLayout!!
|
||||
thisWordStr = thisWord.textBuffer // ALWAYS starts and ends with \0
|
||||
lineWidthNow = if (currentLine.isEmpty()) -spaceWidth else currentLine.last().let { it.posX + it.block.glyphLayout!!.width }
|
||||
|
||||
// println("Processing word [$wordCount] ${thisWordStr.joinToString("") { Character.toString(it.toChar()) }} ; \t\t${thisWordStr.joinToString(" ") { it.toHex() }}")
|
||||
|
||||
// if the word is \n
|
||||
if (thisWordStr.size == 3 && thisWordStr[1] == 0x0A) {
|
||||
println("Strategy [L ${lines.size}]: (line is shorter than the paper width)")
|
||||
|
||||
// flush the line
|
||||
flush()
|
||||
|
||||
// remove the word from the list of future words
|
||||
dequeue()
|
||||
}
|
||||
// decide if it should add last word and make newline, or make newline then add the word
|
||||
// would adding the current word would cause line overflow?
|
||||
else if (lineWidthNow + spaceWidth + thisWord.width >= width) {
|
||||
justifyAndFlush(lineWidthNow, thisWordObj, thisWord)
|
||||
}
|
||||
// typeset the text normally
|
||||
else {
|
||||
currentLine.add(Block(lineWidthNow + spaceWidth, thisWordObj))
|
||||
|
||||
// remove the word from the list of future words
|
||||
dequeue()
|
||||
}
|
||||
} // end while
|
||||
|
||||
println("Strategy [L ${lines.size}]: (end of the text)")
|
||||
flush()
|
||||
|
||||
|
||||
height = lines.size
|
||||
} }
|
||||
|
||||
fun draw(batch: Batch, x: Int, y: Int, lineStart: Int = 0, linesToDraw: Int = -1, lineHeight: Int = 24) =
|
||||
draw(batch, x.toFloat(), y.toFloat(), lineStart, linesToDraw, lineHeight)
|
||||
|
||||
fun draw(batch: Batch, x: Float, y: Float, lineStart: Int = 0, linesToDraw: Int = 2147483647, lineHeight: Int = 24) {
|
||||
if (isNull) return
|
||||
|
||||
lines.subList(lineStart, minOf(lines.size, lineStart + linesToDraw)).forEachIndexed { lineNum, lineBlocks ->
|
||||
// println("Line [${lineNum+1}] anchors: "+ lineBlocks.map { it.posX }.joinToString())
|
||||
|
||||
lineBlocks.forEach {
|
||||
batch.draw(it.block.glyphLayout!!.linotype, x + it.posX - 16, y + lineNum * lineHeight)
|
||||
}
|
||||
|
||||
// font.draw(batch, "I", x, y + lineNum * lineHeight + 14)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Block(var posX: Int, val block: TextCacheObj) // a single word
|
||||
|
||||
companion object {
|
||||
private val periods = listOf(0x2E, 0x3A, 0x21, 0x3F, 0x2026, 0x3002).toHashSet()
|
||||
private val quots = listOf(0x22, 0x27, 0xAB, 0xBB, 0x2018, 0x2019, 0x201A, 0x201B, 0x201C, 0x201D, 0x201E, 0x201F, 0x2039, 0x203A).toHashSet()
|
||||
private val commas = listOf(0x2C, 0x3B, 0x3001).toHashSet()
|
||||
private val hangable = listOf(0x2E, 0x2C).toHashSet()
|
||||
private val spaceWidth = 5
|
||||
private val hangWidth = 6
|
||||
|
||||
private fun Int.toHex() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}"
|
||||
|
||||
/**
|
||||
* @return indices of blocks in the `currentLine`
|
||||
*/
|
||||
private fun sortWordsByPriority(currentLine: List<Block>, length: Int): List<Int> {
|
||||
// priority:
|
||||
// 1. words ending with period/colon/!/?/ellipses
|
||||
// 2. words ending or starting with quotation marks or <>s
|
||||
// 3. words ending with comma or semicolon
|
||||
// 4. words
|
||||
|
||||
val ret = ArrayList<Int>()
|
||||
|
||||
while (ret.size < length) {
|
||||
// give "score" then sort by it to give both priority and randomisation
|
||||
val sackOfIndices = (1 until currentLine.size).map {
|
||||
val thisWord = currentLine[it].block.glyphLayout!!.textBuffer
|
||||
|
||||
// println(" Index word [$it/$length]: ${thisWord.joinToString("") { Character.toString(it.toChar()) }} ; \t\t${thisWord.joinToString(" ") { it.toHex() }}")
|
||||
|
||||
val thisWordEnd = thisWord[thisWord.lastIndex]
|
||||
val thisWordFirst = thisWord[0]
|
||||
|
||||
val priority = if (periods.contains(thisWordEnd))
|
||||
1
|
||||
else if (quots.contains(thisWordEnd) or quots.contains(thisWordFirst))
|
||||
2
|
||||
else if (commas.contains(thisWordEnd))
|
||||
3
|
||||
else
|
||||
255
|
||||
|
||||
it to (Math.random() * 65535).toInt().or(priority.shl(16))
|
||||
}.sortedBy { it.second }.map { it.first }
|
||||
|
||||
ret.addAll(sackOfIndices)
|
||||
}
|
||||
|
||||
if (ret.isEmpty()) return emptyList()
|
||||
return ret.toList().subList(0, length)
|
||||
}
|
||||
|
||||
// return: [ job count for 0th word, job count for 1st word, job count for 2nd word, ... ]
|
||||
private fun coalesceIndices(listOfJobs: List<Int>): IntArray {
|
||||
if (listOfJobs.isEmpty()) return IntArray(0)
|
||||
|
||||
// println(" sample: ${listOfJobs.joinToString()}")
|
||||
|
||||
val ret = IntArray(listOfJobs.maxOrNull()!! + 1)
|
||||
listOfJobs.forEach {
|
||||
ret[it] += 1
|
||||
}
|
||||
|
||||
// println(" ret: ${ret.joinToString()}")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun CodepointSequence.tokenise(): MutableList<CodepointSequence> {
|
||||
val tokens = mutableListOf<CodepointSequence>()
|
||||
var currentToken = mutableListOf<Int>()
|
||||
|
||||
this.forEach {
|
||||
if (it == 0x20 || it == 0x0A) {
|
||||
tokens.add(CodepointSequence(currentToken))
|
||||
if (it != 0x20)
|
||||
tokens.add(CodepointSequence(listOf(it)))
|
||||
currentToken = mutableListOf()
|
||||
}
|
||||
else {
|
||||
currentToken.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last token if it's not empty
|
||||
if (currentToken.isNotEmpty()) {
|
||||
tokens.add(CodepointSequence(currentToken))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
private fun <E> java.util.ArrayList<E>.penultimate(): E {
|
||||
return this[this.size - 2]
|
||||
}
|
||||
|
||||
private fun penaliseTightening(score: Int): Float = if (score < 0f)
|
||||
-(-score).toFloat().pow(1.05f)
|
||||
else
|
||||
score.toFloat().pow(1.05f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.badlogic.gdx.graphics.g2d.*
|
||||
import com.badlogic.gdx.utils.GdxRuntimeException
|
||||
import net.torvald.terrarumsansbitmap.DiacriticsAnchor
|
||||
import net.torvald.terrarumsansbitmap.GlyphProps
|
||||
import net.torvald.terrarumsansbitmap.MovableType
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
@@ -122,13 +123,14 @@ class TerrarumSansBitmap(
|
||||
private var textCacheCap = 0
|
||||
private val textCache = HashMap<Long, TextCacheObj>(textCacheSize * 2)
|
||||
|
||||
private var typesetCacheCap = 0
|
||||
private val typesetCache = HashMap<Long, MovableType>(textCacheSize)
|
||||
|
||||
/**
|
||||
* Insertion sorts the last element fo the textCache
|
||||
*/
|
||||
private fun addToCache(text: CodepointSequence, linotype: Texture, width: Int) {
|
||||
val cacheObj = TextCacheObj(text.getHash(), ShittyGlyphLayout(text, linotype, width))
|
||||
|
||||
if (textCacheCap < textCacheSize) {
|
||||
private fun addToCache(cacheObj: TextCacheObj) {
|
||||
if (textCacheCap < textCacheSize * 2) {
|
||||
textCache[cacheObj.hash] = cacheObj
|
||||
textCacheCap += 1
|
||||
}
|
||||
@@ -141,10 +143,28 @@ class TerrarumSansBitmap(
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToTypesetCache(cacheObj: MovableType) {
|
||||
if (typesetCacheCap < textCacheSize) {
|
||||
typesetCache[cacheObj.hash] = cacheObj
|
||||
typesetCacheCap += 1
|
||||
}
|
||||
else {
|
||||
// randomly eliminate one
|
||||
typesetCache.remove(typesetCache.keys.random())!!.dispose()
|
||||
|
||||
// add new one
|
||||
typesetCache[cacheObj.hash] = cacheObj
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCache(hash: Long): TextCacheObj? {
|
||||
return textCache[hash]
|
||||
}
|
||||
|
||||
private fun getTypesetCache(hash: Long): MovableType? {
|
||||
return typesetCache[hash]
|
||||
}
|
||||
|
||||
|
||||
private fun getColour(codePoint: Int): Int { // input: 0x10F_RGB, out: RGBA8888
|
||||
if (colourBuffer.containsKey(codePoint))
|
||||
@@ -244,6 +264,7 @@ class TerrarumSansBitmap(
|
||||
|
||||
if (isVariable) buildWidthTable(pixmap, codeRange[index], if (isExtraWide) 32 else 16)
|
||||
buildWidthTableFixed()
|
||||
buildWidthTableInternal()
|
||||
|
||||
|
||||
/*if (!noShadow) {
|
||||
@@ -333,141 +354,24 @@ class TerrarumSansBitmap(
|
||||
val charSeqNotBlank = codepoints.size > 0 // determine emptiness BEFORE you hack a null chars in
|
||||
val newCodepoints = codepoints
|
||||
|
||||
fun Int.flipY() = this * if (flipY) 1 else -1
|
||||
|
||||
// always draw at integer position; this is bitmap font after all
|
||||
val x = Math.round(x)
|
||||
val y = Math.round(y + (lineHeight - 20 * scale) / 2)
|
||||
|
||||
val charSeqHash = newCodepoints.getHash()
|
||||
|
||||
var renderCol = -1 // subject to change with the colour code
|
||||
|
||||
if (charSeqNotBlank) {
|
||||
|
||||
val cacheObj = getCache(charSeqHash)
|
||||
var cacheObj = getCache(charSeqHash)
|
||||
|
||||
if (cacheObj == null || flagFirstRun) {
|
||||
textBuffer = newCodepoints
|
||||
|
||||
val posmap = buildPosMap(textBuffer)
|
||||
|
||||
flagFirstRun = false
|
||||
|
||||
//dbgprn("text not in buffer: $charSeq")
|
||||
|
||||
|
||||
//textBuffer.forEach { print("${it.toHex()} ") }
|
||||
//dbgprn()
|
||||
|
||||
|
||||
// resetHash(charSeq, x.toFloat(), y.toFloat())
|
||||
|
||||
val textWidth = posmap.width
|
||||
val _pw = textWidth + (linotypePaddingX * 2)
|
||||
val _ph = H + (linotypePaddingY * 2)
|
||||
if (_pw < 0 || _ph < 0) throw RuntimeException("Illegal linotype dimension (w: $_pw, h: $_ph)")
|
||||
val linotypePixmap = Pixmap(_pw, _ph, Pixmap.Format.RGBA8888)
|
||||
|
||||
|
||||
var index = 0
|
||||
while (index <= textBuffer.lastIndex) {
|
||||
try {
|
||||
var c = textBuffer[index]
|
||||
val sheetID = getSheetType(c)
|
||||
|
||||
val (sheetX, sheetY) =
|
||||
if (index == 0) getSheetwisePosition(0, c)
|
||||
else getSheetwisePosition(textBuffer[index - 1], c)
|
||||
val hash = getHash(c) // to be used with Bad Transmission Modifier
|
||||
|
||||
if (isColourCode(c)) {
|
||||
if (c == 0x100000) {
|
||||
renderCol = -1
|
||||
}
|
||||
else {
|
||||
renderCol = getColour(c)
|
||||
}
|
||||
}
|
||||
else if (sheetID == SHEET_HANGUL) {
|
||||
// Flookahead for {I, P, F}
|
||||
|
||||
val cNext = if (index + 1 < textBuffer.size) textBuffer[index + 1] else 0
|
||||
val cNextNext = if (index + 2 < textBuffer.size) textBuffer[index + 2] else 0
|
||||
|
||||
val hangulLength = if (isHangulJongseong(cNextNext) && isHangulJungseong(cNext))
|
||||
3
|
||||
else if (isHangulJungseong(cNext))
|
||||
2
|
||||
else
|
||||
1
|
||||
|
||||
val (indices, rows) = toHangulIndexAndRow(c, cNext, cNextNext)
|
||||
|
||||
val (indexCho, indexJung, indexJong) = indices
|
||||
val (choRow, jungRow, jongRow) = rows
|
||||
val hangulSheet = sheets[SHEET_HANGUL]
|
||||
|
||||
|
||||
|
||||
val choTex = hangulSheet.get(indexCho, choRow)
|
||||
val jungTex = hangulSheet.get(indexJung, jungRow)
|
||||
val jongTex = hangulSheet.get(indexJong, jongRow)
|
||||
|
||||
linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
|
||||
|
||||
index += hangulLength - 1
|
||||
|
||||
}
|
||||
else {
|
||||
try {
|
||||
val posY = posmap.y[index].flipY() +
|
||||
if (sheetID == SHEET_UNIHAN) // evil exceptions
|
||||
offsetUnihan
|
||||
else if (sheetID == SHEET_CUSTOM_SYM)
|
||||
offsetCustomSym
|
||||
else 0
|
||||
|
||||
val posX = posmap.x[index]
|
||||
val texture = sheets[sheetID].get(sheetX, sheetY)
|
||||
|
||||
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
|
||||
|
||||
|
||||
}
|
||||
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
index++
|
||||
}
|
||||
catch (e: NullPointerException) {
|
||||
System.err.println("Shit hit the multithreaded fan")
|
||||
e.printStackTrace()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
makeShadow(linotypePixmap)
|
||||
|
||||
tempLinotype = Texture(linotypePixmap)
|
||||
tempLinotype.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest)
|
||||
|
||||
// put things into cache
|
||||
//textCache[charSeq] = ShittyGlyphLayout(textBuffer, linotype!!)
|
||||
addToCache(textBuffer, tempLinotype, textWidth)
|
||||
linotypePixmap.dispose()
|
||||
}
|
||||
else {
|
||||
textBuffer = cacheObj.glyphLayout!!.textBuffer
|
||||
tempLinotype = cacheObj.glyphLayout!!.linotype
|
||||
cacheObj = createTextCache(newCodepoints)
|
||||
addToCache(cacheObj)
|
||||
}
|
||||
|
||||
textBuffer = cacheObj.glyphLayout!!.textBuffer
|
||||
tempLinotype = cacheObj.glyphLayout!!.linotype
|
||||
|
||||
batch.draw(tempLinotype,
|
||||
(x - linotypePaddingX).toFloat(),
|
||||
(y - linotypePaddingY).toFloat() + (if (flipY) (tempLinotype.height) else 0) * scale,
|
||||
@@ -479,6 +383,169 @@ class TerrarumSansBitmap(
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun createTextCache(newCodepoints: CodepointSequence): TextCacheObj {
|
||||
fun Int.flipY() = this * if (flipY) 1 else -1
|
||||
|
||||
var renderCol = -1 // subject to change with the colour code
|
||||
|
||||
val textBuffer = newCodepoints
|
||||
|
||||
val posmap = buildPosMap(textBuffer)
|
||||
|
||||
flagFirstRun = false
|
||||
|
||||
//dbgprn("text not in buffer: $charSeq")
|
||||
|
||||
|
||||
//textBuffer.forEach { print("${it.toHex()} ") }
|
||||
//dbgprn()
|
||||
|
||||
|
||||
// resetHash(charSeq, x.toFloat(), y.toFloat())
|
||||
|
||||
val textWidth = posmap.width
|
||||
val _pw = textWidth + (linotypePaddingX * 2)
|
||||
val _ph = H + (linotypePaddingY * 2)
|
||||
if (_pw < 0 || _ph < 0) throw RuntimeException("Illegal linotype dimension (w: $_pw, h: $_ph)")
|
||||
val linotypePixmap = Pixmap(_pw, _ph, Pixmap.Format.RGBA8888)
|
||||
|
||||
|
||||
var index = 0
|
||||
while (index <= textBuffer.lastIndex) {
|
||||
try {
|
||||
var c = textBuffer[index]
|
||||
val sheetID = getSheetType(c)
|
||||
|
||||
val (sheetX, sheetY) =
|
||||
if (index == 0) getSheetwisePosition(0, c)
|
||||
else getSheetwisePosition(textBuffer[index - 1], c)
|
||||
val hash = getHash(c) // to be used with Bad Transmission Modifier
|
||||
|
||||
if (isColourCode(c)) {
|
||||
if (c == 0x100000) {
|
||||
renderCol = -1
|
||||
}
|
||||
else {
|
||||
renderCol = getColour(c)
|
||||
}
|
||||
}
|
||||
else if (sheetID == SHEET_HANGUL) {
|
||||
// Flookahead for {I, P, F}
|
||||
|
||||
val cNext = if (index + 1 < textBuffer.size) textBuffer[index + 1] else 0
|
||||
val cNextNext = if (index + 2 < textBuffer.size) textBuffer[index + 2] else 0
|
||||
|
||||
val hangulLength = if (isHangulJongseong(cNextNext) && isHangulJungseong(cNext))
|
||||
3
|
||||
else if (isHangulJungseong(cNext))
|
||||
2
|
||||
else
|
||||
1
|
||||
|
||||
val (indices, rows) = toHangulIndexAndRow(c, cNext, cNextNext)
|
||||
|
||||
val (indexCho, indexJung, indexJong) = indices
|
||||
val (choRow, jungRow, jongRow) = rows
|
||||
val hangulSheet = sheets[SHEET_HANGUL]
|
||||
|
||||
|
||||
|
||||
val choTex = hangulSheet.get(indexCho, choRow)
|
||||
val jungTex = hangulSheet.get(indexJung, jungRow)
|
||||
val jongTex = hangulSheet.get(indexJong, jongRow)
|
||||
|
||||
linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||||
|
||||
|
||||
index += hangulLength - 1
|
||||
|
||||
}
|
||||
else {
|
||||
try {
|
||||
val posY = posmap.y[index].flipY() +
|
||||
if (sheetID == SHEET_UNIHAN) // evil exceptions
|
||||
offsetUnihan
|
||||
else if (sheetID == SHEET_CUSTOM_SYM)
|
||||
offsetCustomSym
|
||||
else 0
|
||||
|
||||
val posX = posmap.x[index]
|
||||
val texture = sheets[sheetID].get(sheetX, sheetY)
|
||||
|
||||
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
|
||||
|
||||
|
||||
}
|
||||
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
index++
|
||||
}
|
||||
catch (e: NullPointerException) {
|
||||
System.err.println("Shit hit the multithreaded fan")
|
||||
e.printStackTrace()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
makeShadow(linotypePixmap)
|
||||
|
||||
val tempLinotype = Texture(linotypePixmap)
|
||||
tempLinotype.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest)
|
||||
|
||||
// make cache object
|
||||
val cacheObj = TextCacheObj(textBuffer.getHash(), ShittyGlyphLayout(textBuffer, tempLinotype, textWidth))
|
||||
linotypePixmap.dispose()
|
||||
|
||||
return cacheObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||||
*/
|
||||
fun typesetParagraph(batch: Batch, charSeq: CharSequence, targetWidth: Int): MovableType =
|
||||
typesetParagraphNormalised(batch, charSeq.toCodePoints(2), targetWidth.toFloat())
|
||||
/**
|
||||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||||
*/
|
||||
fun typesetParagraph(batch: Batch, charSeq: CharSequence, targetWidth: Float): MovableType =
|
||||
typesetParagraphNormalised(batch, charSeq.toCodePoints(2), targetWidth)
|
||||
|
||||
|
||||
private val nullType = MovableType(this, "".toCodePoints(2), 0, true)
|
||||
|
||||
/**
|
||||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||||
*/
|
||||
fun typesetParagraphNormalised(batch: Batch, codepoints: CodepointSequence, targetWidth: Float): MovableType {
|
||||
val charSeqNotBlank = codepoints.size > 0 // determine emptiness BEFORE you hack a null chars in
|
||||
val newCodepoints = codepoints
|
||||
|
||||
val charSeqHash = newCodepoints.getHash()
|
||||
|
||||
if (charSeqNotBlank) {
|
||||
var cacheObj = getTypesetCache(charSeqHash)
|
||||
|
||||
if (cacheObj == null || flagFirstRun) {
|
||||
cacheObj = MovableType(this, codepoints, targetWidth.toInt())
|
||||
addToTypesetCache(cacheObj)
|
||||
}
|
||||
|
||||
return cacheObj
|
||||
}
|
||||
else {
|
||||
return nullType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
@@ -681,7 +748,12 @@ class TerrarumSansBitmap(
|
||||
|
||||
}
|
||||
|
||||
private val glyphLayout = GlyphLayout()
|
||||
private fun buildWidthTableInternal() {
|
||||
for (i in 0 until 16) {
|
||||
glyphProps[MOVABLE_BLOCK_1 + i] = GlyphProps(i + 1)
|
||||
glyphProps[MOVABLE_BLOCK_M1 + i] = GlyphProps(-i - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWidth(text: String) = getWidthNormalised(text.toCodePoints())
|
||||
fun getWidth(s: CodepointSequence) = getWidthNormalised(s.normalise())
|
||||
@@ -946,7 +1018,10 @@ class TerrarumSansBitmap(
|
||||
}
|
||||
|
||||
// basically an Unicode NFD with some additional flavours
|
||||
private fun CodepointSequence.normalise(): CodepointSequence {
|
||||
/**
|
||||
* @param normaliseOption 1-full, 2-omit null filling
|
||||
*/
|
||||
private fun CodepointSequence.normalise(normaliseOption: Int = 1): CodepointSequence {
|
||||
val seq0 = CodepointSequence()
|
||||
val seq = CodepointSequence()
|
||||
val seq2 = CodepointSequence()
|
||||
@@ -1466,7 +1541,13 @@ class TerrarumSansBitmap(
|
||||
|
||||
// println("seq5 = " + seq5.joinToString(" ") { it.toCh() })
|
||||
|
||||
return seq5
|
||||
if (normaliseOption == 2) {
|
||||
while (seq5.remove(0)) {}
|
||||
return seq5
|
||||
}
|
||||
else {
|
||||
return seq5
|
||||
}
|
||||
}
|
||||
|
||||
private fun dbgprnLig(i: Any) { if (false) println("[${this.javaClass.simpleName}] $i") }
|
||||
@@ -1511,11 +1592,17 @@ class TerrarumSansBitmap(
|
||||
* Note: CharSequence IS a String. java.lang.String implements CharSequence.
|
||||
*
|
||||
* Note to Programmer: DO NOT USE CHAR LITERALS, CODE EDITORS WILL CHANGE IT TO SOMETHING ELSE !!
|
||||
*
|
||||
* @param normaliseOption 0-don't, 1-full, 2-omit null filling
|
||||
*/
|
||||
private fun CharSequence.toCodePoints(): CodepointSequence {
|
||||
private fun CharSequence.toCodePoints(normaliseOption: Int = 1): CodepointSequence {
|
||||
val seq = CodepointSequence()
|
||||
this.forEach { seq.add(it.toInt()) }
|
||||
return seq.normalise()
|
||||
|
||||
return when (normaliseOption) {
|
||||
0 -> seq
|
||||
else -> seq.normalise(normaliseOption)
|
||||
}
|
||||
}
|
||||
|
||||
private fun surrogatesToCodepoint(var0: Int, var1: Int): Int {
|
||||
@@ -1709,26 +1796,6 @@ class TerrarumSansBitmap(
|
||||
}
|
||||
|
||||
|
||||
fun CodepointSequence.getHash(): Long {
|
||||
val hashBasis = -3750763034362895579L
|
||||
val hashPrime = 1099511628211L
|
||||
var hashAccumulator = hashBasis
|
||||
|
||||
try {
|
||||
this.forEach {
|
||||
hashAccumulator = hashAccumulator xor it.toLong()
|
||||
hashAccumulator *= hashPrime
|
||||
}
|
||||
}
|
||||
catch (e: NullPointerException) {
|
||||
System.err.println("CodepointSequence is null?!")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return hashAccumulator
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun CharSequence.crc32(): Int {
|
||||
val crc = CRC32()
|
||||
@@ -1968,6 +2035,25 @@ class TerrarumSansBitmap(
|
||||
|
||||
companion object {
|
||||
|
||||
fun CodepointSequence.getHash(): Long {
|
||||
val hashBasis = -3750763034362895579L
|
||||
val hashPrime = 1099511628211L
|
||||
var hashAccumulator = hashBasis
|
||||
|
||||
try {
|
||||
this.forEach {
|
||||
hashAccumulator = hashAccumulator xor it.toLong()
|
||||
hashAccumulator *= hashPrime
|
||||
}
|
||||
}
|
||||
catch (e: NullPointerException) {
|
||||
System.err.println("CodepointSequence is null?!")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return hashAccumulator
|
||||
}
|
||||
|
||||
private fun Boolean.toSign() = if (this) 1 else -1
|
||||
|
||||
/**
|
||||
@@ -2066,6 +2152,9 @@ class TerrarumSansBitmap(
|
||||
internal const val CHARSET_OVERRIDE_BG_BG = 0xFFFC1
|
||||
internal const val CHARSET_OVERRIDE_SR_SR = 0xFFFC2
|
||||
|
||||
internal const val MOVABLE_BLOCK_1 = 0xFFFF0
|
||||
internal const val MOVABLE_BLOCK_M1 = 0xFFFE0
|
||||
|
||||
|
||||
private val autoShiftDownOnLowercase = arrayOf(
|
||||
SHEET_DIACRITICAL_MARKS_VARW
|
||||
|
||||
Reference in New Issue
Block a user