From 3d0a33619daf3acfc2da32daaffe9d9d245101fc Mon Sep 17 00:00:00 2001 From: patbeagan1 Date: Sat, 7 Jan 2023 23:38:00 -0600 Subject: [PATCH] Migrating b20 code back into the consolevision codebase --- .../dev/patbeagan/consolevision/Lines.kt | 300 ++++++++++++++++++ .../patbeagan/consolevision/ImagePrinter.kt | 72 ++++- .../patbeagan/consolevision/TerminalStyle.kt | 47 +++ .../patbeagan/consolevision/ansi/AnsiSGR.kt | 2 +- .../patbeagan/consolevision/ansi/Color256.kt | 33 -- .../consolevision/ansi/StyleExtensions.kt | 35 -- .../consolevision/ext/ColorReduction.kt | 38 +++ .../consolevision/ext/StyleExtensions.kt | 33 ++ .../dev/patbeagan/consolevision/types/ARGB.kt | 23 ++ .../consolevision/types/CompressedPoint.kt | 60 +++- .../consolevision/types/CoordRect.kt | 16 + .../patbeagan/consolevision/types/List2D.kt | 90 ++++++ .../patbeagan/consolevision/util/Distance.kt | 17 + .../util/TerminalColorStyleTest.kt | 2 +- 14 files changed, 696 insertions(+), 72 deletions(-) create mode 100644 app/src/main/kotlin/dev/patbeagan/consolevision/Lines.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/TerminalStyle.kt delete mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/StyleExtensions.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/ext/ColorReduction.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/ext/StyleExtensions.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/types/ARGB.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/types/CoordRect.kt create mode 100644 lib/src/main/kotlin/dev/patbeagan/consolevision/types/List2D.kt diff --git a/app/src/main/kotlin/dev/patbeagan/consolevision/Lines.kt b/app/src/main/kotlin/dev/patbeagan/consolevision/Lines.kt new file mode 100644 index 0000000..4efc920 --- /dev/null +++ b/app/src/main/kotlin/dev/patbeagan/consolevision/Lines.kt @@ -0,0 +1,300 @@ +package dev.patbeagan.consolevision.consolevision.demo + +import dev.patbeagan.consolevision.ansi.AnsiColor +import dev.patbeagan.consolevision.ansi.AnsiConstants.HIDE_CURSOR + +import dev.patbeagan.consolevision.ansi.AnsiConstants.RIS +import dev.patbeagan.consolevision.style +import dev.patbeagan.consolevision.types.ColorInt +import dev.patbeagan.consolevision.types.CompressedPoint +import dev.patbeagan.consolevision.types.CoordRect +import dev.patbeagan.consolevision.types.List2D +import dev.patbeagan.consolevision.types.coord +import dev.patbeagan.consolevision.types.coordRect +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.random.Random + +fun main() { + (1 coord 1).lineByDDATo(3 coord 4) + getCircleByBresenham(1 coord 1, 3) +} + +fun CompressedPoint.lineByDDATo(end: CompressedPoint): List { + val dx = end.x - x + val dy = end.y - y + val steps = max(abs(dx), abs(dy)) + val stepX = dx / steps.toFloat(); + val stepY = dy / steps.toFloat(); + + var x = x.toFloat() + var y = y.toFloat() + + return (0..steps).map { + val ret = x.roundToInt() coord y.roundToInt() + x += stepX + y += stepY + ret + } +} + +fun MutableList.drawCircle(x: Int, y: Int, p: Int, q: Int) = addAll( + listOf( + (x + p) coord (y + q), + (x - p) coord (y + q), + (x + p) coord (y - q), + (x - p) coord (y - q), + (x + q) coord (y + p), + (x - q) coord (y + p), + (x + q) coord (y - p), + (x - q) coord (y - p) + ) +) + +fun circleBres(xc: Int, yc: Int, r: Int): Pair, CoordRect> { + var x = 0 + var y = r + var d = 3 - 2 * r + val res = mutableListOf() + res.drawCircle(xc, yc, x, y) + while (y >= x) { + // for each pixel we will + // draw all eight pixels + x++ + + // check for decision parameter + // and correspondingly + // update d, x, y + d = if (d > 0) { + y-- + d + 4 * (x - y) + 10 + } else d + 4 * x + 6 + res.drawCircle(xc, yc, x, y) + } + val topLeft = (xc - r) coord (yc - r) + val botRight = (xc + r) coord (yc + r) + return res.distinct() to (topLeft coordRect botRight) +} + +fun List2D.fillPolygon( + rect: CoordRect, + fill: Boolean = true, +): List2D { + var last = false + var isInShape = 0 + var entering = true + + for (y in (rect.lesser.y + 1)..rect.greater.y) { + for (x in rect.lesser.x..rect.greater.x) { + if (y !in 1 until height) continue + val b = this.at(x, y) + when { + b != last -> { + if (isInShape == 2) entering = false + if (isInShape == 0) entering = true + if (entering) isInShape++ else isInShape-- + if (isInShape > 0) assign(x, y, fill) + } + + isInShape > 0 -> assign(x, y, fill) + } + last = b + } + + isInShape = 0 + entering = true + last = false + } + return this +} + +fun getCircleByBresenham(center: CompressedPoint, radius: Int) { +// var p = 0 +// var q = radius +// var r = radius +// var decision = 3 - 2 * r +// val results = mutableListOf() +// while (p < q) { +// results.drawCircle(center.x, center.y, p, q) +// p++ +// if (decision < 0) { +// decision += 4 * p + 6 +// } else { +// r -= 1 +// decision += 4 * (p - q) + 10 +// } +// results.drawCircle(center.x, center.y, p, q) +// } +// val sorted = results.sorted() +// (0..results.maxBy { it.y }!!.y).forEach { y -> +// (0..results.maxBy { it.x }!!.x).forEach { x -> +// if (x coord y in sorted) print("x") else print(".") +// } +// println() +// } +// println(sorted) +// println() + + previewCircle() + + val screen = (Array(40) { + Array(80) { 0 } + }).toList2D() + var tick = 0 + forever(1000 / 20) { + println(HIDE_CURSOR + RIS) + (0..5).forEach { + screen.addLayer((it * 192 % 256) shl 16 or 0x00AA88) { + drawCircle(getRandomCircleCoordinate(it), 5) + } + } + screen.addLayer(0xff0000) { + drawCircle(((tick++) % 80) coord 20, 10) + } + screen.also { it.traverseMap { ColorInt(it) }.printScreenColor() } + screen.traverseMutate { x, y, i -> 0 } + } +} + +fun forever(limiter: Int? = 100, action: () -> Unit) { + while (true) { + action() + limiter?.let { Thread.sleep(it.toLong()) } + } +} + + +fun List2D.drawCircle( + center: CompressedPoint, + radius: Int +) { + circleBres(center.x, center.y, radius).also { + val (list, _) = it + traverseAssign(list, true) + } +} + +fun List2D.drawLine( + start: CompressedPoint, + end: CompressedPoint +) { + start.lineByDDATo(end).also { traverseAssign(it, true) } +} + +fun List2D.drawCircleFilled( + center: CompressedPoint, + radius: Int +) { + circleBres(center.x, center.y, radius).also { + val (list, rect) = it + traverseAssign(list, true) + this.fillPolygon(rect.modifyBy(gy = -1), true) + } +} + +private fun previewCircle() { + val screen = (Array(43) { + Array(80) { false } + }).toList2D() +// circleBres(20, 20, 10).also { pair -> +// val (list, rect) = pair +// screen.traverseAssign(list, true) +// +// printScreen(screen) +// +// println() +// +// screen.fillPolygon(rect.modifyBy(gy = -1), true) +// printScreen(screen) +// } + + screen.drawCircleFilled(30 coord 30, 10) + screen.drawCircle(20 coord 20, 10) + screen.printScreen() + + val screen2 = screen.traverseMap { false } + screen2.drawCircleFilled(35 coord 35, 10) + + val merge = + screen.mergeWith(screen2, false) { first: Boolean, second: Boolean -> first xor second } + merge.printScreen() + + val screenColor = screen + .traverseMapIndexed { x, y, t -> x shl 16 or y shl 8 } + .also { + it.traverseMap { ColorInt(it) }.printScreenColor() + } + + val screenColor2 = screen + .traverseMap { t -> if (t) 0xFF0000 else 0 } + + val also = screenColor.mergeWith(screenColor2, 0) { first: Int, second: Int -> + if (second != 0) second else first + }.also { + it.traverseMap { ColorInt(it) }.printScreenColor() + } + + val traverseMap = screen.traverseMap { false } + traverseMap.drawLine(23 coord 20, 40 coord 75) + (also to traverseMap).merge(0) { first, second -> + if (second) 0xffffff else first + }.also { + it.traverseMap { ColorInt(it) }.printScreenColor() + } + + (0..5).forEach { + also.addLayer((it * 192 % 256) shl 16 or 0x00AA88) { + drawCircle( + getRandomCircleCoordinate(it), + 5 + ) + } + } + also.also { + it.traverseMap { ColorInt(it) }.printScreenColor() + } + +// circleBres(40, 30, 20).also { +// val (list, rect) = it +// screen.traverseAssign(list, true) +// screen.fillPolygon(rect.modifyBy(gy = -1), true) +// } +// +// printScreen(screen) +} + +private fun getRandomCircleCoordinate(it: Int) = + it * 7 + (Random.nextInt() % 6) coord 32 + (Random.nextInt() % 20) * if (Random.nextBoolean()) 1 else -1 + +private fun List2D.printScreen() { + traverseMap { t -> if (t) "XX" else ".." }.printAll("") +} + +private fun List2D.printScreenColor() { + traverseMap { t -> + " ".style( + colorBackground = AnsiColor.Custom(t) + ) + }.printAll("") +} + +inline fun Array>.toList2D() = List2D.from(map { rows -> rows.toList() }) + +inline fun Pair, List2D>.merge( + default: R, + crossinline onElement: (first: T, second: S) -> R, +): List2D = first.mergeWith(second, default, onElement) + +fun List2D.addLayer( + color: Int, + config: List2D.() -> Unit, +) { + val other = traverseMap { false }.also(config) + this.traverseMutate { x, y, each -> + if (other.isValidCoordinate(x coord y)) { + if (other.at(x, y)) return@traverseMutate color + } + return@traverseMutate each + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ImagePrinter.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ImagePrinter.kt index 84ae998..15fcc32 100644 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/ImagePrinter.kt +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/ImagePrinter.kt @@ -1,7 +1,6 @@ package dev.patbeagan.consolevision import dev.patbeagan.consolevision.ansi.AnsiColor -import dev.patbeagan.consolevision.ansi.StyleExtensions.style import dev.patbeagan.consolevision.imagefilter.ColorMutation import dev.patbeagan.consolevision.imagefilter.ColorNormalization import dev.patbeagan.consolevision.types.ColorInt @@ -103,3 +102,74 @@ class ImagePrinter( } } } + +// +//class ImagePrinter { +// enum class CompressionStyle { +// UP_DOWN, DOTS +// } +// +// fun printImageCompressed(read: BufferedImage, compressionStyle: CompressionStyle = UP_DOWN) { +// (read.minY until read.height).chunked(2).forEach { y -> +// (read.minX until read.width).forEach { x -> +// val (a, r, g, b) = read.getRGB(x, y[0]).colorIntToARGB() +// val (a1, r1, g1, b1) = read.getRGB(x, y[1]).colorIntToARGB() +// when (compressionStyle) { +// UP_DOWN -> "▄" +// DOTS -> "▓" +// }.style( +// colorBackground = if (a == 0) Colors.Custom(g = 255) else Colors.Custom(r, g, b), +// colorForeground = if (a1 == 0) Colors.Custom(g = 255) else Colors.Custom(r1, g1, b1) +// ).also { print(it) } +// } +// println() +// } +// } +// +// fun printImageReducedPalette(read: BufferedImage) { +// (read.minY until read.height).chunked(2).forEach { y -> +// (read.minX until read.width).forEach { x -> +// val a = read.getRGB(x, y[0]).colorIntToARGB().argbToColorInt(false) +// val b = read.getRGB(x, y[1]).colorIntToARGB().argbToColorInt(false) +// "▄".style( +// colorBackground = Color256.reduceColor16(a).let { Colors.CustomPreset(it ) }, +// colorForeground = Color256.reduceColor16(b).let { Colors.CustomPreset(it ) } +// ).also { print(it) } +// } +// println() +// } +// } +// +// fun printImage(read: BufferedImage) { +// (read.minY until read.height).forEach { y -> +// (read.minX until read.width).forEach { x -> +// val (a, r, g, b) = read.getRGB(x, y).colorIntToARGB() +// " ".style( +// colorBackground = if (a == 0) { +// Colors.Custom(g = 255) +// } else { +// Colors.Custom(r, g, b) +// } +// ).also { print(it) } +// } +// println() +// } +// } +//} +// +//fun Image.convertToBufferedImage(): BufferedImage? { +// if (this is BufferedImage) { +// return this +// } +// +// // Create a buffered image with transparency +// val bi = BufferedImage( +// getWidth(null), getHeight(null), +// BufferedImage.TYPE_INT_ARGB +// ) +// val graphics2D = bi.createGraphics() +// graphics2D.drawImage(this, 0, 0, null) +// graphics2D.dispose() +// return bi +//} +// diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/TerminalStyle.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/TerminalStyle.kt new file mode 100644 index 0000000..37d56ae --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/TerminalStyle.kt @@ -0,0 +1,47 @@ +package dev.patbeagan.consolevision + +import dev.patbeagan.consolevision.ansi.AnsiColor +import dev.patbeagan.consolevision.ansi.AnsiConstants +import dev.patbeagan.consolevision.ansi.AnsiSGR + +/** + * Defines the style for a single character + */ +data class TerminalStyle( + /** + * The foreground color of this character + */ + val colorForeground: AnsiColor = AnsiColor.Default, + /** + * The background color of this character + */ + val colorBackground: AnsiColor = AnsiColor.Default, + /** + * The graphics applied to this character. See [AnsiSGR] for more info. + */ + val sgr: AnsiSGR = AnsiSGR.Normal +) + +fun String.style( + colorForeground: AnsiColor = AnsiColor.Default, + colorBackground: AnsiColor = AnsiColor.Default, + sgr: AnsiSGR = AnsiSGR.Normal +): String = this.style(colorForeground, colorBackground, arrayOf(sgr)) + +fun String.style( + style: TerminalStyle +): String = this.style(style.colorForeground, style.colorBackground, style.sgr) + +fun String.style( + colorForeground: AnsiColor = AnsiColor.Default, + colorBackground: AnsiColor = AnsiColor.Default, + sgr: Array +): String { + val enableStyles: String = sgr.joinToString(";") { it.enable.toString() } + val disableStyles: String = sgr.joinToString(";") { it.disable.toString() } + val startColor = + "${AnsiConstants.CSI}$enableStyles;${colorForeground.foreground};${colorBackground.background}m" + val endColor = + "${AnsiConstants.CSI}$disableStyles;${AnsiColor.Default.foreground};${AnsiColor.Default.background}m" + return startColor + this + endColor +} diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/AnsiSGR.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/AnsiSGR.kt index 2eb695c..0e0ed10 100644 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/AnsiSGR.kt +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/AnsiSGR.kt @@ -28,4 +28,4 @@ enum class AnsiSGR( fun enableString() = "${AnsiConstants.CSI}${this.enable}m" fun disableString() = "${AnsiConstants.CSI}${this.disable}m" -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/Color256.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/Color256.kt index 7c24faa..db5273d 100644 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/Color256.kt +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/Color256.kt @@ -346,38 +346,5 @@ enum class Color256( Color254, Color255 ) - - /** - * Takes any color, and finds the closest matching color from the 256 color set. - * - * @return the closest matching [Color256] - */ - fun reduceColor(color: Int) = values().associateBy { - abs(color - it.color) - }.toSortedMap()[0] - ?.number ?: 0 - - /** - * Takes any color, and finds the closest matching color from - * - * 16 evenly dispersed colors in the 256 color set. - * - * @return one of 16 possible [Color256] - */ - fun reduceColor16(color: Int): Int { - val toSortedMap: Map.Entry? = - values().take(16).associateBy { color256 -> - distance( - color shr 16 and 255, - color shr 8 and 255, - color and 255, - color256.color shr 16 and 255, - color256.color shr 8 and 255, - color256.color and 255 - ) - // abs(color - color256.color) - }.minByOrNull { it.key } - return toSortedMap?.value?.number ?: 0 - } } } diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/StyleExtensions.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/StyleExtensions.kt deleted file mode 100644 index 6b91bb4..0000000 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/ansi/StyleExtensions.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.patbeagan.consolevision.ansi - -object StyleExtensions { - /** - * An extension for styling strings via ansi. - * - * @param colorForeground the color of the text - * @param colorBackground the color of the area behind the text - * @param sgr a list of special effects to apply to the text, such as **bold** - */ - fun String.style( - colorForeground: AnsiColor = AnsiColor.Default, - colorBackground: AnsiColor = AnsiColor.Default, - sgr: AnsiSGR = AnsiSGR.Normal - ): String = this.style(colorForeground, colorBackground, arrayOf(sgr)) - - /** - * An extension for styling strings via ansi. - * - * @param colorForeground the color of the text - * @param colorBackground the color of the area behind the text - * @param sgr a list of special effects to apply to the text, such as **bold** - */ - fun String.style( - colorForeground: AnsiColor = AnsiColor.Default, - colorBackground: AnsiColor = AnsiColor.Default, - sgr: Array - ): String { - val startColor = - "${AnsiConstants.CSI}${sgr.joinToString(";") { it.enable.toString() }};${colorForeground.foreground};${colorBackground.background}m" - val endColor = - "${AnsiConstants.CSI}${sgr.joinToString(";") { it.disable.toString() }};${AnsiColor.Default.foreground};${AnsiColor.Default.background}m" - return startColor + this + endColor - } -} diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/ColorReduction.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/ColorReduction.kt new file mode 100644 index 0000000..02f6d3b --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/ColorReduction.kt @@ -0,0 +1,38 @@ +package dev.patbeagan.consolevision.ext + +import dev.patbeagan.consolevision.ansi.Color256 +import dev.patbeagan.consolevision.util.distance +import kotlin.math.abs + +/** + * Takes any color, and finds the closest matching color from the 256 color set. + * + * @return the closest matching [Color256] + */ +fun reduceColor(color: Int) = Color256.values().associateBy { + abs(color - it.color) +}.toSortedMap()[0] + ?.number ?: 0 + +/** + * Takes any color, and finds the closest matching color from + * + * 16 evenly dispersed colors in the 256 color set. + * + * @return one of 16 possible [Color256] + */ +fun reduceColor16(color: Int): Int { + val toSortedMap: Map.Entry? = + Color256.values().take(16).associateBy { color256 -> + distance( + color shr 16 and 255, + color shr 8 and 255, + color and 255, + color256.color shr 16 and 255, + color256.color shr 8 and 255, + color256.color and 255 + ) + // abs(color - color256.color) + }.minByOrNull { it.key } + return toSortedMap?.value?.number ?: 0 +} diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/StyleExtensions.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/StyleExtensions.kt new file mode 100644 index 0000000..ef15452 --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/ext/StyleExtensions.kt @@ -0,0 +1,33 @@ +package dev.patbeagan.consolevision.ansi + +/** + * An extension for styling strings via ansi. + * + * @param colorForeground the color of the text + * @param colorBackground the color of the area behind the text + * @param sgr a list of special effects to apply to the text, such as **bold** + */ +fun String.style( + colorForeground: AnsiColor = AnsiColor.Default, + colorBackground: AnsiColor = AnsiColor.Default, + sgr: AnsiSGR = AnsiSGR.Normal +): String = this.style(colorForeground, colorBackground, arrayOf(sgr)) + +/** + * An extension for styling strings via ansi. + * + * @param colorForeground the color of the text + * @param colorBackground the color of the area behind the text + * @param sgr a list of special effects to apply to the text, such as **bold** + */ +fun String.style( + colorForeground: AnsiColor = AnsiColor.Default, + colorBackground: AnsiColor = AnsiColor.Default, + sgr: Array +): String { + val startColor = + "${AnsiConstants.CSI}${sgr.joinToString(";") { it.enable.toString() }};${colorForeground.foreground};${colorBackground.background}m" + val endColor = + "${AnsiConstants.CSI}${sgr.joinToString(";") { it.disable.toString() }};${AnsiColor.Default.foreground};${AnsiColor.Default.background}m" + return startColor + this + endColor +} diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/types/ARGB.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/ARGB.kt new file mode 100644 index 0000000..748ab82 --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/ARGB.kt @@ -0,0 +1,23 @@ +package dev.patbeagan.consolevision.types + +@Deprecated("Please use ColorInt instead, if possible") +data class ARGB( + val a: Int, + val r: Int, + val g: Int, + val b: Int +) + +fun Int.colorIntToARGB(): ARGB = ARGB( + this shr 24 and 255, + this shr 16 and 255, + this shr 8 and 255, + this and 255 +) + +fun ARGB.argbToColorInt(withAlpha: Boolean = true): Int = + (a shl 24).takeIf { withAlpha } ?: 0 + .or(r shl 16) + .or(g shl 8) + .or(b) + diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CompressedPoint.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CompressedPoint.kt index 70601a1..0cabca4 100644 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CompressedPoint.kt +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CompressedPoint.kt @@ -1,13 +1,26 @@ package dev.patbeagan.consolevision.types +import dev.patbeagan.consolevision.util.distance +import kotlin.math.absoluteValue import kotlin.math.pow +import kotlin.math.roundToInt + +infix fun Int.coord(other: Int) = CompressedPoint.from(this, other) +infix fun CompressedPoint.coordRect(other: CompressedPoint) = CoordRect(this, other) /** * Holds an X,Y coordinate as a single Long. * The X portion is the leading half, and the Y portion is the trailing half. */ @JvmInline -value class CompressedPoint(private val base: Long) { +value class CompressedPoint(private val base: Long) : Comparable { + + override fun compareTo(other: CompressedPoint): Int = when { + other.y < y -> -other.distanceFrom(this) + other.x < x -> -other.distanceFrom(this) + else -> other.distanceFrom(this) + }.roundToInt() + /** * The first coordinate in the CompressedPoint */ @@ -18,6 +31,51 @@ value class CompressedPoint(private val base: Long) { */ val y: Int get() = base.toInt() // (base and (2.0.pow(32) - 1).toLong()).toInt() + + /** + * Computes the true distance between the two listed points. + */ + fun distanceFrom(other: CompressedPoint): Double = distance( + x.toDouble(), + y.toDouble(), + other.x.toDouble(), + other.y.toDouble() + ) + + /** + * Computes the manhattan distance between the 2 points. + * + * Manhattan distance is the number of steps that you'd have to take + * to move between the points in a grid system. + * Only purely vertical or horiztonal moves are allowed. + */ + fun distanceManhattanFrom(other: CompressedPoint): Int = + (x - other.x).absoluteValue + (y - other.y).absoluteValue + + /** + * Checks to see if the point is inside the given triangle. + */ + fun isInTriangle( + v1: CompressedPoint, + v2: CompressedPoint, + v3: CompressedPoint + ): Boolean { + fun sign(p1: CompressedPoint, p2: CompressedPoint, p3: CompressedPoint): Float = + (p1.x.toFloat() - p3.x.toFloat()) * + (p2.y.toFloat() - p3.y.toFloat()) - + (p2.x.toFloat() - p3.x.toFloat()) * + (p1.y.toFloat() - p3.y.toFloat()) + + val hasNeg: Boolean + val hasPos: Boolean + val d1: Float = sign(this, v1, v2) + val d2: Float = sign(this, v2, v3) + val d3: Float = sign(this, v3, v1) + hasNeg = d1 < 0 || d2 < 0 || d3 < 0 + hasPos = d1 > 0 || d2 > 0 || d3 > 0 + return !(hasNeg && hasPos) + } + override fun toString(): String = "CompressedPoint of ${this.x} to ${this.y}\n" + " ${Integer.toBinaryString(x)}\n" + diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CoordRect.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CoordRect.kt new file mode 100644 index 0000000..7990dfe --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/CoordRect.kt @@ -0,0 +1,16 @@ +package dev.patbeagan.consolevision.types + +data class CoordRect( + val lesser: CompressedPoint, + val greater: CompressedPoint +) { + fun modifyBy( + lx: Int = 0, + ly: Int = 0, + gx: Int = 0, + gy: Int = 0 + ) = CoordRect( + (this.lesser.x + lx) coord (this.lesser.y + ly), + (this.greater.x + gx) coord (this.greater.y + gy) + ) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/types/List2D.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/List2D.kt new file mode 100644 index 0000000..3b5c118 --- /dev/null +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/types/List2D.kt @@ -0,0 +1,90 @@ +package dev.patbeagan.consolevision.types + +@JvmInline +value class List2D(private val value: MutableList>) : Iterable { + + val height get() = value.size + val width get() = value.firstOrNull()?.size ?: 0 + + fun at(x: Int, y: Int): T = value[y][x] + fun assign(x: Int, y: Int, item: T) { + value[y][x] = item + } + + override fun iterator(): Iterator = iterator { + this@List2D.traverseInternal(value, {}) { _, _, t -> yield(t) } + } + + fun traverseMutate( + onElement: (x: Int, y: Int, each: T) -> T, + ): Unit = value.forEachIndexed { y, row -> + row.forEachIndexed { x, t -> value[y][x] = onElement(x, y, t) } + } + + fun traverseMapIndexed( + onElement: (x: Int, y: Int, T) -> R, + ): List2D = from( + value.mapIndexed { y, r -> + r.mapIndexed { x, it -> onElement(x, y, it) } + }) + + fun traverseAssign(list: List, t: T) { + list.forEach { + if (it.y in this.value.indices && it.x in this.value[0].indices) { + this.value[it.y][it.x] = t + } + } + } + + fun traverseMap( + onElement: (T) -> R, + ): List2D = traverseMapIndexed { _, _, t -> onElement(t) } + + fun traverse( + onRowEnd: () -> Unit = {}, + onElement: (x: Int, y: Int, T) -> Unit, + ): Unit = traverseInternal(value, onRowEnd, onElement) + + private inline fun traverseInternal( + list: MutableList>, + onRowEnd: () -> Unit = {}, + onElement: (x: Int, y: Int, T) -> Unit, + ): Unit = list.forEachIndexed { y, r -> + r.forEachIndexed { x, t -> onElement(x, y, t) } + onRowEnd() + } + + fun isValidCoordinate(c: CompressedPoint): Boolean = + value.isNotEmpty() && + value.all { it.size == value[0].size } && + c.y in 0..value.size && + c.x in 0..value[0].size + + fun printAll(delimiter: String = "\t") { + traverse({ println() }) { _, _, t -> print("$t$delimiter") } + } + + inline fun mergeWith( + other: List2D, + default: R, + crossinline onElement: (first: T, second: S) -> R, + ): List2D = this.traverseMapIndexed { x, y, t -> + if (other.isValidCoordinate(x coord y)) { + onElement(this.at(x, y), other.at(x, y)) + } else { + default + } + } + + fun flatten(): List { + val ret = mutableListOf() + iterator().forEach { ret.add(it) } + return ret + } + + companion object { + fun from(value: List>) = + List2D(value.map { it.toMutableList() }.toMutableList()) + } +} + diff --git a/lib/src/main/kotlin/dev/patbeagan/consolevision/util/Distance.kt b/lib/src/main/kotlin/dev/patbeagan/consolevision/util/Distance.kt index 6aca875..ed0362c 100644 --- a/lib/src/main/kotlin/dev/patbeagan/consolevision/util/Distance.kt +++ b/lib/src/main/kotlin/dev/patbeagan/consolevision/util/Distance.kt @@ -1,6 +1,10 @@ package dev.patbeagan.consolevision.util +import dev.patbeagan.consolevision.types.CompressedPoint +import kotlin.math.cos import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sin import kotlin.math.sqrt /** @@ -19,3 +23,16 @@ fun distance( val d3 = (z2 - z1).toDouble().pow(2) return sqrt(d1 * d2 * d3) } + +fun distance(xy1: Pair, xy2: Pair) = + distance(xy1.first, xy1.second, xy2.first, xy2.second) + +fun distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = + sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)) + +fun toCartesian(range: Double, angle: Double): Pair = + range * cos(angle) to range * sin(angle) + +fun convertToPointAndTranslateBy(x: Int, y: Int): (Pair) -> CompressedPoint = { + CompressedPoint.from(x + it.first.roundToInt(), y + it.second.roundToInt()) +} \ No newline at end of file diff --git a/lib/src/test/kotlin/dev/patbeagan/consolevision/util/TerminalColorStyleTest.kt b/lib/src/test/kotlin/dev/patbeagan/consolevision/util/TerminalColorStyleTest.kt index 4174937..e6ea086 100644 --- a/lib/src/test/kotlin/dev/patbeagan/consolevision/util/TerminalColorStyleTest.kt +++ b/lib/src/test/kotlin/dev/patbeagan/consolevision/util/TerminalColorStyleTest.kt @@ -9,7 +9,7 @@ import dev.patbeagan.consolevision.ansi.AnsiColor.Red import dev.patbeagan.consolevision.ansi.AnsiConstants.ESC import dev.patbeagan.consolevision.ansi.AnsiSGR import dev.patbeagan.consolevision.ansi.Color256 -import dev.patbeagan.consolevision.ansi.StyleExtensions.style +import dev.patbeagan.consolevision.ansi.style import dev.patbeagan.consolevision.types.ColorInt import org.junit.Test