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 @@
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() {
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