Private/SlideLayoutPlan.ps1

function Assert-TerminalElementPadding {
    param(
        [Parameter(Mandatory)][TerminalSlides.Schema.V1.SlideElement]$Element,
        [Parameter(Mandatory)][hashtable]$Region,
        [Parameter(Mandatory)][string]$RegionName
    )

    $borderWidth = if ($Element.Border) { 2 } else { 0 }
    $maximumPadding = [Math]::Floor(($Region.Width - $borderWidth - 1) / 2)
    if ($Element.Padding -lt 0 -or $Element.Padding -gt $maximumPadding) {
        throw "Element padding '$($Element.Padding)' is invalid for region '$RegionName' width '$($Region.Width)'. Padding must be between 0 and $maximumPadding to leave content space after two-sided padding and border cells."
    }
}

function Get-TerminalSlideLayoutPlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][TerminalSlides.Schema.V1.TerminalPresentation]$Presentation,
        [Parameter(Mandatory)][int]$SlideIndex,
        [int]$RevealStep = [int]::MaxValue,
        [Parameter(Mandatory)][TerminalSlides.Schema.V1.TerminalCapability]$Capability
    )

    $theme = Resolve-TerminalPresentationTheme -Presentation $Presentation
    $dimensions = Get-SlideRenderDimensions -Presentation $Presentation -Capability $Capability
    $slide = $Presentation.Slides[$SlideIndex]
    [void](Get-TerminalSlideMaximumRevealStep -Slide $slide)
    $regions = Get-LayoutRegions -Layout $slide.Layout -Width $dimensions.Width -Height $dimensions.Height
    $elementRegionNames = @(Get-TerminalElementRegionNames -Regions $regions)
    $resolvedElementRegions = @(
        foreach ($element in $slide.Elements) {
            if ([string]::IsNullOrWhiteSpace($element.Region)) { 'Content' } else { $element.Region }
        }
    )
    foreach ($regionName in $resolvedElementRegions) {
        if ($regionName -notin $elementRegionNames) {
            throw "Region '$regionName' is not available in layout '$($slide.Layout)'. Supported element regions: $($elementRegionNames -join ', ')."
        }
    }
    Assert-TerminalElementRegionCombination -Layout $slide.Layout -Regions $regions -RegionNames $resolvedElementRegions
    for ($elementIndex = 0; $elementIndex -lt $slide.Elements.Count; $elementIndex++) {
        $regionName = $resolvedElementRegions[$elementIndex]
        Assert-TerminalElementPadding -Element $slide.Elements[$elementIndex] -Region $regions[$regionName] -RegionName $regionName
    }
    $revealedElements = @($slide.Elements | Where-Object { $_.RevealStep -le $RevealStep })
    $placements = [System.Collections.Generic.List[object]]::new()
    $overflowLines = 0

    if ($regions.ContainsKey('Title') -and $slide.Title) {
        $title = New-InternalSlideElement -Kind Title -Payload ([TerminalSlides.Schema.V1.TextPayload]::new($slide.Title)) -ForegroundColor $theme.Heading
        $rawTitleLines = ConvertTo-ElementLines -Element $title -Theme $theme -Width $regions.Title.Width -Capability $Capability -StartColumn $regions.Title.X
        $titleLines = ConvertTo-TerminalPreparedLines -Lines $rawTitleLines -StartColumn $regions.Title.X -MaxWidth $regions.Title.Width -Alignment $title.Alignment
        $placements.Add([pscustomobject]@{
            Element = $title
            Lines = $titleLines
            Region = $regions.Title
            StartY = $regions.Title.Y
            Border = $false
            BorderHeight = 0
        })
        $overflowLines += @($titleLines | Where-Object { $_.Width -gt $_.AvailableWidth }).Count
        $overflowLines += [Math]::Max(0, $titleLines.Count - $regions.Title.Height)

        if ($Presentation.Subtitle -and $slide.Layout -eq 'Title') {
            $subtitle = New-InternalSlideElement -Kind Subtitle -Payload ([TerminalSlides.Schema.V1.TextPayload]::new($Presentation.Subtitle)) -ForegroundColor $theme.Muted
            $subtitleRegion = $regions.Subtitle
            $rawSubtitleLines = ConvertTo-ElementLines -Element $subtitle -Theme $theme -Width $subtitleRegion.Width -Capability $Capability -StartColumn $subtitleRegion.X
            $subtitleLines = ConvertTo-TerminalPreparedLines -Lines $rawSubtitleLines -StartColumn $subtitleRegion.X -MaxWidth $subtitleRegion.Width -Alignment $subtitle.Alignment
            $placements.Add([pscustomobject]@{
                Element = $subtitle
                Lines = $subtitleLines
                Region = $subtitleRegion
                StartY = $subtitleRegion.Y
                Border = $false
                BorderHeight = 0
            })
            $overflowLines += @($subtitleLines | Where-Object { $_.Width -gt $_.AvailableWidth }).Count
            $overflowLines += [Math]::Max(0, $subtitleLines.Count - $subtitleRegion.Height)
        }
    }

    foreach ($regionName in $elementRegionNames) {
        $region = $regions[$regionName]
        $y = $region.Y
        $elements = $revealedElements | Where-Object {
            $resolvedRegion = if ([string]::IsNullOrWhiteSpace($_.Region)) { 'Content' } else { $_.Region }
            $resolvedRegion -eq $regionName
        }

        foreach ($element in $elements) {
            if ($element.Border) {
                $contentWidth = [Math]::Max(1, $region.Width - 2 - ($element.Padding * 2))
                $remainingHeight = [Math]::Max(0, $region.Height - ($y - $region.Y))
                $contentStartColumn = $region.X + 1 + $element.Padding
                $rawLines = ConvertTo-ElementLines -Element $element -Theme $theme -Width $contentWidth -Height ([Math]::Max(1, $remainingHeight - 2)) -Capability $Capability -StartColumn $contentStartColumn
                $lines = ConvertTo-TerminalPreparedLines -Lines $rawLines -StartColumn $contentStartColumn -MaxWidth $contentWidth -Alignment $element.Alignment
                $requiredHeight = [Math]::Max(3, $lines.Count + 2)
                $visibleHeight = [Math]::Min($remainingHeight, $requiredHeight)
                if ($visibleHeight -ge 3) {
                    $innerRegion = @{ X=$region.X + 1; Y=$y + 1; Width=$region.Width - 2; Height=$visibleHeight - 2 }
                    $placements.Add([pscustomobject]@{
                        Element = $element
                        Lines = $lines
                        Region = $innerRegion
                        StartY = $y + 1
                        Border = $true
                        BorderRegion = @{ X=$region.X; Y=$y; Width=$region.Width; Height=$visibleHeight }
                        BorderHeight = $visibleHeight
                    })
                }
                $overflowLines += @($lines | Where-Object { $_.Width -gt $_.AvailableWidth }).Count
                $overflowLines += [Math]::Max(0, $requiredHeight - $remainingHeight)
                $y += $requiredHeight + 1
            }
            else {
                $contentWidth = [Math]::Max(1, $region.Width - ($element.Padding * 2))
                $remainingHeight = [Math]::Max(0, $region.Height - ($y - $region.Y))
                $contentStartColumn = $region.X + $element.Padding
                $rawLines = ConvertTo-ElementLines -Element $element -Theme $theme -Width $contentWidth -Height ([Math]::Max(1, $remainingHeight)) -Capability $Capability -StartColumn $contentStartColumn
                $lines = ConvertTo-TerminalPreparedLines -Lines $rawLines -StartColumn $contentStartColumn -MaxWidth $contentWidth -Alignment $element.Alignment
                $placements.Add([pscustomobject]@{
                    Element = $element
                    Lines = $lines
                    Region = $region
                    StartY = $y
                    Border = $false
                    BorderHeight = 0
                })
                $overflowLines += @($lines | Where-Object { $_.Width -gt $_.AvailableWidth }).Count
                $overflowLines += [Math]::Max(0, $lines.Count - $remainingHeight)
                $y += $lines.Count + 1
            }
        }
    }

    return [pscustomobject]@{
        Dimensions = $dimensions
        Theme = $theme
        Slide = $slide
        Regions = $regions
        Placements = $placements.ToArray()
        OverflowLines = $overflowLines
    }
}