mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-06-06 05:58:30 +09:00
hyphenation wip
This commit is contained in:
@@ -8,6 +8,7 @@ import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap
|
|||||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.getHash
|
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.getHash
|
||||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.TextCacheObj
|
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.TextCacheObj
|
||||||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ShittyGlyphLayout
|
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ShittyGlyphLayout
|
||||||
|
import java.lang.Math.pow
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +51,7 @@ class MovableType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
font.createTextCache(CodepointSequence(seq))
|
font.createTextCache(CodepointSequence(seq))
|
||||||
} // list of [ word, word, \n, word, word, word, ... ]
|
}.toMutableList() // list of [ word, word, \n, word, word, word, ... ]
|
||||||
|
|
||||||
|
|
||||||
println("Length of input text: ${inputText.size}")
|
println("Length of input text: ${inputText.size}")
|
||||||
@@ -75,8 +76,14 @@ class MovableType(
|
|||||||
fun justifyAndFlush(
|
fun justifyAndFlush(
|
||||||
lineWidthNow: Int,
|
lineWidthNow: Int,
|
||||||
thisWordObj: TextCacheObj,
|
thisWordObj: TextCacheObj,
|
||||||
thisWord: ShittyGlyphLayout
|
thisWord: ShittyGlyphLayout,
|
||||||
|
hyphenated: Boolean = false,
|
||||||
|
hyphenationScore0: Int? = null,
|
||||||
|
hyphenationScore: Float? = null
|
||||||
) {
|
) {
|
||||||
|
println(" JustifyAndFlush: widthNow = $lineWidthNow, thisWord = ${thisWord.textBuffer.toReadable()}, hyphenated = $hyphenated")
|
||||||
|
|
||||||
|
|
||||||
val thislineEndsWithHangable =
|
val thislineEndsWithHangable =
|
||||||
hangable.contains(currentLine.last().block.glyphLayout!!.textBuffer.penultimate())
|
hangable.contains(currentLine.last().block.glyphLayout!!.textBuffer.penultimate())
|
||||||
val nextWordEndsWithHangable = hangable.contains(thisWordObj.glyphLayout!!.textBuffer.penultimate())
|
val nextWordEndsWithHangable = hangable.contains(thisWordObj.glyphLayout!!.textBuffer.penultimate())
|
||||||
@@ -86,71 +93,111 @@ class MovableType(
|
|||||||
val thisWordWidth = thisWord.width - if (nextWordEndsWithHangable) hangWidth else 0
|
val thisWordWidth = thisWord.width - if (nextWordEndsWithHangable) hangWidth else 0
|
||||||
val scoreForAddingWordThenTightening0 = lineWidthNow + spaceWidth + thisWordWidth - width
|
val scoreForAddingWordThenTightening0 = lineWidthNow + spaceWidth + thisWordWidth - width
|
||||||
val scoreForAddingWordThenTightening = penaliseTightening(scoreForAddingWordThenTightening0)
|
val scoreForAddingWordThenTightening = penaliseTightening(scoreForAddingWordThenTightening0)
|
||||||
// widen: 1, tighten: -1
|
|
||||||
val operation = if (scoreForWidening == 0f && scoreForAddingWordThenTightening == 0f) 0
|
|
||||||
else if (scoreForWidening < scoreForAddingWordThenTightening) 1
|
|
||||||
else -1
|
|
||||||
|
|
||||||
// if adding word and contracting is better (has LOWER score), add the word
|
val (hyphFore, hypePost) = if (hyphenated) thisWord.textBuffer to CodepointSequence()
|
||||||
if (operation == -1) {
|
else thisWord.textBuffer.hyphenate() // hypePost may be empty!
|
||||||
currentLine.add(Block(lineWidthNow + spaceWidth, thisWordObj))
|
val scoreForHyphenateThenTryAgain0 = if (!hyphenated) {
|
||||||
// remove this word from the list of future words
|
val halfWordWidth = font.getWidth(hyphFore)
|
||||||
dequeue()
|
(lineWidthNow + spaceWidth + halfWordWidth - width)
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
val numberOfWords = currentLine.size
|
2147483647
|
||||||
|
|
||||||
// continue with the widening/contraction
|
|
||||||
val moveDeltas = IntArray(numberOfWords)
|
|
||||||
|
|
||||||
val finalScore = when (operation) {
|
|
||||||
1 -> scoreForWidening.toFloat()
|
|
||||||
-1 -> scoreForAddingWordThenTightening0.toFloat()
|
|
||||||
else -> 0f
|
|
||||||
}
|
}
|
||||||
|
val scoreForHyphenateThenTryAgain = penaliseHyphenation(scoreForHyphenateThenTryAgain0)
|
||||||
|
|
||||||
if (numberOfWords > 1) {
|
println("Prestrategy [L ${lines.size}] Scores: W $scoreForWidening, T $scoreForAddingWordThenTightening ($scoreForAddingWordThenTightening0), H $scoreForHyphenateThenTryAgain ($scoreForHyphenateThenTryAgain0)")
|
||||||
val moveAmountsByWord =
|
|
||||||
coalesceIndices(sortWordsByPriority(currentLine, round(finalScore.absoluteValue).toInt()))
|
|
||||||
for (i in 1 until moveDeltas.size) {
|
if (scoreForHyphenateThenTryAgain < minOf(scoreForWidening, scoreForAddingWordThenTightening) && !hyphenated) {
|
||||||
moveDeltas[i] = moveDeltas[i - 1] + moveAmountsByWord.getOrElse(i) { 0 }
|
println(" Hyphenation: '${hyphFore.toReadable()}' '${hypePost.toReadable()}'")
|
||||||
|
|
||||||
|
inputWords[wordCount] = font.createTextCache(hyphFore)
|
||||||
|
inputWords.add(wordCount + 1, font.createTextCache(hypePost))
|
||||||
|
|
||||||
|
// testing only
|
||||||
|
// inputWords[wordCount] = font.createTextCache(CodepointSequence("FORE-".toCharArray().map { it.toInt() }))
|
||||||
|
// inputWords.add(wordCount + 1, font.createTextCache((CodepointSequence("POST".toCharArray().map { it.toInt() }))))
|
||||||
|
|
||||||
|
val thisWordObj = inputWords[wordCount]
|
||||||
|
val thisWord = thisWordObj.glyphLayout!!
|
||||||
|
|
||||||
|
val dropped = currentLine.removeLast()
|
||||||
|
val newBlock = Block(dropped.posX, thisWordObj)
|
||||||
|
currentLine.add(newBlock)
|
||||||
|
|
||||||
|
val lineWidthNow = if (currentLine.isEmpty()) -spaceWidth
|
||||||
|
else currentLine.penultimate().let { it.posX + it.block.glyphLayout!!.width } - 1 // subtract the tiny space AFTER the hyphen
|
||||||
|
|
||||||
|
justifyAndFlush(lineWidthNow, thisWordObj, thisWord, true, scoreForHyphenateThenTryAgain0, scoreForHyphenateThenTryAgain)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// widen: 1, tighten: -1
|
||||||
|
val operation = if (hyphenated) -1
|
||||||
|
else if (scoreForWidening == 0f && scoreForAddingWordThenTightening == 0f) 0
|
||||||
|
else if (scoreForWidening < scoreForAddingWordThenTightening) 1
|
||||||
|
else -1
|
||||||
|
|
||||||
|
// if adding word and contracting is better (has LOWER score), add the word
|
||||||
|
if (operation == -1) {
|
||||||
|
if (!hyphenated) 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 = when (operation) {
|
||||||
|
1 -> scoreForWidening.toFloat()
|
||||||
|
-1 -> scoreForAddingWordThenTightening0.toFloat()
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfWords > 1) {
|
||||||
|
val moveAmountsByWord =
|
||||||
|
coalesceIndices(sortWordsByPriority(currentLine, round(finalScore.absoluteValue).toInt()))
|
||||||
|
for (i in 1 until moveDeltas.size) {
|
||||||
|
moveDeltas[i] = moveDeltas[i - 1] + moveAmountsByWord.getOrElse(i) { 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveDeltas.indices.forEach {
|
||||||
|
moveDeltas[it] = moveDeltas[it] * finalScore.sign.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 * finalScore.sign.toInt() == 0) "Nop" else if (operation * finalScore.sign.toInt() == 1) "Widen" else "Tighten") +
|
||||||
|
" (W $scoreForWidening, T $scoreForAddingWordThenTightening, H $hyphenationScore; $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()
|
||||||
}
|
}
|
||||||
|
|
||||||
moveDeltas.indices.forEach {
|
|
||||||
moveDeltas[it] = moveDeltas[it] * finalScore.sign.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 * finalScore.sign.toInt() == 0) "Nop" else if (operation * finalScore.sign.toInt() == 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: TextCacheObj
|
var thisWordObj: TextCacheObj
|
||||||
@@ -172,7 +219,7 @@ class MovableType(
|
|||||||
val spaceWidth = if (thisWordStr[1].isCJ() && currentLine.isNotEmpty()) 0 else spaceWidth
|
val spaceWidth = if (thisWordStr[1].isCJ() && currentLine.isNotEmpty()) 0 else spaceWidth
|
||||||
|
|
||||||
println(
|
println(
|
||||||
"Processing word [$wordCount] ${thisWordStr.joinToString("") { Character.toString(it.toChar()) }} ; \t\t${
|
"Processing word [$wordCount] ${thisWordStr.toReadable()} ; \t\t${
|
||||||
thisWordStr.joinToString(
|
thisWordStr.joinToString(
|
||||||
" "
|
" "
|
||||||
) { it.toHex() }
|
) { it.toHex() }
|
||||||
@@ -235,10 +282,10 @@ class MovableType(
|
|||||||
private data class Block(var posX: Int, val block: TextCacheObj) // a single word
|
private data class Block(var posX: Int, val block: TextCacheObj) // a single word
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val periods = listOf(0x2E, 0x3A, 0x21, 0x3F, 0x2026, 0x3002, 0xff0e).toHashSet()
|
private val periods = listOf(0x2E, 0x3A, 0x21, 0x3F, 0x2026, 0x3002, 0xff0e).toSortedSet()
|
||||||
private val quots = listOf(0x22, 0x27, 0xAB, 0xBB, 0x2018, 0x2019, 0x201A, 0x201B, 0x201C, 0x201D, 0x201E, 0x201F, 0x2039, 0x203A).toHashSet()
|
private val quots = listOf(0x22, 0x27, 0xAB, 0xBB, 0x2018, 0x2019, 0x201A, 0x201B, 0x201C, 0x201D, 0x201E, 0x201F, 0x2039, 0x203A).toSortedSet()
|
||||||
private val commas = listOf(0x2C, 0x3B, 0x3001, 0xff0c).toHashSet()
|
private val commas = listOf(0x2C, 0x3B, 0x3001, 0xff0c).toSortedSet()
|
||||||
private val hangable = listOf(0x2E, 0x2C).toHashSet()
|
private val hangable = listOf(0x2E, 0x2C).toSortedSet()
|
||||||
private val spaceWidth = 5
|
private val spaceWidth = 5
|
||||||
private val hangWidth = 6
|
private val hangWidth = 6
|
||||||
|
|
||||||
@@ -261,7 +308,7 @@ class MovableType(
|
|||||||
val sackOfIndices = (1 until currentLine.size).map {
|
val sackOfIndices = (1 until currentLine.size).map {
|
||||||
val thisWord = currentLine[it].block.glyphLayout!!.textBuffer
|
val thisWord = currentLine[it].block.glyphLayout!!.textBuffer
|
||||||
|
|
||||||
// println(" Index word [$it/$length]: ${thisWord.joinToString("") { Character.toString(it.toChar()) }} ; \t\t${thisWord.joinToString(" ") { it.toHex() }}")
|
// println(" Index word [$it/$length]: ${thisWord.toReadable()} ; \t\t${thisWord.joinToString(" ") { it.toHex() }}")
|
||||||
|
|
||||||
val thisWordEnd = thisWord[thisWord.lastIndex]
|
val thisWordEnd = thisWord[thisWord.lastIndex]
|
||||||
val thisWordFirst = thisWord[0]
|
val thisWordFirst = thisWord[0]
|
||||||
@@ -337,12 +384,67 @@ class MovableType(
|
|||||||
return this[this.size - 2]
|
return this[this.size - 2]
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun penaliseTightening(score: Int): Float = 0.0006f * score * score * score + 0.036f * score
|
private fun penaliseTightening(score: Int): Float = 0.06f * score * score * score + 0.036f * score
|
||||||
|
|
||||||
|
private fun penaliseHyphenation(score: Int): Float = (14.6 * pow(score.toDouble(), 1.0/3.0) + 0.14*score).toFloat()
|
||||||
|
|
||||||
private fun CodePoint.isCJ() = listOf(4, 6).any {
|
private fun CodePoint.isCJ() = listOf(4, 6).any {
|
||||||
TerrarumSansBitmap.codeRange[it].contains(this)
|
TerrarumSansBitmap.codeRange[it].contains(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hyphenates the word at the middle ("paragraph" -> "para-graph")
|
||||||
|
*
|
||||||
|
* @return left word ("para-"), right word ("graph")
|
||||||
|
*/
|
||||||
|
private fun CodepointSequence.hyphenate(): Pair<CodepointSequence, CodepointSequence> {
|
||||||
|
val middlePoint = this.size / 2
|
||||||
|
// search for the end of the vowel cluster for left and right
|
||||||
|
// one with the least distance from the middle point will be used for hyphenating point
|
||||||
|
val hyphenateCandidates = ArrayList<Int>()
|
||||||
|
for (i in 1 until this.size) {
|
||||||
|
val thisChar = this[i]
|
||||||
|
val prevChar = this[i-1]
|
||||||
|
if (!isVowel(thisChar) && isVowel(prevChar))
|
||||||
|
hyphenateCandidates.add(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
hyphenateCandidates.removeIf { it <= 2 || it >= this.size - 2 }
|
||||||
|
|
||||||
|
// println("Hyphenating ${this.toReadable()} -> [${hyphenateCandidates.joinToString()}]")
|
||||||
|
|
||||||
|
if (hyphenateCandidates.isEmpty()) {
|
||||||
|
return this to CodepointSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
val hyphPoint = hyphenateCandidates.minByOrNull { (it - middlePoint).absoluteValue }!!
|
||||||
|
|
||||||
|
// println("hyphPoint = $hyphPoint")
|
||||||
|
|
||||||
|
val fore = this.subList(0, hyphPoint).toMutableList().let {
|
||||||
|
it.add(0x2d); it.add(0x00)
|
||||||
|
CodepointSequence(it)
|
||||||
|
}
|
||||||
|
val post = this.subList(hyphPoint, this.size).toMutableList().let {
|
||||||
|
it.add(0, 0x00)
|
||||||
|
CodepointSequence(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// println("hyph return: ${fore.toReadable()} ${post.toReadable()}")
|
||||||
|
|
||||||
|
return fore to post
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isVowel(c: CodePoint) = vowels.contains(c)
|
||||||
|
|
||||||
|
private val vowels = (listOf(0x41, 0x45, 0x49, 0x4f, 0x55, 0x59, 0x41, 0x65, 0x69, 0x6f, 0x75, 0x79) +
|
||||||
|
(0xc0..0xc6) + (0xc8..0xcf) + (0xd2..0xd6) + (0xd8..0xdd) +
|
||||||
|
(0xe0..0xe6) + (0xe8..0xef) + (0xf2..0xf6) + (0xf8..0xfd) +
|
||||||
|
(0xff..0x105) + (0x112..0x118) + (0x128..0x131) + (0x14c..0x153) +
|
||||||
|
(0x168..0x173) + (0x176..0x178)).toSortedSet()
|
||||||
|
|
||||||
|
private fun CodepointSequence.toReadable() = this.joinToString("") { Character.toString(it.toChar()) }
|
||||||
|
|
||||||
} // end of companion object
|
} // end of companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user