Invoke-OpenSCAD.ps1

function Invoke-OpenScad
{
    <#
    .Synopsis
        Invokes OpenScad
    .Description
        Invokes OpenSCAD
    .Example
        Invoke-OpenSCAD -ScadFilePath .\MyDesign.scad
    .Example
        Invoke-OpenSCAD -ScadFilePath .\MyCustomizableDesign.scad -Parameter @{ThingWidth=10}
    .Example
        Invoke-OpenSCAD -ScadFilePath .\MyCustomizableDesign.scad -CustomizerPreset MyPreset
    .LINK
        Get-OpenSCAD
    #>

    [CmdletBinding(PositionalBinding=$false)]
    param(
    # The path to an OpenSCAD file.
    [Parameter(Mandatory,ParameterSetName='ScadFilePath',ValueFromPipelineByPropertyName,Position=0)]
    [alias('FullName', 'InputPath')]
    [string]
    $ScadFilePath,

    # A dictionary of parameters
    [Parameter(ValueFromPipelineByPropertyName)]
    [Collections.IDictionary]
    [Alias('Parameters')]
    $Parameter,

    # A customizer parameter file.
    # If no file is provided, and a -CustomizerPreset is used, then the -ScadFilePath.json will be used.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('CustomFile')]
    [string[]]    
    $CustomizerFile,

    # The name of one or more presets within a -CustomizerFile.
    # One output will be generated per preset, and will have the name FileName-Preset.stl
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('CustomPreset','CustomPresets','Preset','PresetName','PresetNames')]
    [string[]]
    $CustomizerPreset,

    # The output path. If not provided, this will be the -ScadFilePath, with the extension changed to .stl.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $OutputPath,

    # The path to the OpenScad command. This parameter is not required if openscad is in $env:Path.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $OpenScadCommand,

    # If set, will run as a background job
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $AsJob,

    # Any remaining arguments to OpenSCAD.
    [Parameter(ValueFromPipelineByPropertyName,ValueFromRemainingArguments)]
    [Alias('OpenSCADArguments', 'OpenScadArgs')]
    [string[]]
    $OpenScadArgument,

    # Any parameters to OpenSCAD.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('OpenScadParameters', 'OpenScadParam', 'OpenScadParams')]
    [Collections.IDictionary]
    $OpenScadParameter
    )

    begin {
        function =>[OpenScad.Output] {
            param(
            [string[]]
            $OpenSCADArguments,

            [string]
            $OutputPath
            )
            begin {
                $outputData = [Ordered]@{PSTypeName='OpenScad.Output';OutputPath  =$OutputPath; OpenSCADArguments =$OpenSCADArguments; OpenSCADLog = @()}
                $fixCasing  = [Regex]::new('\s\p{L}')
            }
            process {
                $line =  $_
                $outputData.OpenSCADLog += $line
                if ($line -match '([\w\s]+):\s+([\w\.\:]+)') {
                    $key, $value = $matches.1, $matches.2
                    $key = $key -replace '^\s+'
                    $key = $fixCasing.Replace($key, {
                        $args[0].ToString().Trim().ToUpper()
                    })
                    $outputData[$key] = 
                        if ($value -as [int]) {
                            $value -as [int]
                        } elseif ($value -in 'yes', 'no') {
                            $value -replace 'yes', 'true' -replace 'no' -as [bool]
                        } elseif ($value -as [Timespan]) {
                            $value -as [timespan]
                        } else {
                            $value
                        }
                }
            }
            end {                
                [PSCustomObject]$outputData
            }
        }
    }

    process {
        if ($AsJob) {
            $PSBoundParameters.Remove('AsJob')
            $myDefinition = '' + {
param([Collections.IDictionary]$params)
} + [ScriptBlock]::Create("function $($MyInvocation.MyCommand.Name) {
$($MyInvocation.MyCommand.ScriptBlock)
}

$($MyInvocation.MyCommand.Name) @params
"
)
            Start-Job -ScriptBlock $myDefinition -ArgumentList $PSBoundParameters -Name "$ScadFilePath"
            return
        }

        if (-not $OpenScadCommand) {
            $OpenScadCommand = 'openscad'
        }

        $openScadCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($openScadCommand, 'Application')
        if (-not $openScadCmd) {
            if ($env:ProgramFiles) { 
                $openScadCmd = 
                    $ExecutionContext.SessionState.InvokeCommand.GetCommand(
                        ($env:ProgramFiles,'OpenScad', 'openscad.com' -join [io.path]::DirectorySeparatorChar),
                        'Application'
                    )
            }
            if (-not $openScadCmd) {
                Write-Error "OpenScad not found"
                return
            }            
        }
        
        $resolvedScadPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($ScadFilePath)
        if (-not $resolvedScadPath) { return }
        $scadFileName = ($resolvedScadPath | Split-Path -Leaf) -replace '\.scad$'
        $ScadOutPath = 
            if ($PSBoundParameters['OutputPath']) {
                $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
            } else {
                $resolvedScadPath -replace '\.scad$', '.stl'
            }
        
        $scadContent = @(Get-content $resolvedScadPath)

        $scadParamPath = 
            if ($env:windir -and $env:TEMP) {
                Join-Path $env:TEMP "$scadFileName.json"
            } else {
                Join-Path /tmp "$scadFileName.json"
            }

        
        $openScadArgs = @(
        "$resolvedScadPath"
        "-o"
        )


        if ($OpenScadParameter) {
            $OpenScadArgument += 
                @(foreach ($param in $OpenScadParameter.GetEnumerator()) {
                    $param.Key -replace '^-{0,}', '--'
                    $param.Value
                })
        }

        if ($CustomizerPreset) {
            if (-not $CustomizerFile) {
                $CustomizerFile = $resolvedScadPath -replace '\.scad$', '.json'
            }

            if (Test-Path $CustomizerFile) {
                $customizerPresetFile = Get-Content $CustomizerFile -Raw | ConvertFrom-Json
            }

            foreach ($pre in $CustomizerPreset) {
                $outPath = 
                    if ($PSBoundParameters['OutputPath']) {
                        $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(($OutputPath -replace '\.(.+)$', "-$pre.`$1"))
                    } else {
                        $resolvedScadPath -replace '\.scad$', "-$pre.stl"
                    }
                $preArgs = @() + $openScadArgs + @(
                    $outPath
                    "-p"
                    $CustomizerFile
                    "-P"
                    $pre
                )
                 & $openScadCmd @preArgs @OpenScadArgument *>&1 | =>[OpenScad.Output] -OpenSCADArguments $preArgs -OutputPath $outPath                
            }

            
        } else {
            $openScadArgs += @(                
                "$scadOutPath"
                if ($Parameter) {
                    # '-D'
                    $paramsFileContent = [Ordered]@{}
                    foreach ($p in $Parameter.GetEnumerator()) {
                        $paramMatch = "(?<Name>$($p.Key))\s?=.*$"
                        $matchingLine = $($scadContent -match $paramMatch)
                        if (-not $matchingLine) {
                            Write-Warning "Parameter '$($p.Key)' not found in '$ScadFilePath'"
                            continue
                        }

                        $null = $matchingLine -match $paramMatch
                        $paramName = $matches.Name
                        $paramValue = $p.Value

                        $paramsFileContent[$paramName] = $p.Value                    
                    }
                
                    @{parameterSets=@{Parameters=$paramsFileContent}} | 
                        ConvertTo-Json -Depth 10 |
                        Set-Content -Path $scadParamPath

                    '-p'
                    "$scadParamPath"
                    "-P"
                    'Parameters'
                }
            )

            Write-Verbose "$OpenScadCmd $($openScadArgs -join ' ')"

            & $OpenScadCmd @OpenScadArgs @OpenScadArgument *>&1 | =>[OpenScad.Output] -OpenSCADArguments $openScadArgs -OutputPath "$scadOutPath"

            if (Test-Path $scadParamPath) {
                Remove-Item $scadParamPath
            }
        } 
    }
}