diff --git a/assets/mods/basegame/books/btex.xml b/assets/mods/basegame/books/btex.xml index 1d518691f..0fa11a0b7 100644 --- a/assets/mods/basegame/books/btex.xml +++ b/assets/mods/basegame/books/btex.xml @@ -18,7 +18,7 @@
What Really Is a Book

A book is a collection of texts printed in a special way that allows them to be read easily, with - enumerable pages and insertion of other helpful resources, such as illustrations and hyperlinks.

+ enumerable pages and insertion of other helpful resources, such as illustrations and hyperlinks.

@@ -34,33 +34,33 @@ Writing Book Using Pen and Papers

If you open a book on a writing table, you will be welcomed with a - toolbar used to put other book elements, such as chapters, sections.

+ toolbar used to put other book elements, such as chapters and sections.

- Writing Book Using Typewriter + Writing Book Using a Typewriter -

Typewriters can only write single style of font, therefore chapters and +

Typewriters can only write in a single style of font, chapters and sections are not available.

- Writing Book using Computer + Writing Book Using a Computer -

Writing book using a computer requires a use of the Book Typesetting Engine Extended, or

+

Writing book using a computer requires the use of the Book Typesetting Engine Extended, or .

Full Control of the Shape

With you can fully control how your publishing would look like, from a pile of papers that look like they have been typed out using typewriter, a pile of papers but a - fully-featured printouts that have illustrations in it, to a fully-featured hardcover book.

+ fully-featured printouts that have illustrations in it, to a true hardcover book.

This style is controlled using the cover attribute on the root tag, - with following values: typewriter, printout, hardcover

+ with following values: typewriter, printout and hardcover.

-

Typewriter and Printout are considered not bound and readers will only see one page at a time, +

Typewriter and Printout are considered not-bound and readers will only see one page at a time, while Hardcover is considered bound and two pages are presented to the readers.

diff --git a/lib/TerrarumSansBitmap.jar b/lib/TerrarumSansBitmap.jar index e410aa5ab..93568aa88 100644 --- a/lib/TerrarumSansBitmap.jar +++ b/lib/TerrarumSansBitmap.jar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2bb4f64036ccb2b27f47cb0bb8e34691206170c358d4128512f6aa79f61706 -size 188472 +oid sha256:73281a8cb58252a6b66b7a97c26a9d962609b0c25abee0df0d9acc2407008e30 +size 189535 diff --git a/src/net/torvald/btex/BTeXDocument.kt b/src/net/torvald/btex/BTeXDocument.kt index 0d66cdc64..78a994471 100644 --- a/src/net/torvald/btex/BTeXDocument.kt +++ b/src/net/torvald/btex/BTeXDocument.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.TextureRegion import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarumsansbitmap.MovableType +import net.torvald.terrarumsansbitmap.gdx.CodepointSequence /** * Created by minjaesong on 2023-10-28. @@ -15,9 +16,9 @@ class BTeXDocument { var inner = "standard" var papersize = "standard" - var textWidth = 450 + var textWidth = 480 var lineHeightInPx = 24 - var pageLines = 24 + var pageLines = 20 var textHeight = pageLines * lineHeightInPx val pageMarginH = 15 @@ -38,6 +39,9 @@ class BTeXDocument { val currentPage: Int get() = pages.size - 1 + val currentPageObj: BTeXPage + get() = pages[currentPage] + val pageIndices: IntRange get() = pages.indices @@ -72,7 +76,7 @@ class BTeXPage( private val drawCalls = ArrayList() fun appendDrawCall(drawCall: BTeXDrawCall) { - drawCalls.add(drawCall) + if (drawCall.isNotBlank()) drawCalls.add(drawCall) } fun render(frameDelta: Float, batch: SpriteBatch, x: Int, y: Int, marginH: Int, marginV: Int) { @@ -82,6 +86,9 @@ class BTeXPage( it.draw(batch, x + marginH, y + marginV) } } + + fun isEmpty() = drawCalls.isEmpty() + fun isNotEmpty() = drawCalls.isNotEmpty() } interface BTeXTextDrawCall { @@ -103,8 +110,8 @@ data class MovableTypeDrawCall(val movableType: MovableType, override val rowSta }*/ class BTeXDrawCall( - val posX: Int, // position relative to the page start (excluding page margin) - val posY: Int, // position relative to the page start (excluding page margin) + var posX: Int, // position relative to the page start (excluding page margin) + var posY: Int, // position relative to the page start (excluding page margin) val theme: String, val colour: Color, val text: BTeXTextDrawCall? = null, @@ -132,11 +139,25 @@ class BTeXDrawCall( batch.draw(texture, px, py) } else throw Error("Text and Texture are both non-null") + + extraDrawFun(batch, px, py) } + fun isNotBlank(): Boolean { + if (text == null && texture == null) return false + if (text is MovableTypeDrawCall && text.movableType.inputText.isBlank()) return false +// if (text is RaggedLeftDrawCall && text.raggedType.inputText.isBlank()) return false + return true + } + + internal var extraDrawFun: (SpriteBatch, Float, Float) -> Unit = { _,_,_ ->} internal val lineCount = if (text != null) text.rowEnd - text.rowStart else TODO() + companion object { + private fun CodepointSequence.isBlank() = this.all { whitespaces.contains(it) } + private val whitespaces = (listOf(0x00, 0x20, 0x3000, 0xA0, 0xAD) + (0x2000..0x200F) + (0x202A..0x202F) + (0x205F..0x206F) + (0xFFFE0..0xFFFFF)).toHashSet() + } } \ No newline at end of file diff --git a/src/net/torvald/btex/BTeXParser.kt b/src/net/torvald/btex/BTeXParser.kt index dc05aa524..4ce0847d6 100644 --- a/src/net/torvald/btex/BTeXParser.kt +++ b/src/net/torvald/btex/BTeXParser.kt @@ -2,11 +2,13 @@ package net.torvald.btex import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.App import net.torvald.terrarum.btex.BTeXDocument import net.torvald.terrarum.btex.BTeXDrawCall import net.torvald.terrarum.btex.MovableTypeDrawCall import net.torvald.terrarum.gameitems.ItemID +import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarumsansbitmap.MovableType import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap import org.xml.sax.Attributes @@ -75,6 +77,9 @@ object BTeXParser { private val paragraphBuffer = StringBuilder() + private var lastTagAtDepth = Array(24) { "" } + private var pTagCntAtDepth = IntArray(24) + init { BTeXHandler::class.declaredFunctions.filter { it.findAnnotation() != null }.forEach { println("Tag opener: ${it.name}") @@ -119,6 +124,10 @@ object BTeXParser { if (tagStack.isNotEmpty() && !textTags.contains(tagStack.last()) && textDecorTags.contains(theTag)) throw IllegalStateException("Text decoration tag '$theTag' used outside of a text tag (tag stack is ${tagStack.joinToString()})") + if (lastTagAtDepth[tagStack.size] != "P") pTagCntAtDepth[tagStack.size] = 0 + if (theTag == "P") pTagCntAtDepth[tagStack.size] += 1 + lastTagAtDepth[tagStack.size] = theTag + tagStack.add(theTag) val attribs = HashMap().also { @@ -131,10 +140,10 @@ object BTeXParser { System.err.println("Unknown tag: $theTag") else { try { - it.call(this, this, doc, theTag, uri, attribs) + it.call(this, this, doc, theTag, uri, attribs, pTagCntAtDepth[tagStack.size]) } catch (e: Throwable) { - throw BTeXParsingException(e.stackTraceToString()) + throw BTeXParsingException("processElem$theTag"+"\n"+e.stackTraceToString()) } } } @@ -143,13 +152,15 @@ object BTeXParser { } override fun endElement(uri: String, localName: String, qName: String) { + lastTagAtDepth[tagStack.size] = "xxx" + val popped = tagStack.removeLast() val theTag = qName.uppercase() elemClosers["closeElem$theTag"].let { try { - it?.call(this, this, doc, theTag, uri) + it?.call(this, this, doc, theTag, uri, pTagCntAtDepth[tagStack.size]) } catch (e: Throwable) { throw BTeXParsingException(e.stackTraceToString()) @@ -396,15 +407,15 @@ object BTeXParser { ) private val pageWidthMap = hashMapOf( - "standard" to 450 + "standard" to 480 ) private val pageHeightMap = hashMapOf( - "standard" to 24 + "standard" to 20 ) @OpenTag // reflective access is impossible with 'private' - fun processElemBTEXDOC(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemBTEXDOC(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { if (handler.btexOpened) { throw BTeXParsingException("BTEXDOC tag has already opened") } @@ -431,7 +442,7 @@ object BTeXParser { } @OpenTag // reflective access is impossible with 'private' - fun processElemPAIR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemPAIR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { if (tagStack.size == 3 && tagStack.getOrNull(1) == "blocklut") { blockLut[attribs["key"]!!] = attribs["value"]!! } @@ -441,35 +452,35 @@ object BTeXParser { } @OpenTag // reflective access is impossible with 'private' - fun processElemSPAN(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemSPAN(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { attribs["span"]?.let { spanColour = it } } @OpenTag // reflective access is impossible with 'private' - fun processElemTABLEOFCONTENTS(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemTABLEOFCONTENTS(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { // TODO add post-parsing hook to the handler } @OpenTag // reflective access is impossible with 'private' - fun processElemBTEX(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemBTEX(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { handler.paragraphBuffer.append("BTeX") } @OpenTag // reflective access is impossible with 'private' - fun processElemCOVER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemCOVER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { doc.addNewPage(Color(0x6f4a45ff)) handler.spanColour = "white" } @OpenTag // reflective access is impossible with 'private' - fun processElemTOC(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemTOC(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { doc.addNewPage() } @OpenTag // reflective access is impossible with 'private' - fun processElemMANUSCRIPT(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemMANUSCRIPT(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { doc.addNewPage() } @@ -480,48 +491,87 @@ object BTeXParser { @OpenTag // reflective access is impossible with 'private' - fun processElemBR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemBR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { handler.paragraphBuffer.append("\n") } @OpenTag // reflective access is impossible with 'private' - fun processElemNEWPAGE(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemNEWPAGE(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { doc.addNewPage() } @CloseTag // reflective access is impossible with 'private' - fun closeElemFULLPAGEBOX(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) { + fun closeElemFULLPAGEBOX(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { doc.addNewPage() } @OpenTag // reflective access is impossible with 'private' - fun processElemP(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { + fun processElemP(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap, siblingIndex: Int) { } @CloseTag - fun closeElemCOVER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) { + fun closeElemCOVER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { handler.spanColour = null } @CloseTag // reflective access is impossible with 'private' - fun closeElemTITLE(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) = closeElemP(handler, doc, theTag, uri) + fun closeElemTITLE(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) = closeElemP(handler, doc, theTag, uri, siblingIndex) @CloseTag // reflective access is impossible with 'private' - fun closeElemAUTHOR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) = closeElemP(handler, doc, theTag, uri) + fun closeElemAUTHOR(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) = closeElemP(handler, doc, theTag, uri, siblingIndex) @CloseTag // reflective access is impossible with 'private' - fun closeElemEDITION(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) = closeElemP(handler, doc, theTag, uri) + fun closeElemEDITION(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) = closeElemP(handler, doc, theTag, uri, siblingIndex) @CloseTag // reflective access is impossible with 'private' - fun closeElemCHAPTER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) = closeElemP(handler, doc, theTag, uri) + fun closeElemCHAPTER(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { + val indent = 16 + val thePar = "\n" + handler.paragraphBuffer.toString().trim() + typesetParagraphs(thePar, handler, doc.textWidth - indent).also { + // add indents and adjust text y pos + it.forEach { + it.posX += indent + it.posY -= doc.lineHeightInPx / 2 + } + // add ornamental column on the left + it.forEach { + it.extraDrawFun = { batch, x, y -> + Toolkit.fillArea(batch, x - (indent - 2), y + doc.lineHeightInPx, 6f, (it.lineCount - 1).coerceAtLeast(1) * doc.lineHeightInPx.toFloat()) + } + } + } + handler.paragraphBuffer.clear() + } @CloseTag // reflective access is impossible with 'private' - fun closeElemSECTION(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) = closeElemP(handler, doc, theTag, uri) + fun closeElemSECTION(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { + val indent = 8 + val thePar = "\n" + handler.paragraphBuffer.toString().trim() + typesetParagraphs(thePar, handler, doc.textWidth - indent).also { + // add indents and adjust text y pos + it.forEach { + it.posX += indent + it.posY -= doc.lineHeightInPx / 2 + } + } + handler.paragraphBuffer.clear() + } @CloseTag // reflective access is impossible with 'private' - fun closeElemP(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) { - val thePar = handler.paragraphBuffer.toString().trim() + "\n" - printdbg("Par: '$thePar'") + fun closeElemP(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { + val thePar = (if (siblingIndex > 1) "\u3000" else "") + handler.paragraphBuffer.toString().trim() // indent the strictly non-first pars + typesetParagraphs(thePar, handler) + handler.paragraphBuffer.clear() + } + + @CloseTag // reflective access is impossible with 'private' + fun closeElemSPAN(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, siblingIndex: Int) { + spanColour = null + } + + + private fun typesetParagraphs(thePar: String, handler: BTeXHandler, width: Int = doc.textWidth): List { val font = getFont() - val slugs = MovableType(font, thePar, doc.textWidth) + val slugs = MovableType(font, thePar, width) + val drawCalls = ArrayList() var remainder = doc.pageLines - doc.currentLine var slugHeight = slugs.height @@ -540,7 +590,7 @@ object BTeXParser { MovableTypeDrawCall(slugs, subset.first, subset.second) ) - doc.appendDrawCall(drawCall) + doc.appendDrawCall(drawCall); drawCalls.add(drawCall) linesOut += remainder slugHeight -= remainder @@ -561,7 +611,7 @@ object BTeXParser { MovableTypeDrawCall(slugs, subset.first, subset.second) ) - doc.appendDrawCall(drawCall) + doc.appendDrawCall(drawCall); drawCalls.add(drawCall) linesOut += remainder slugHeight -= remainder @@ -571,19 +621,10 @@ object BTeXParser { } } - handler.paragraphBuffer.clear() - } + // if typesetting the paragraph leaves the first line of new page empty, move the "row cursor" back up + if (doc.currentLine == 1 && doc.currentPageObj.isEmpty()) doc.currentLine = 0 // '\n' adds empty draw call to the page, which makes isEmpty() to return false - @OpenTag // reflective access is impossible with 'private' - fun processElemARST(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String, attribs: HashMap) { - - } - - - - @CloseTag // reflective access is impossible with 'private' - fun closeElemSPAN(handler: BTeXHandler, doc: BTeXDocument, theTag: String, uri: String) { - spanColour = null + return drawCalls } } diff --git a/src/net/torvald/terrarum/tests/BTeXTest.kt b/src/net/torvald/terrarum/tests/BTeXTest.kt index 31a30cf9c..439778e62 100644 --- a/src/net/torvald/terrarum/tests/BTeXTest.kt +++ b/src/net/torvald/terrarum/tests/BTeXTest.kt @@ -5,7 +5,10 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration +import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.TextureRegion import com.badlogic.gdx.graphics.glutils.ShaderProgram import net.torvald.btex.BTeXParser import net.torvald.terrarum.FlippingSpriteBatch @@ -34,9 +37,9 @@ class BTeXTest : ApplicationAdapter() { - - - + + + What Is a Book

This example book is designed to give you the example of the Book Language.

@@ -44,49 +47,49 @@ class BTeXTest : ApplicationAdapter() {
What Really Is a Book

A book is a collection of texts printed in a special way that allows them to be read easily, with - enumerable pages and insertion of other helpful resources, such as illustrations and hyperlinks.

+ enumerable pages and insertion of other helpful resources, such as illustrations and hyperlinks.

- + + + + - - - Writing Book Using Pen and Papers

If you open a book on a writing table, you will be welcomed with a - toolbar used to put other book elements, such as chapters, sections.

+ toolbar used to put other book elements, such as chapters and sections.

- - - - Writing Book Using Typewriter -

Typewriters can only write single style of font, therefore chapters and + + + Writing Book Using a Typewriter + +

Typewriters can only write in a single style of font, chapters and sections are not available.

- - - - Writing Book using Computer -

Writing book using a computer requires a use of the Book Typesetting Engine Extended, or

+ + + Writing Book Using a Computer + +

Writing book using a computer requires the use of the Book Typesetting Engine Extended, or .

Full Control of the Shape

With you can fully control how your publishing would look like, from a pile of papers that look like they have been typed out using typewriter, a pile of papers but a - fully-featured printouts that have illustrations in it, to a fully-featured hardcover book.

+ fully-featured printouts that have illustrations in it, to a true hardcover book.

This style is controlled using the cover attribute on the root tag, - with following values: typewriter, printout, hardcover

+ with following values: typewriter, printout and hardcover.

-

Typewriter and Printout are considered not bound and readers will only see one page at a time, +

Typewriter and Printout are considered not-bound and readers will only see one page at a time, while Hardcover is considered bound and two pages are presented to the readers.

@@ -94,12 +97,16 @@ class BTeXTest : ApplicationAdapter() { + + """ private lateinit var document: BTeXDocument private lateinit var batch: FlippingSpriteBatch private lateinit var camera: OrthographicCamera + private lateinit var bg: TextureRegion + override fun create() { batch = FlippingSpriteBatch(1000) camera = OrthographicCamera(1280f, 720f) @@ -107,20 +114,29 @@ class BTeXTest : ApplicationAdapter() { camera.update() batch.projectionMatrix = camera.combined + bg = TextureRegion(Texture(Gdx.files.internal("test_assets/Screenshot-1714034883660.png"))) + document = BTeXParser.invoke(tex) } private var scroll = 0 + val pageGap = 6 override fun render() { gdxClearAndEnableBlend(.063f, .070f, .086f, 1f) + val drawX = (1280 - (pageGap + document.pageWidth*2)) / 2 + val drawY = 100 + batch.inUse { + batch.color = Color.WHITE + batch.draw(bg, 0f, 0f) + if (scroll - 1 in document.pageIndices) - document.render(0f, batch, scroll - 1, 12, 12) + document.render(0f, batch, scroll - 1, drawX, drawY) if (scroll in document.pageIndices) - document.render(0f, batch, scroll, 12 + (6 + document.pageWidth), 12) + document.render(0f, batch, scroll, drawX + (6 + document.pageWidth), drawY) } diff --git a/test_assets/Screenshot-1714034883660.png b/test_assets/Screenshot-1714034883660.png new file mode 100644 index 000000000..5ce788cf5 --- /dev/null +++ b/test_assets/Screenshot-1714034883660.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d24ff646475f7b91850653fead9597e78f2c98e5e7443252c508cbc92fa80da7 +size 660318