functions/utility/New-MiniGameDrawUtility.ps1

function New-MiniGameDrawUtility {
    <#
    .SYNOPSIS
    This return a utility class for drawing text on the screen.
 
    .DESCRIPTION
    This return a utility class for drawing text on the screen.
 
    Text can be single or multiline by passing a [System.String[]]-Array.
     
    By default X=0,Y=0 points to the top-rightmost-corner. Both Axis can be inverted.
 
    Foreground and Background Colors can be changed to:
    - predefined colors Red, Green, Yellow, etc.
    - a 8-Bit value of predefined colors.
    - passing RGB values for a specific color.
 
    Can defined custom boundaries as the canvas. (Defaults to full screen)
    Inside the main boundary, sub-boundaries can be created for:
    - checking objects to be out of bounds.
    - drawing objects relative to some sub-boundary.
 
    .OUTPUTS
 
     
    .EXAMPLE
 
    Create a new MiniGameDrawUtility object and draw a text in color on the screen:
 
    PS> $canvas = New-MiniGameDrawUtility
        $canvas.ForeGround.setColor("Red")
        $canvas.draw(10, 1, "Hello World 1")
        $canvas.draw(10, 2, "Hello World 2")
        $canvas.ForeGround.Reset()
 
        # Only apply color once to the next draw.
        $canvas.ForeGround.onceColor("Cyan")
        $canvas.draw(10, 4, "Hello World 3")
        $canvas.draw(10, 5, "Hello World 4")
 
        # Undraw text on the position.
        # $canvas.undraw(10, 25, "Hello World 3")
 
        # Draw multiline text.
        $canvas.draw(10, 7, @(
            "Hello World 4",
            "Hello World 5"
        ))
 
     
 
    .LINK
     
    #>


    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $false
        )]
        [System.Int32]
        $MaximumX,

        [Parameter(
            Position = 1,
            Mandatory = $false
        )]
        [System.Int32]
        $MaximumY,

        [Parameter(
            Position = 2,
            Mandatory = $false
        )]
        [System.Int32]
        $MinimumX = 0,

        [Parameter(
            Position = 3,
            Mandatory = $false
        )]
        [System.Int32]
        $MinimumY = 0,


        # Inverts drawing on the Y-Axis. Normally Y=0 points to the top of the screen.
        # This is usefull for having elements fly from the bottom upwards.
        [Parameter()]
        [switch]
        $InvertY,

        # Inverts drawing on the X-Axis. Normally X=0 points to the left side of the screen.
        # Might not be useful but complement to InvertY
        $InvertX,

        # This automatically trims empty spaces from inputet strings.
        [Parameter(
            Mandatory = $false
        )]
        [System.Boolean]
        $TrimSpaces = $true
    )

    # If no values are provided, use the current buffer size.
    # TODO: Maybe make it dynamic when the screen size changes.
    if ($null -EQ $PSBoundParameters['MaximumX'] -OR $MaximumX -LT 0) {
        $MaximumX = $host.UI.RawUI.BufferSize.Width
    }
    if ($null -EQ $PSBoundParameters['MaximumY'] -OR $MaximumY -LT 0) {
        $MaximumY = $host.UI.RawUI.BufferSize.Height
    }

    $ColorCodes = @{
        DarkGray    = @{
            FOREGROUND = "`e[30m"
            BACKGROUND = "`e[40m"
        }
        DarkRed     = @{
            FOREGROUND = "`e[31m"
            BACKGROUND = "`e[41m"
        }
        DarkGreen   = @{
            FOREGROUND = "`e[32m"
            BACKGROUND = "`e[42m"
        }
        DarkYellow  = @{
            FOREGROUND = "`e[33m"
            BACKGROUND = "`e[43m"
        }
        DarkBlue    = @{
            FOREGROUND = "`e[34m"
            BACKGROUND = "`e[44m"
        }
        DarkMagenta = @{
            FOREGROUND = "`e[35m"
            BACKGROUND = "`e[45m"
        }
        DarkCyan    = @{
            FOREGROUND = "`e[36m"
            BACKGROUND = "`e[46m"
        }
        White       = @{
            FOREGROUND = "`e[37m"
            BACKGROUND = "`e[47m"
        }
        Black       = @{
            FOREGROUND = "`e[30m"
            BACKGROUND = "`e[40m"
        }
        Default     = @{
            FOREGROUND = "`e[39m"
            BACKGROUND = "`e[49m"
        }
        Gray        = @{
            FOREGROUND = "`e[90m"
            BACKGROUND = "`e[100m"
        }
        Red         = @{
            FOREGROUND = "`e[91m"
            BACKGROUND = "`e[101m"
        }
        Green       = @{
            FOREGROUND = "`e[92m"
            BACKGROUND = "`e[102m"
        }
        Yellow      = @{
            FOREGROUND = "`e[93m"
            BACKGROUND = "`e[103m"
        }
        Blue        = @{
            FOREGROUND = "`e[94m"
            BACKGROUND = "`e[104m"
        }
        Magenta     = @{
            FOREGROUND = "`e[95m"
            BACKGROUND = "`e[105m"
        }
        Cyan        = @{
            FOREGROUND = "`e[96m"
            BACKGROUND = "`e[106m"
        }
        #White = @{
        # FOREGROUND = "`e[97m"
        # BACKGROUND = "`e[107m"
        #}
    }


    class MiniGameDrawUtilityGraphics {

        [System.String] $_mode = $null
        [System.String] $_code = $null
        [System.Boolean] $_autoReset = $false 
        [System.Object] $_colorCodes = $ColorCodes



        [void] Reset() {
            $this._code = $null
            $this._autoReset = $false
        }
        [void] _draw() {
            if ($null -NE $this._code) {
                [System.Console]::Write($this._code)
                if ($this._autoReset) {
                    $this.Reset()
                }
            }
        }



        MiniGameDrawUtilityGraphics([System.String] $mode) {
            $this._mode = $mode
        }

        
        <#
         
            Methods for setting colors
            Once: Resets automatically after draw has been called
            Set: Keeps color permanent, until call of Reset() or setting another color.
 
        #>

        [void] OnceColor([System.ConsoleColor] $color) {
            $this._code = $this._colorCodes."$color"."$($this._mode)"
            $this._autoReset = $true
        }
        [void] setColor([System.ConsoleColor] $color) {
            $this._code = $this._colorCodes."$color"."$($this._mode)"
        }


        [void] Once8Bit([System.Byte] $byte) {
            $this.set8Bit($byte)
            $this._autoReset = $true
        }
        [void] Set8Bit([System.Byte] $byte) {
            $mode = $this._mode -EQ "FOREGROUND" ? 38 : 48
            $this._code = "`e[$($mode);5;${byte}m"
        }


        [void] OnceRGB([System.Byte[]]$rgb) {
            $this.setRGB($rgb[0], $rgb[1], $rgb[2])
            $this._autoReset = $true
        }
        [void] OnceRGB(            
            [System.Byte] $red,
            [System.Byte] $green,
            [System.Byte] $blue
        ) {
            $this.setRGB($red, $blue, $green)
            $this._autoReset = $true
        }
        [void] SetRGB([System.Byte[]] $rgb) {
            $this.SetRGB($rgb[0], $rgb[1], $rgb[2])
        }
        [void] SetRGB(            
            [System.Byte] $red,
            [System.Byte] $green,
            [System.Byte] $blue
        ) {
            $mode = $this._mode -EQ "FOREGROUND" ? 38 : 48
            $this._code = "`e[$($mode);2;${red};${green};${blue}m"
        }
    }


    <#
     
        This defines a boundary on the terminal. Boundaries can be used for:
        - Out-Of-Bounds Checks
        - Drawing inside a boundary
 
        Each boundary can have an offset, which allows multiple different boundaries.
 
        The canvases __default__ boundary has an offset of X=0;Y=0.
    #>

    class MiniGameDrawUtilityBounds {
        [System.String] $Name
        [System.Numerics.Vector2] $Offset
        [System.Numerics.Vector2] $Minimum
        [System.Numerics.Vector2] $Maximum
        [MiniGameDrawUtilityBounds] Init() {
            $this.Minimum = [System.Numerics.Vector2]@{
                X = $this.Offset.X + $this.Minimum.X 
                Y = $this.Offset.Y + $this.Minimum.Y
            }
            $this.Maximum = [System.Numerics.Vector2]@{
                X = $this.Offset.X + $this.Maximum.X
                Y = $this.Offset.Y + $this.Maximum.Y
            }
            return $this
        }
        [System.Boolean] IsOutside(
            [System.Int32] $x,
            [System.Int32] $y
        ) {
            return $this.Offset.X + $x -GT $this.Maximum.X -OR
            $this.Offset.Y + $y -GT $this.Maximum.Y -OR
            $this.Offset.X + $x -LT $this.Minimum.X -OR
            $this.Offset.Y + $y -LT $this.Minimum.Y
        }
    }



    class MiniGameDrawUtilityBoundsManager {

        # All boundaries are set relative to the canvas __default__ boundary.
        [MiniGameDrawUtilityBounds] $Current
        [System.Collections.Hashtable] $Bounds = @{}
        [MiniGameDrawUtilityBounds] $Default = [MiniGameDrawUtilityBounds]@{
            Name    = '__default__'
            Offset  = [System.Numerics.Vector2]::Zero
            Minimum = [System.Numerics.Vector2]@{X = $MinimumX; Y = $MinimumY }
            Maximum = [System.Numerics.Vector2]@{X = $MaximumX; Y = $MaximumY }
        }



        MiniGameDrawUtilityBoundsManager() {
            $this.Current = $this.Default
            $this.Bounds.Add($this.Current.Name, $this.Current)
        }


        <#
         
            Adds boundaries inside the __default__ canvas boundary.
 
        #>

        [void] Add(
            [System.String] $name,
            [System.Int32] $minX,
            [System.Int32] $minY,
            [System.Int32] $maxX,
            [System.Int32] $maxY
        ) {
            $bound = [MiniGameDrawUtilityBounds]@{
                Name    = $name
                Offset  = $this.Default.Minimum
                Minimum = [System.Numerics.Vector2]@{X = $minX; Y = $minY }
                Maximum = [System.Numerics.Vector2]@{X = $maxX; Y = $maxY }
            }
            $this.Bounds.Remove($name)
            $this.Bounds.Add($name, $bound.Init())
        }
        [void] Set([System.String] $name) {
            $this.Current = $this.Bounds[$name]
        }
        [void] Reset() {
            $this.Set($this.Default.Name)
        }


        <#
 
            Out of Bounds checks.
 
        #>

        [System.Boolean] IsOutside(
            [System.String] $name,
            [System.Int32] $x,
            [System.Int32] $y
        ) {
            return $this.Bounds[$name].IsOutside($x, $y)
        }
        [System.Boolean] IsOutside(
            [System.Int32] $x,
            [System.Int32] $y
        ) {
            return $this.IsOutside($this.Current.name, $X, $Y)
        }
        [System.Boolean] IsOutside(
            [System.String] $name,
            [System.Numerics.Vector2] $pos
        ) {
            return $this.IsOutside($name, $pos.X, $pos.Y)
        }
        [System.Boolean] IsOutside(
            [System.Numerics.Vector2] $pos
        ) {
            return $this.IsOutside($this.Current.name, $pos.X, $pos.Y)
        }



        <#
 
            These methods are used to move a vector on the screen relative to the bounds.
 
        #>

        [System.Numerics.Vector2] Move(
            [System.Numerics.Vector2] $position,
            [System.Int32] $x,
            [System.Int32] $y
        ) {
            return $this.Move(
                [System.Numerics.Vector2]@{
                    X = $position.X + $x; 
                    Y = $position.Y + $y 
                }
            )
        }
        [System.Numerics.Vector2] Move(
            [System.Numerics.Vector2] $position,
            [System.Numerics.Vector2] $move
        ) {
            $position.Add($move)
            if ($position.X -LT $this.Current.Minimum.X) {
                $position.X = $this.Current.Maximum.X
            }
            elseif ($position.X -GT $this.Current.Maximum.X) {
                $position.X = $this.Current.Minimum.X
            }
            if ($position.Y -LT $this.Current.Minimum.Y) {
                $position.Y = $this.Current.Maximum.Y
            }
            elseif ($position.Y -GT $this.Current.Maximum.Y) {
                $position.Y = $this.Current.Minimum.Y
            }

            return $position
        }
    }



    class MiniGameDrawUtility {

        [System.Boolean] $InvertX = $InvertX.IsPresent
        [System.Boolean] $InvertY = $InvertY.IsPresent
        [System.Boolean] $_trimSpaces = $TrimSpaces

        [MiniGameDrawUtilityGraphics] $ForeGround = [MiniGameDrawUtilityGraphics]::new("FOREGROUND")
        [MiniGameDrawUtilityGraphics] $BackGround = [MiniGameDrawUtilityGraphics]::new("BACKGROUND")
        [MiniGameDrawUtilityBoundsManager] $Bounds = [MiniGameDrawUtilityBoundsManager]@{}

        [void] Clear() {
            [System.Console]::Clear()
        }
        [void] ResetGraphics() {
            $this.ForeGround.Reset()
            $this.BackGround.Reset()
        }



        <#
         
            These methods draw text on the screen, offset from the right side.
         
        #>

        [void] DrawRight(
            [System.Numerics.Vector2] $pos,
            [System.String[]] $text
        ) {
            $this.DrawRight($pos.X, $pos.Y, $text)
        }
        [void] DrawRight(
            [System.Int32] $x,
            [System.Int32] $y,
            [System.String[]] $text
        ) {
            $this.Draw(($this.Maximum.X - $this.Minimum.X) - $text[0].Length - $x, $y, $text)
        }
        [void] UndrawRight(
            [System.Numerics.Vector2] $pos,
            [System.String[]] $text
        ) {
            $this.Undraw($pos.X, $pos.Y, $text)
        }
        [void] UndrawRight(
            [System.Int32] $x,
            [System.Int32] $y,
            [System.String[]] $text
        ) {
            $this.Undraw(($this.Maximum.X - $this.Minimum.X) - $text[0].Length - $x, $y, $text)
        }



        <#
 
            These methods are used to draw text on the screen.
 
            NOTE:
            When TrimSpaces=$true, drawing empty spaces won't have effect. Either the element on screen
            - has to be overdrawn.
            - or undraw() has to be used.
 
        #>

        [void] Draw(
            [System.String[]] $text
        ) {
            $this.Draw(0, 0, $text)
        }
        [void] Draw(
            [System.Numerics.Vector2] $pos,
            [System.String[]] $text
        ) {
            $this.Draw($pos.X, $pos.Y, $text)
        }
        [void] Draw(
            [System.Int32] $x,
            [System.Int32] $y,
            [System.String[]] $text
        ) {
            $this.ForeGround._draw()
            $this.BackGround._draw()
            $this._internalDraw($x, $y, $text, $false)
            [System.Console]::Write("`e[0m")
        }



        <#
 
            These methods are used to delete text from the screen.
 
        #>

        [void] Undraw(
            [System.String[]] $text
        ) {
            $this.Undraw(0, 0, $text)
        }
        [void] Undraw(
            [System.Numerics.Vector2] $pos,
            [System.String[]] $text
        ) {
            $this.Undraw($pos.X, $pos.Y, $text)
        }
        [void] Undraw(
            [System.Int32] $x,
            [System.Int32] $y,
            [System.String[]] $text
        ) {
            $this._internalDraw($x, $y, $text, $true)
        }



        <#
         
            This is an internal method for drawing.
         
        #>

        [void] _internalDraw(
            [System.Int32] $x,
            [System.Int32] $y,
            [System.String[]] $text,
            [System.Boolean] $undraw
        ) {

            $canvasX = $this.Minimum.X + $x
            $canvasY = $this.Minimum.y + $y
  
            if ($this.InvertY) {
                $canvasY = $this.Maximum.Y - ($canvasY + 1)
            }
            if ($this.InvertX) {
                $canvasX = $this.Maximum.X - ($canvasX + 1)
            }

            # - In normal mode: (0,0) should be top most of screen and (0,maxY-1) should be bottom of the screen.
            # - In inverted mode: (0,0) should be the bottom most of the screen and (0,minY-1) should be the topmost of the screen.
            
            # 0 is topmost of screen.
            # If it goes out of the screen upwards only the lowest part gets drawn at Y=0

            # ObjectHeight: 3 - drawing starts from pos 0 to 2
            # At Y=0 => 1 Part of object is drawn
            # At Y=3 => 3 Parts of object is drawn
            $indexYStart = ($text.Count - 1) - ($canvasY - $this.Minimum.Y)
            $indexYStart = [System.Math]::Max(0, $indexYStart)

            # Max is bottommost of screen.
            # If it goes out of the screen downwards only the upper part gets drawn at Y=Max-1

            # ScreenMax is 13 (Example)
            # ObjectHeight: 3 - drawing starts from pos 0 to 2
            # At Y=12 => 1 Part of object is drawn
            # At Y=10 => 3 Parts of object is drawn
            $indexYEnd = ($this.Maximum.Y - $canvasY) + 1 # Because -LT check in loop
            $indexYEnd = [System.Math]::Min($text.Count, $indexYEnd)
                
            # Because when drawing multiple lines, the acual posY is the bottom-left.
            # But When looping we want to start at the top-left.
            $canvasY = $canvasY - ($text.Count - 1)
                
            for ($index = $indexYStart; $index -LT $indexYEnd; $index++) {

                $line = $text[$index]
                $drawY = $canvasY + $index
                $drawX = $canvasX

                # This removes whitespace, but offsets the drawnX by the removed whitespaces.
                if ($this._trimSpaces) {
                    $line = $line.TrimStart()
                    $drawX += $text[$index].Length - $line.Length
                    $line = $line.TrimEnd()
                }

                # Skip drawing when fully outside of the left screen.
                if (($drawX + $line.Length) -LT $this.Minimum.X) {
                    continue
                }
                # Handle line only being partially outside of the left screen.
                elseif ($drawX -LT $this.Minimum.X) {
                    # 0 is leftmost of the screen
                    # If it goes out of the screen to the left the rightmost part is drawn.

                    $diff = $this.Minimum.X - $drawX
                    $line = $line.Substring($diff)

                    # DrawX needs to be adjusted to be still inside of the screen.
                    $drawX = $this.Minimum.X
                }

                # Skip drawing when fully outside of the right screen.
                if ($drawX -GE $this.Maximum.X) {
                    continue
                } 
                # Handle line only being partially outside of the right screen.
                elseif (($drawX + $line.Length) -GT $this.Maximum.X) {
                    # MaxX-1 is the rightmost of the screen
                    # If it goes out of the screen to the right, the leftmost part is drawn.
                    $line = $line.Substring(0, $this.Maximum.X - $drawX)
                }

                if ($undraw) {
                    [System.Console]::SetCursorPosition($drawX, $drawY)
                    [System.Console]::Write(" " * $line.Length)
                }
                else {
                    [System.Console]::SetCursorPosition($drawX, $drawY)
                    [System.Console]::Write($line)
                }
            }
        }
    }

    if (
        $null -EQ (Get-TypeData -TypeName  'MiniGameDrawUtility').Members.Minimum
    ) {
        Update-TypeData -TypeName  'MiniGameDrawUtility' -MemberName  'Minimum'  -Value { return $this.Bounds.Current.Minimum } -MemberType  ScriptProperty
    }
    if (
        $null -EQ (Get-TypeData -TypeName  'MiniGameDrawUtility').Members.Maximum
    ) {
        Update-TypeData -TypeName  'MiniGameDrawUtility' -MemberName  'Maximum'  -Value { return $this.Bounds.Current.Maximum } -MemberType  ScriptProperty
    }

    return [MiniGameDrawUtility]@{}

}