Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
commonMain.ru.casperix.multiplatform.text.impl.CacheBasedTextRenderer.kt Maven / Gradle / Ivy
package ru.casperix.multiplatform.text.impl
import ru.casperix.math.axis_aligned.float32.Box2f
import ru.casperix.math.color.Colors
import ru.casperix.math.vector.float32.Vector2f
import ru.casperix.math.vector.toQuad
import ru.casperix.multiplatform.font.FontMetrics
import ru.casperix.multiplatform.font.FontReference
import ru.casperix.multiplatform.text.StringMetrics
import ru.casperix.multiplatform.text.TextGraphicProcessor
import ru.casperix.multiplatform.text.TextRenderConfig
import ru.casperix.multiplatform.text.TextRendererApi
import ru.casperix.renderer.material.SimpleMaterial
import ru.casperix.renderer.misc.AlignMode
import ru.casperix.renderer.vector.GeometryBuilder
import ru.casperix.renderer.vector.VectorGraphic
import ru.casperix.renderer.vector.VectorShape
import kotlin.math.max
class CacheBasedTextRenderer(val processor: TextGraphicProcessor) : TextRendererApi {
data class GraphicKey(val items: List, val availableArea: Vector2f)
private val lineListCache = mutableMapOf()
private val textGraphicCache = mutableMapOf()
private val metricsGraphicCache = mutableMapOf()
data class Cursor(
val xOffset: Float,
val yOffset: Float,
val maxAscent: Float,
val maxDescent: Float,
val maxLeading: Float
)
data class TextPartInfo(
val parent: TextView,
val part: TextPart,
val xOffset: Float,
val fontMetrics: FontMetrics,
val stringMetrics: StringMetrics
)
override fun getFontMetrics(font: FontReference): FontMetrics {
return processor.getFontMetrics(font)
}
override fun getTextGraphic(scheme: TextScheme): VectorGraphic {
return textGraphicCache.getOrPut(scheme) {
createBackgroundGraphic(scheme) + processor.create(scheme)
}
}
private fun createBackgroundGraphic(scheme: TextScheme): VectorGraphic {
return VectorGraphic(scheme.elements.mapNotNull {
if (it.background != null) {
VectorShape(SimpleMaterial(it.background), GeometryBuilder.texturedBox(it.textArea, Box2f.ONE))
} else null
})
}
override fun getTextMetricGraphic(scheme: TextScheme): VectorGraphic {
return metricsGraphicCache.getOrPut(scheme) {
createMetrics(scheme)
}
}
override fun getTextScheme(
blocks: List,
availableArea: Vector2f,
alignMode: AlignMode,
): TextScheme {
val key = GraphicKey(blocks, availableArea)
return lineListCache.getOrPut(key) {
createTextScheme(blocks, availableArea, alignMode)
}
}
private fun createTextScheme(
viewList: List,
availableArea: Vector2f,
alignMode: AlignMode,
): TextScheme {
var cursor = Cursor(0f, 0f, 0f, 0f, 0f)
val lineBuffer = mutableListOf()
val lineList = mutableListOf()
fun callDrawLine() {
var leftToRightDirection = true
lineBuffer.forEach {
if (!processor.isLeftToRight(it.part.text)) {
leftToRightDirection = false
return@forEach
}
}
lineList += generateLineElements(cursor, lineBuffer, leftToRightDirection)
lineBuffer.clear()
}
viewList.forEach { view ->
val font = view.font
val fontMetrics = processor.getFontMetrics(font)
val partList = TextProcessor.splitByParts(view.text)
partList.forEach { part ->
val stringMetrics = processor.getStringMetrics(font, part.text)
val stringWidth = stringMetrics.size.width
val needNextLine =
(!part.isWhitespace && cursor.xOffset + stringWidth > availableArea.x) || part.isLineEnd
if (needNextLine) {
callDrawLine()
val lineHeight = cursor.maxAscent + cursor.maxDescent + cursor.maxLeading
cursor = Cursor(
0f,
cursor.yOffset + lineHeight,
0f,
0f,
0f
)
}
lineBuffer += TextPartInfo(view, part, cursor.xOffset, fontMetrics, stringMetrics)
cursor = cursor.copy(
xOffset = cursor.xOffset + stringWidth,
maxAscent = max(cursor.maxAscent, fontMetrics.ascent),
maxDescent = max(cursor.maxDescent, fontMetrics.descent),
maxLeading = max(cursor.maxLeading, fontMetrics.leading),
)
}
}
callDrawLine()
val scheme = TextScheme(lineList)
return applyAlign(scheme, availableArea, alignMode)
}
private fun generateLineElements(
cursor: Cursor,
buffer: List,
leftToRightDirection: Boolean,
): List {
if (buffer.isEmpty()) {
return emptyList()
}
val baseline = cursor.yOffset + cursor.maxAscent
val fullWidth = buffer.maxOf { it.xOffset + it.stringMetrics.size.width }
return buffer.map { bufferItem ->
bufferItem.run {
val leftOffset = if (leftToRightDirection) {
xOffset
} else {
fullWidth - xOffset - stringMetrics.size.width
}
val leftTop = Vector2f(leftOffset, baseline - fontMetrics.ascent)
TextSchemeElement(
part,
Box2f.byDimension(leftTop, bufferItem.stringMetrics.size.toVector2f()),
stringMetrics,
parent.font,
parent.foreground,
parent.background,
)
}
}
}
private fun createMetrics(scheme: TextScheme): VectorGraphic {
return VectorGraphic(scheme.elements.flatMap { item ->
val fontMetrics = processor.getFontMetrics(item.font)
listOf(
Pair(Colors.GREEN.setAlpha(0.5f), 0f..fontMetrics.ascent),
Pair(
Colors.RED.setAlpha(0.5f),
fontMetrics.ascent..fontMetrics.textHeight
),
Pair(
Colors.BLUE.setAlpha(0.5f),
fontMetrics.textHeight..fontMetrics.lineHeight
),
).map { (color, range) ->
val quad = Box2f.byDimension(
item.textArea.min + Vector2f(0f, range.start),
Vector2f(item.textArea.dimension.x, range.endInclusive - range.start)
).toQuad()
VectorShape(SimpleMaterial(color), GeometryBuilder.texturedQuad(quad))
}
})
}
private fun applyAlign(scheme: TextScheme, viewportSize: Vector2f, alignMode: AlignMode): TextScheme {
if (alignMode == AlignMode.LEFT_TOP) {
return scheme
}
if (!viewportSize.isFinite()) {
throw Exception("Invalid viewport")
}
val dimension = scheme.summaryArea.value.dimension
val alignOffset = alignMode.getPosition(viewportSize, dimension)
if (alignOffset == Vector2f.ZERO) {
return scheme
}
return TextScheme(scheme.elements.map {
val last = it.textArea
val nextCorner = (last.min + alignOffset).run {
if (TextRenderConfig.textRoundToPixel) round()
else this
}
it.copy(textArea = Box2f.byDimension(nextCorner, last.dimension))
})
}
}