From 4dbda6e6d004b3dfa79e1f67c3501ee2cfcf68c9 Mon Sep 17 00:00:00 2001 From: Miley Chandonnet Date: Mon, 25 May 2026 17:17:02 -0500 Subject: [PATCH] PHO-22/#42 add CliLattice orthographic VoxelFrame projection Project VoxelFrame to LumosTerminalFrame via orthographic mapping of voxel (x, y) to character cells, with z-priority occlusion and a 10-step luminance ramp keyed on voxel scale. Passes ambient and glyph state through unchanged so the renderer can use them downstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lumos/cli/projection/CliLattice.kt | 133 +++++ .../lumos/cli/projection/CliLatticeTest.kt | 564 ++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 phosphor-lumos-cli/src/commonMain/kotlin/link/socket/phosphor/lumos/cli/projection/CliLattice.kt create mode 100644 phosphor-lumos-cli/src/commonTest/kotlin/link/socket/phosphor/lumos/cli/projection/CliLatticeTest.kt diff --git a/phosphor-lumos-cli/src/commonMain/kotlin/link/socket/phosphor/lumos/cli/projection/CliLattice.kt b/phosphor-lumos-cli/src/commonMain/kotlin/link/socket/phosphor/lumos/cli/projection/CliLattice.kt new file mode 100644 index 0000000..2b882fe --- /dev/null +++ b/phosphor-lumos-cli/src/commonMain/kotlin/link/socket/phosphor/lumos/cli/projection/CliLattice.kt @@ -0,0 +1,133 @@ +package link.socket.phosphor.lumos.cli.projection + +import kotlin.math.roundToInt +import link.socket.phosphor.color.NeutralColor +import link.socket.phosphor.color.OklabColor +import link.socket.phosphor.lumos.VoxelFrame +import link.socket.phosphor.lumos.cli.frame.LumosTerminalFrame +import link.socket.phosphor.lumos.cli.frame.LumosTerminalFrame.TerminalCell + +/** + * Orthographic projection of a [VoxelFrame] to a [LumosTerminalFrame]. + * + * Each voxel's `(x, y)` lattice position maps to a character cell, ignoring + * `z` for placement but using `z` for occlusion priority (closer voxels win + * the cell). Zero-scale voxels (those thinned away by bipolar boundary + * compression or glyph carving) are skipped, so empty cells remain empty + * instead of being overpainted by ghost cubes. + * + * Orthographic, not perspective: at typical terminal resolutions (40x20 to + * 60x30) foreshortening barely reads and the simpler projection halves the + * code volume. Color quantization is deliberately not done here — the + * winning voxel's sRGB triplet is forwarded as [OklabColor] for the renderer + * (CliOrb / AnsiColorMap) to quantize. + * + * The projection is pure: it allocates one [LumosTerminalFrame] plus two + * working buffers sized `width * height` per call, and is safe to share + * across threads as long as each call's [project] is isolated. + * + * @param width Output grid width in character cells; must be > 0. + * @param height Output grid height in character cells; must be > 0. + * @param characterAspectRatio Ratio of character cell height to width. + * Defaults to 2.0 because most monospace fonts render characters at ~1:2 + * width:height. Configurable for high-DPI terminals or custom fonts. + */ +class CliLattice( + val width: Int, + val height: Int, + val characterAspectRatio: Float = 2.0f, +) { + init { + require(width > 0) { "width must be > 0, got $width" } + require(height > 0) { "height must be > 0, got $height" } + require(characterAspectRatio > 0f) { + "characterAspectRatio must be > 0, got $characterAspectRatio" + } + } + + private val cellCount: Int = width * height + + /** + * Project [frame] to a terminal-cell grid. + * + * Algorithm: + * 1. For every voxel with nonzero scale, compute screen `(x, y)` via + * orthographic mapping of normalized lattice position. + * 2. Per cell, keep the voxel with the largest `z` (closest to camera). + * 3. Map each winning voxel's scale to a luminance-ramp character and + * pass its sRGB color through as [OklabColor]. + * 4. Empty cells become transparent blanks. + * + * Ambient and glyph state pass through unchanged. + */ + fun project(frame: VoxelFrame): LumosTerminalFrame { + val winningIndex = IntArray(cellCount) { -1 } + val winningZ = FloatArray(cellCount) + val cells = frame.cells + val invAspect = 1f / characterAspectRatio + + for (i in cells.indices) { + val cell = cells[i] + if (cell.scale <= 0f) continue + + val screenXf = (cell.x + 1f) * 0.5f * width + val screenYf = (1f - (cell.y + 1f) * 0.5f) * height * invAspect + + val sx = screenXf.toInt() + val sy = screenYf.toInt() + if (sx < 0 || sx >= width || sy < 0 || sy >= height) continue + + val idx = sy * width + sx + if (winningIndex[idx] < 0 || cell.z > winningZ[idx]) { + winningIndex[idx] = i + winningZ[idx] = cell.z + } + } + + val out = ArrayList(cellCount) + for (idx in 0 until cellCount) { + val winner = winningIndex[idx] + if (winner < 0) { + out += EMPTY_CELL + } else { + val v = cells[winner] + val char = luminanceChar(v.scale) + val color = OklabColor.fromSrgb(NeutralColor.fromRgba(v.red, v.green, v.blue)) + out += TerminalCell(char = char, foreground = color) + } + } + + return LumosTerminalFrame( + width = width, + height = height, + cells = out, + ambient = frame.ambient, + glyphState = frame.glyph, + frameNumber = frame.tick, + ) + } + + companion object { + /** + * Ten-step luminance ramp from densest to lightest. Index 0 (`@`) + * is full-scale; index 9 (space) is zero-scale. + */ + const val LUMINANCE_RAMP: String = "@%#*+=-:. " + + private val EMPTY_CELL = TerminalCell(char = ' ', foreground = null, background = null) + + /** + * Map a voxel `scale` in 0..1 to a character on [LUMINANCE_RAMP]. + * + * Scale clamps to 0..1 so out-of-range inputs still produce a valid + * ramp character. `1.0` returns `@`, `0.0` returns space, midpoints + * land near the middle of the ramp. + */ + fun luminanceChar(scale: Float): Char { + val clamped = scale.coerceIn(0f, 1f) + val maxIndex = LUMINANCE_RAMP.length - 1 + val index = ((1f - clamped) * maxIndex).roundToInt().coerceIn(0, maxIndex) + return LUMINANCE_RAMP[index] + } + } +} diff --git a/phosphor-lumos-cli/src/commonTest/kotlin/link/socket/phosphor/lumos/cli/projection/CliLatticeTest.kt b/phosphor-lumos-cli/src/commonTest/kotlin/link/socket/phosphor/lumos/cli/projection/CliLatticeTest.kt new file mode 100644 index 0000000..843e6ff --- /dev/null +++ b/phosphor-lumos-cli/src/commonTest/kotlin/link/socket/phosphor/lumos/cli/projection/CliLatticeTest.kt @@ -0,0 +1,564 @@ +package link.socket.phosphor.lumos.cli.projection + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import link.socket.phosphor.coordinate.CoordinateSpace +import link.socket.phosphor.field.SubstrateState +import link.socket.phosphor.lumos.VoxelAmbient +import link.socket.phosphor.lumos.VoxelCell +import link.socket.phosphor.lumos.VoxelFrame +import link.socket.phosphor.lumos.VoxelFrameBuilder +import link.socket.phosphor.lumos.VoxelGlyphState +import link.socket.phosphor.lumos.cli.frame.LumosTerminalFrame.TerminalCell +import link.socket.phosphor.palette.AtmospherePresets +import link.socket.phosphor.runtime.SceneSnapshot +import link.socket.phosphor.signal.AtmosphereState +import link.socket.phosphor.signal.CognitivePhase + +class CliLatticeTest { + @Test + fun `constructor rejects non-positive dimensions and aspect ratio`() { + assertFailsWith { CliLattice(width = 0, height = 10) } + assertFailsWith { CliLattice(width = -1, height = 10) } + assertFailsWith { CliLattice(width = 10, height = 0) } + assertFailsWith { CliLattice(width = 10, height = -1) } + assertFailsWith { + CliLattice(width = 10, height = 10, characterAspectRatio = 0f) + } + assertFailsWith { + CliLattice(width = 10, height = 10, characterAspectRatio = -1f) + } + } + + @Test + fun `frame of zero-scale voxels produces an all-blank terminal frame`() { + val lattice = CliLattice(width = 10, height = 6) + val frame = + voxelFrame( + cells = + List(8) { i -> + VoxelCell( + x = (i.toFloat() / 4f) - 1f, + y = (i.toFloat() / 4f) - 1f, + z = 0f, + scale = 0f, + red = 1f, + green = 0.5f, + blue = 0.25f, + ) + }, + ) + + val terminal = lattice.project(frame) + + assertEquals(10, terminal.width) + assertEquals(6, terminal.height) + assertEquals(60, terminal.cells.size) + terminal.cells.forEach { cell -> + assertEquals(' ', cell.char) + assertNull(cell.foreground) + assertNull(cell.background) + } + } + + @Test + fun `single voxel at origin projects to the center cell`() { + val lattice = CliLattice(width = 10, height = 6, characterAspectRatio = 1f) + val frame = + voxelFrame( + cells = + listOf( + VoxelCell( + x = 0f, + y = 0f, + z = 0f, + scale = 1f, + red = 1f, + green = 1f, + blue = 1f, + ), + ), + ) + + val terminal = lattice.project(frame) + + // x = 0 -> screenX = (0 + 1) * 0.5 * 10 = 5 + // y = 0, aspectRatio = 1 -> screenY = (1 - 0.5) * 6 * 1 = 3 + val centerIndex = 3 * 10 + 5 + val centerCell = terminal.cells[centerIndex] + assertEquals('@', centerCell.char, "scale 1.0 should produce '@'") + assertNotNull(centerCell.foreground, "winning cell should carry a foreground color") + + terminal.cells.forEachIndexed { idx, cell -> + if (idx != centerIndex) { + assertEquals(' ', cell.char, "non-winning cells should be blank (idx=$idx)") + } + } + } + + @Test + fun `closer voxel wins the cell when two voxels project to the same cell`() { + val lattice = CliLattice(width = 10, height = 6, characterAspectRatio = 1f) + val far = + VoxelCell( + x = 0f, + y = 0f, + z = -0.5f, + scale = 1f, + red = 1f, + green = 0f, + blue = 0f, + ) + val near = + VoxelCell( + x = 0f, + y = 0f, + z = 0.5f, + scale = 0.5f, + red = 0f, + green = 1f, + blue = 0f, + ) + + val terminal = lattice.project(voxelFrame(cells = listOf(far, near))) + + val center = terminal.cells[3 * 10 + 5] + // near has scale 0.5 (mid-ramp character), far has scale 1.0 ('@'). + // near wins on z, so the cell should reflect near's scale, not far's. + assertNotEquals('@', center.char, "the larger-z voxel should beat the higher-scale voxel") + assertEquals(CliLattice.luminanceChar(0.5f), center.char) + } + + @Test + fun `two zero-scale voxels at the same cell leave the cell blank`() { + val lattice = CliLattice(width = 10, height = 6, characterAspectRatio = 1f) + val a = + VoxelCell( + x = 0f, + y = 0f, + z = -0.5f, + scale = 0f, + red = 1f, + green = 0f, + blue = 0f, + ) + val b = + VoxelCell( + x = 0f, + y = 0f, + z = 0.5f, + scale = 0f, + red = 0f, + green = 1f, + blue = 0f, + ) + + val terminal = lattice.project(voxelFrame(cells = listOf(a, b))) + + terminal.cells.forEach { cell -> + assertEquals(' ', cell.char) + assertNull(cell.foreground) + } + } + + @Test + fun `luminance char maps endpoints and midpoint correctly`() { + assertEquals('@', CliLattice.luminanceChar(1f)) + assertEquals(' ', CliLattice.luminanceChar(0f)) + // Mid-scale lands in the middle of the ramp, not at the endpoints. + val mid = CliLattice.luminanceChar(0.5f) + assertNotEquals('@', mid) + assertNotEquals(' ', mid) + // Out-of-range inputs clamp. + assertEquals('@', CliLattice.luminanceChar(2f)) + assertEquals(' ', CliLattice.luminanceChar(-1f)) + } + + @Test + fun `ambient and glyph state pass through unchanged`() { + val ambient = + VoxelAmbient( + glowRed = 0.1f, + glowGreen = 0.2f, + glowBlue = 0.3f, + glowIntensity = 0.4f, + orbRotationX = 0.5f, + orbRotationY = 0.6f, + orbRotationZ = 0.7f, + ) + val glyph = + VoxelGlyphState( + glyphName = "CHECK", + progress = 0.4f, + red = 0.9f, + green = 0.8f, + blue = 0.7f, + ) + val frame = + voxelFrame( + tick = 99L, + cells = emptyList(), + ambient = ambient, + glyph = glyph, + ) + + val terminal = CliLattice(width = 4, height = 4).project(frame) + + assertEquals(ambient, terminal.ambient) + assertEquals(glyph, terminal.glyphState) + assertEquals(99L, terminal.frameNumber) + } + + @Test + fun `aspect ratio compensation squashes the projected y range`() { + val tall = + VoxelCell( + x = 0f, + y = 0.95f, + z = 0f, + scale = 1f, + red = 1f, + green = 1f, + blue = 1f, + ) + val short = + VoxelCell( + x = 0f, + y = -0.95f, + z = 0f, + scale = 1f, + red = 1f, + green = 1f, + blue = 1f, + ) + val cells = listOf(tall, short) + + val withCompensation = + CliLattice(width = 40, height = 20, characterAspectRatio = 2f) + .project(voxelFrame(cells = cells)) + val withoutCompensation = + CliLattice(width = 40, height = 20, characterAspectRatio = 1f) + .project(voxelFrame(cells = cells)) + + // With aspect ratio = 1, y = -1 should land in the bottom row (index 19). + // With aspect ratio = 2, the same y compresses to row 10. + val withCompRows = nonEmptyRows(withCompensation.cells, 40, 20) + val withoutCompRows = nonEmptyRows(withoutCompensation.cells, 40, 20) + assertTrue( + withCompRows.max() < withoutCompRows.max(), + "expected compensation to pull the bottom voxel up, " + + "got max-row $withCompRows vs $withoutCompRows", + ) + } + + @Test + fun `projection is deterministic for identical inputs`() { + val frame = settledFrame(AtmospherePresets.IDLE) + val lattice = CliLattice(width = 40, height = 20) + + val a = lattice.project(frame) + val b = lattice.project(frame) + + assertEquals(a.cells, b.cells) + assertEquals(a.ambient, b.ambient) + } + + @Test + fun `IDLE atmosphere snapshot at 40x20 matches expected ascii grid`() { + assertAsciiSnapshot( + name = "IDLE", + atmosphere = AtmospherePresets.IDLE, + expected = IDLE_SNAPSHOT, + ) + } + + @Test + fun `LISTENING atmosphere snapshot at 40x20 matches expected ascii grid`() { + assertAsciiSnapshot( + name = "LISTENING", + atmosphere = AtmospherePresets.LISTENING, + expected = LISTENING_SNAPSHOT, + ) + } + + @Test + fun `THINKING atmosphere snapshot at 40x20 matches expected ascii grid`() { + assertAsciiSnapshot( + name = "THINKING", + atmosphere = AtmospherePresets.THINKING, + expected = THINKING_SNAPSHOT, + ) + } + + @Test + fun `UNCERTAIN atmosphere snapshot at 40x20 matches expected ascii grid`() { + assertAsciiSnapshot( + name = "UNCERTAIN", + atmosphere = AtmospherePresets.UNCERTAIN, + expected = UNCERTAIN_SNAPSHOT, + ) + } + + @Test + fun `READY atmosphere snapshot at 40x20 matches expected ascii grid`() { + assertAsciiSnapshot( + name = "READY", + atmosphere = AtmospherePresets.READY, + expected = READY_SNAPSHOT, + ) + } + + private fun assertAsciiSnapshot( + name: String, + atmosphere: AtmosphereState, + expected: String, + ) { + val frame = settledFrame(atmosphere) + val lattice = CliLattice(width = 40, height = 20) + val terminal = lattice.project(frame) + val rendered = terminal.cells.toAsciiGrid(width = 40, height = 20) + // Trailing whitespace within each row is visually meaningless and + // brittle to capture in source. Compare row-by-row with trailing + // spaces stripped. + assertEquals( + expected.lines().joinToString("\n") { it.trimEnd() }, + rendered.lines().joinToString("\n") { it.trimEnd() }, + "snapshot mismatch for $name atmosphere", + ) + } + + private fun List.toAsciiGrid( + width: Int, + height: Int, + ): String = + buildString { + for (row in 0 until height) { + for (col in 0 until width) { + append(this@toAsciiGrid[row * width + col].char) + } + if (row < height - 1) append('\n') + } + } + + private fun nonEmptyRows( + cells: List, + width: Int, + height: Int, + ): List { + val rows = mutableListOf() + for (row in 0 until height) { + for (col in 0 until width) { + if (cells[row * width + col].char != ' ') { + rows += row + break + } + } + } + return rows + } + + private fun voxelFrame( + tick: Long = 0L, + cells: List, + ambient: VoxelAmbient = DEFAULT_AMBIENT, + glyph: VoxelGlyphState? = null, + ): VoxelFrame = + VoxelFrame( + tick = tick, + timestampEpochMillis = 0L, + resolution = 10, + cells = cells, + ambient = ambient, + glyph = glyph, + ) + + private fun settledFrame(atmosphere: AtmosphereState): VoxelFrame { + // The canonical presets use voxelGap = 0.05 for 3D renderers that draw + // small cubes inside a larger lattice cell. ASCII projection has no + // cube-size analog — it just picks a luminance character — so for + // visual regression we lift voxelGap to 1.0 to exercise the full + // luminance ramp. CliOrb's runtime configuration is expected to make + // an equivalent adjustment so the orb is visible in a terminal. + val cliAtmosphere = atmosphere.copy(voxelGap = 1f) + val builder = VoxelFrameBuilder(initialResolution = cliAtmosphere.resolution) + val snapshot = + SceneSnapshot( + frameIndex = 0L, + elapsedTimeSeconds = 0f, + coordinateSpace = CoordinateSpace.WORLD_CENTERED, + agentStates = emptyList(), + substrateState = SubstrateState.create(2, 2), + particleStates = emptyList(), + flowConnections = emptyList(), + flowField = null, + waveformHeightField = null, + waveformGridWidth = null, + waveformGridDepth = null, + cameraTransform = null, + emitterStates = emptyList(), + choreographyPhase = CognitivePhase.NONE, + atmosphere = cliAtmosphere, + atmosphereTransition = null, + ) + // Step the builder long enough to advance pulse and pattern phase past + // their initial zero values, so the snapshot represents the atmosphere + // mid-cycle rather than the moment of construction. + var frame = builder.build(snapshot, dt = 0f) + repeat(SETTLE_FRAMES) { + frame = builder.build(snapshot, dt = SETTLE_DT) + } + return frame + } + + companion object { + private val DEFAULT_AMBIENT = + VoxelAmbient( + glowRed = 0f, + glowGreen = 0f, + glowBlue = 0f, + glowIntensity = 0f, + orbRotationX = 0f, + orbRotationY = 0f, + orbRotationZ = 0f, + ) + + private const val SETTLE_FRAMES: Int = 60 + private const val SETTLE_DT: Float = 0.05f + + // Expected ASCII grids capture the projection at 40x20 after 60 ticks + // of 50 ms each, with voxelGap lifted to 1.0 (see [settledFrame] for + // why). Trailing whitespace per row is normalized away in + // [assertAsciiSnapshot], so rows are listed by their visible content. + // Each snapshot is 20 rows; the orb sits in the top half because of + // the spec's aspect ratio compensation. + private fun snapshotOf(vararg rows: String): String { + require(rows.size == 20) { "snapshot must be exactly 20 rows, got ${rows.size}" } + return rows.joinToString("\n") + } + + private val IDLE_SNAPSHOT: String = + snapshotOf( + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@ @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ @", + " @@@@@@@@@@ @@ @@ @ @ @ @", + " @", + "", + "", + "", + "", + "", + "", + "", + "", + ) + + private val LISTENING_SNAPSHOT: String = + snapshotOf( + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@ @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @ @@@@@@@ @@@@@@@@@@@ @@", + " @", + "", + "", + "", + "", + "", + "", + "", + "", + ) + + private val THINKING_SNAPSHOT: String = + snapshotOf( + "@ @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ @", + " @ @@ @ @@@@@ @@@@ @@@@@@ @", + " @", + "", + "", + "", + "", + "", + "", + "", + "", + ) + + private val UNCERTAIN_SNAPSHOT: String = + snapshotOf( + " @ @+@@=@@ @@@@@@ @=@@@@@@@@@@@. @", + "=-:@@@@@@@@@@@@@@@@@@@@@@ @@=@@@@@@@@ @", + "@@@@:@.@@@@@@@@@@@@@@@@@@@@@@@@=@@@-@@@@", + "@ =@@=@@+@- #@@@@@@@-@@@@@@@@@@@ @@@@@@@", + "@@@@@@@@@:@@@+@@ @@@@@@@#@-@@@@@@.@%@@-@", + "#@@@@@@@@@@*@@ @.@@=+@@@@@@%#+@@@@@@@@@@", + "@@@@#@@@%@@@@@@@-@ @@%@@@ @#=@..@@@@@@@@", + "@@@ @@@@@@@@@@@@@@@@@@#@@ @@@ @ @ @@@@@", + " @@@@@=@*#%@@-@@@@@@@@@@@@@@@@@@@*@@ @@@", + "@ @ * @@@@@@@. @@@@@@@@@@@@@@@@@ - @", + " @%. @@@#@@ @@@@@@@@@@@ @ @", + " @", + "", + "", + "", + "", + "", + "", + "", + "", + ) + + private val READY_SNAPSHOT: String = + snapshotOf( + " @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + "@ @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@", + " @ @ @@@@@@@@@@@ @ @@@ @", + " @", + "", + "", + "", + "", + "", + "", + "", + "", + ) + } +}