# Script to organize texture files by checksum with two-level duplicate detection # Pass 1: Intra-blendfile duplicates → [blendfile]\common # Pass 2: Inter-blendfile duplicates → \textures\common # Usage: .\organize_textures.ps1 # Prompt user for texture folder path $textureFolderPath = Read-Host "Enter texture folder path" # Validate the input path if ([string]::IsNullOrWhiteSpace($textureFolderPath)) { Write-Host "Error: No path provided." -ForegroundColor Red exit } if (-not (Test-Path -Path $textureFolderPath -PathType Container)) { Write-Host "Error: Path does not exist or is not a directory: $textureFolderPath" -ForegroundColor Red exit } # Resolve the full path $textureFolderPath = (Resolve-Path $textureFolderPath).ProviderPath Write-Host "Processing texture folder: $textureFolderPath" -ForegroundColor Cyan # Add required .NET assemblies for image processing (for FlatColors standardization) Add-Type -AssemblyName System.Drawing # Function to calculate checksums for files function Get-FilesWithChecksums { param( [array]$Files ) $throttleLimit = [Math]::Max(1, [Environment]::ProcessorCount) $parallelScriptBlock = { try { $hash = Get-FileHash -Path $_.FullName -Algorithm SHA256 [PSCustomObject]@{ File = $_ Hash = $hash.Hash } } catch { Write-Warning "Failed to calculate checksum for: $($_.FullName) - $($_.Exception.Message)" $null } } return $Files | ForEach-Object -Parallel $parallelScriptBlock -ThrottleLimit $throttleLimit | Where-Object { $null -ne $_ } } # Function to move files to common folder function Move-FilesToCommon { param( [array]$Files, [string]$CommonPath, [string]$DuplicatesPath, [hashtable]$FilesInCommon ) $movedCount = 0 $duplicateCount = 0 foreach ($fileObj in $Files) { $fileName = $fileObj.Name $destinationPath = Join-Path -Path $CommonPath -ChildPath $fileName # Handle name conflicts if ($FilesInCommon.ContainsKey($fileName)) { $baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) $extension = [System.IO.Path]::GetExtension($fileName) $counter = 1 do { $newFileName = "${baseName}_${counter}${extension}" $destinationPath = Join-Path -Path $CommonPath -ChildPath $newFileName $counter++ } while ($FilesInCommon.ContainsKey($newFileName) -or (Test-Path -Path $destinationPath)) $fileName = $newFileName } try { Move-Item -Path $fileObj.FullName -Destination $destinationPath -Force $FilesInCommon[$fileName] = $true $movedCount++ } catch { Write-Warning "Failed to move file: $($fileObj.FullName) - $($_.Exception.Message)" } } return @{ MovedCount = $movedCount DuplicateCount = $duplicateCount } } # Function to extract suffix after blendfile prefix (e.g., "Demarco_Std_Teeth_ao.jpg" -> "Std_Teeth_ao.jpg") # Only strips prefixes that are in the $ValidPrefixes list function Get-FileNameWithoutPrefix { param( [string]$FileName, [string[]]$ValidPrefixes ) # Validate input if ([string]::IsNullOrWhiteSpace($FileName)) { return $FileName } # If no valid prefixes provided, return original filename if ($null -eq $ValidPrefixes -or $ValidPrefixes.Count -eq 0) { return $FileName } # Use Split instead of regex for robustness (avoids $matches variable issues in loops) # Split by the first underscore only (limit to 2 parts) $parts = $FileName.Split('_', 2) if ($parts.Length -eq 2) { $prefix = $parts[0] $suffix = $parts[1] # Only strip if the prefix is in the valid prefixes list if ($ValidPrefixes -contains $prefix -and -not [string]::IsNullOrWhiteSpace($suffix)) { return $suffix } } # No valid prefix found or suffix is empty, return original filename return $FileName } # Function to process duplicate group function Process-DuplicateGroup { param( [array]$Files, [string]$CommonPath, [hashtable]$FilesInCommon, [switch]$StripPrefix, [string[]]$ValidPrefixes = @(), [System.Collections.ArrayList]$MoveLog = $null ) $movedCount = 0 $duplicateCount = 0 if ($Files.Count -eq 1) { # Single file - leave in place (will be processed in Pass 2) return @{ MovedCount = 0 DuplicateCount = 0 } } # Validate files array if ($null -eq $Files -or $Files.Count -eq 0) { return @{ MovedCount = 0 DuplicateCount = 0 } } # Multiple files with same checksum (duplicates) # Determine the filename to use $firstFile = $Files[0].File if ($null -eq $firstFile -or [string]::IsNullOrWhiteSpace($firstFile.Name)) { Write-Warning "Invalid file object in duplicate group" return @{ MovedCount = 0 DuplicateCount = 0 } } $fileName = $firstFile.Name # If StripPrefix is enabled, always strip prefix from the filename if ($StripPrefix) { # Always try to strip prefix - if file has a prefix, it will be removed $strippedName = Get-FileNameWithoutPrefix -FileName $firstFile.Name -ValidPrefixes $ValidPrefixes # Use stripped name if it's different from original (prefix was removed) # Otherwise keep original (file had no prefix to strip) if ($strippedName -ne $firstFile.Name) { $fileName = $strippedName } # If we have multiple files with same checksum, try to find common suffix # This handles cases where files from different blendfiles share the same texture if ($Files.Count -gt 1) { $allSuffixes = @() foreach ($fileObj in $Files) { # $fileObj is a PSCustomObject with .File property, so access .File.Name $suffix = Get-FileNameWithoutPrefix -FileName $fileObj.File.Name -ValidPrefixes $ValidPrefixes $allSuffixes += $suffix } # If all files strip to the same suffix, use that $uniqueSuffixes = @($allSuffixes | Select-Object -Unique) if ($uniqueSuffixes.Count -eq 1 -and $uniqueSuffixes[0] -ne $firstFile.Name) { $fileName = [string]$uniqueSuffixes[0] } } } # Ensure fileName is never null or empty if ([string]::IsNullOrWhiteSpace($fileName)) { $fileName = $firstFile.Name } $destinationPath = Join-Path -Path $CommonPath -ChildPath $fileName # Handle name conflicts for the first file if ($FilesInCommon.ContainsKey($fileName)) { $baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) $extension = [System.IO.Path]::GetExtension($fileName) $counter = 1 do { $newFileName = "${baseName}_${counter}${extension}" $destinationPath = Join-Path -Path $CommonPath -ChildPath $newFileName $counter++ } while ($FilesInCommon.ContainsKey($newFileName) -or (Test-Path -Path $destinationPath)) $fileName = $newFileName } try { Move-Item -Path $firstFile.FullName -Destination $destinationPath -Force $FilesInCommon[$fileName] = $true $movedCount++ # Log the move if ($null -ne $MoveLog) { $normalizedOriginal = [System.IO.Path]::GetFullPath($firstFile.FullName) $normalizedNew = [System.IO.Path]::GetFullPath($destinationPath) $null = $MoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal NewPath = $normalizedNew Type = "moved" }) } } catch { Write-Warning "Failed to move first duplicate file: $($firstFile.FullName) - $($_.Exception.Message)" } # Delete remaining duplicate files (they're replaced by the common file) for ($i = 1; $i -lt $Files.Count; $i++) { $fileObj = $Files[$i].File try { $originalPath = $fileObj.FullName Remove-Item -Path $originalPath -Force $duplicateCount++ # Log the deletion (replaced by the moved file) if ($null -ne $MoveLog) { $normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath) $normalizedReplacement = [System.IO.Path]::GetFullPath($destinationPath) $null = $MoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal ReplacedBy = $normalizedReplacement Type = "deleted" }) } } catch { Write-Warning "Failed to delete duplicate file: $($fileObj.FullName) - $($_.Exception.Message)" } } return @{ MovedCount = $movedCount DuplicateCount = $duplicateCount } } # Function to extract and normalize hex color code from filename (for FlatColors) # For TGA files, preserves 8-digit codes (RGBA). For others, uses 6-digit codes (RGB). function Get-NormalizedColorCode { param( [string]$FileName, [string]$Extension = "" ) # Remove extension if not provided $baseName = [System.IO.Path]::GetFileNameWithoutExtension($FileName) if ([string]::IsNullOrWhiteSpace($Extension)) { $Extension = [System.IO.Path]::GetExtension($FileName) } # For TGA files, preserve 8-digit codes (RGBA with alpha channel) if ($Extension -eq ".tga") { # Match 8-digit hex: #RRGGBBAA if ($baseName -match '^(#?)([0-9A-Fa-f]{8})') { $hexCode = $Matches[2].ToUpper() return "#$hexCode" } # Fallback to 6-digit if 8-digit not found if ($baseName -match '^(#?)([0-9A-Fa-f]{6})') { $hexCode = $Matches[2].ToUpper() return "#$hexCode" } } else { # For non-TGA files, use 6-digit codes (RGB) if ($baseName -match '^(#?)([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?') { $hexCode = $Matches[2].ToUpper() # Return normalized format: #RRGGBB (always with #, always uppercase, no suffix) return "#$hexCode" } } return $null } # Function to convert and resize image to 16x16 JPG (for FlatColors) function Convert-To16x16Jpg { param( [string]$SourcePath, [string]$DestinationPath ) try { $image = [System.Drawing.Image]::FromFile($SourcePath) # Create 16x16 bitmap $bitmap = New-Object System.Drawing.Bitmap(16, 16) $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality # Draw resized image $graphics.DrawImage($image, 0, 0, 16, 16) # Get JPEG codec $jpegCodec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq "image/jpeg" } $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1) $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 95) # Save as JPG $bitmap.Save($DestinationPath, $jpegCodec, $encoderParams) # Cleanup $graphics.Dispose() $bitmap.Dispose() $image.Dispose() $encoderParams.Dispose() return $true } catch { Write-Warning "Failed to convert image: $SourcePath - $($_.Exception.Message)" return $false } } # Function to check if image is 16x16 (for FlatColors) function Test-ImageSize { param([string]$ImagePath) try { $image = [System.Drawing.Image]::FromFile($ImagePath) $is16x16 = ($image.Width -eq 16 -and $image.Height -eq 16) $image.Dispose() return $is16x16 } catch { return $false } } # ============================================================================ # PASS 1: Intra-Blendfile Processing # ============================================================================ Write-Host "" Write-Host "=== PASS 1: Intra-Blendfile Processing ===" -ForegroundColor Cyan # Get all direct subdirectories of texture folder (blendfile folders) $blendfileFolders = Get-ChildItem -Path $textureFolderPath -Directory | Where-Object { $_.Name -ne "common" } if ($null -eq $blendfileFolders -or $blendfileFolders.Count -eq 0) { Write-Host "No blendfile folders found. Skipping Pass 1." -ForegroundColor Yellow } else { Write-Host "Found $($blendfileFolders.Count) blendfile folder(s) to process." -ForegroundColor Green $totalPass1Moved = 0 $totalPass1Duplicates = 0 $pass1MoveLog = [System.Collections.ArrayList]::new() foreach ($blendfileFolder in $blendfileFolders) { Write-Host "" Write-Host "Processing blendfile: $($blendfileFolder.Name)" -ForegroundColor Yellow # Get all files in this blendfile folder, excluding \common folders $blendfileFiles = Get-ChildItem -Path $blendfileFolder.FullName -Recurse -File | Where-Object { $_.FullName -notlike "*\common\*" } if ($null -eq $blendfileFiles -or $blendfileFiles.Count -eq 0) { Write-Host " No files found in this blendfile folder." -ForegroundColor Gray continue } Write-Host " Found $($blendfileFiles.Count) files." -ForegroundColor Gray # Calculate checksums Write-Host " Calculating checksums..." -ForegroundColor Gray $filesWithChecksums = Get-FilesWithChecksums -Files $blendfileFiles # Group by checksum $groupedByChecksum = $filesWithChecksums | Group-Object -Property Hash Write-Host " Found $($groupedByChecksum.Count) unique checksums." -ForegroundColor Gray # Create [blendfile]\common directory $blendfileCommonPath = Join-Path -Path $blendfileFolder.FullName -ChildPath "common" if (-not (Test-Path -Path $blendfileCommonPath -PathType Container)) { New-Item -ItemType Directory -Path $blendfileCommonPath | Out-Null } # Track filenames already in [blendfile]\common $filesInBlendfileCommon = @{} # Process each checksum group $blendfileMoved = 0 $blendfileDuplicates = 0 foreach ($group in $groupedByChecksum) { $result = Process-DuplicateGroup -Files $group.Group -CommonPath $blendfileCommonPath -FilesInCommon $filesInBlendfileCommon -MoveLog $pass1MoveLog $blendfileMoved += $result.MovedCount $blendfileDuplicates += $result.DuplicateCount } Write-Host " Moved $blendfileMoved file(s) to \common, deleted $blendfileDuplicates duplicate(s)" -ForegroundColor Green $totalPass1Moved += $blendfileMoved $totalPass1Duplicates += $blendfileDuplicates } Write-Host "" Write-Host "Pass 1 complete: $totalPass1Moved file(s) moved, $totalPass1Duplicates duplicate(s) deleted" -ForegroundColor Green } # ============================================================================ # PASS 2: Inter-Blendfile Processing # ============================================================================ Write-Host "" Write-Host "=== PASS 2: Inter-Blendfile Processing ===" -ForegroundColor Cyan # Build list of valid blendfile prefixes from folder names $validBlendfilePrefixes = @() $blendfileFoldersForPrefixes = Get-ChildItem -Path $textureFolderPath -Directory | Where-Object { $_.Name -ne "common" } if ($null -ne $blendfileFoldersForPrefixes) { $validBlendfilePrefixes = $blendfileFoldersForPrefixes | ForEach-Object { $_.Name } Write-Host "Valid blendfile prefixes: $($validBlendfilePrefixes -join ', ')" -ForegroundColor Gray } # Collect FlatColors files separately (including those in \common\FlatColors folders, but not from root \common\FlatColors) Write-Host "Collecting FlatColors files..." -ForegroundColor Yellow $rootCommonFlatColorsPath = Join-Path -Path $textureFolderPath -ChildPath "common\FlatColors" $allFlatColorsFiles = Get-ChildItem -Path $textureFolderPath -Recurse -File | Where-Object { ($_.FullName -like "*\FlatColors\*" -or $_.Name -like "*FlatColors*") -and $_.FullName -notlike "$([regex]::Escape($rootCommonFlatColorsPath))\*" } # Get all remaining files (excluding all \common folders, but we'll handle FlatColors separately) Write-Host "Collecting remaining files..." -ForegroundColor Yellow $remainingFiles = Get-ChildItem -Path $textureFolderPath -Recurse -File | Where-Object { $_.FullName -notlike "*\common\*" } if ($null -eq $remainingFiles -or $remainingFiles.Count -eq 0) { Write-Host "No remaining files found to process." -ForegroundColor Yellow } else { Write-Host "Found $($remainingFiles.Count) remaining files to process." -ForegroundColor Green # Calculate checksums Write-Host "Calculating checksums using parallel processing (this may take a while)..." -ForegroundColor Yellow $filesWithChecksums = Get-FilesWithChecksums -Files $remainingFiles Write-Host "Checksum calculation complete." -ForegroundColor Green # Separate FlatColors files from other files before processing Write-Host "Identifying FlatColors files..." -ForegroundColor Yellow $flatColorsFiles = $allFlatColorsFiles | Where-Object { (Test-Path -Path $_.FullName) } $nonFlatColorsFiles = $remainingFiles | Where-Object { $_.Name -notlike "*FlatColors*" } Write-Host "Found $($flatColorsFiles.Count) FlatColors file(s) (including from \common\FlatColors folders), $($nonFlatColorsFiles.Count) other file(s)" -ForegroundColor Gray # Group non-FlatColors files by checksum Write-Host "Grouping files by checksum..." -ForegroundColor Yellow $nonFlatColorsWithChecksums = $filesWithChecksums | Where-Object { $_.File.Name -notlike "*FlatColors*" } $groupedByChecksum = $nonFlatColorsWithChecksums | Group-Object -Property Hash Write-Host "Found $($groupedByChecksum.Count) unique checksums." -ForegroundColor Green # Create \textures\common directory $rootCommonPath = Join-Path -Path $textureFolderPath -ChildPath "common" if (-not (Test-Path -Path $rootCommonPath -PathType Container)) { New-Item -ItemType Directory -Path $rootCommonPath | Out-Null Write-Host "Created directory: $rootCommonPath" -ForegroundColor Green } # Track filenames already in \textures\common $filesInRootCommon = @{} # Process each checksum group (excluding FlatColors) Write-Host "Moving files to \common and deleting duplicates..." -ForegroundColor Yellow $pass2Moved = 0 $pass2Duplicates = 0 $pass2MoveLog = [System.Collections.ArrayList]::new() foreach ($group in $groupedByChecksum) { $files = $group.Group if ($files.Count -eq 1) { # Single file - leave in place (unique file) continue } # Multiple files with same checksum (duplicates across blendfiles) # Use StripPrefix to remove blendfile prefixes (e.g., "Demarco_", "Chan_") when all files share the same suffix $result = Process-DuplicateGroup -Files $files -CommonPath $rootCommonPath -FilesInCommon $filesInRootCommon -StripPrefix -ValidPrefixes $validBlendfilePrefixes -MoveLog $pass2MoveLog $pass2Moved += $result.MovedCount $pass2Duplicates += $result.DuplicateCount } # Process FlatColors files: standardize to 16x16 JPG (except TGA), merge duplicates by color code Write-Host "Processing FlatColors files (standardizing and merging duplicates)..." -ForegroundColor Yellow if ($null -ne $flatColorsFiles -and $flatColorsFiles.Count -gt 0) { Write-Host "Found $($flatColorsFiles.Count) FlatColors file(s) to process" -ForegroundColor Gray # Group files by normalized color code (instead of checksum) Write-Host "Grouping FlatColors files by color code..." -ForegroundColor Yellow $filesByColor = @{} foreach ($file in $flatColorsFiles) { if (-not (Test-Path -Path $file.FullName)) { continue } # Use composite key: color code + extension to separate TGA (8-digit) from JPG (6-digit) $colorCode = Get-NormalizedColorCode -FileName $file.Name -Extension $file.Extension if ($null -eq $colorCode) { Write-Warning "Could not extract color code from: $($file.Name)" continue } # Create composite key: colorCode_extension to keep TGA and JPG separate $groupKey = "$colorCode$($file.Extension)" if (-not $filesByColor.ContainsKey($groupKey)) { $filesByColor[$groupKey] = @() } $filesByColor[$groupKey] += $file } Write-Host "Found $($filesByColor.Count) unique color code(s)" -ForegroundColor Gray # Create \textures\common\FlatColors directory $flatColorsPath = Join-Path -Path $rootCommonPath -ChildPath "FlatColors" if (-not (Test-Path -Path $flatColorsPath -PathType Container)) { New-Item -ItemType Directory -Path $flatColorsPath | Out-Null Write-Host "Created directory: $flatColorsPath" -ForegroundColor Green } # Track filenames already in \common\FlatColors $filesInFlatColors = @{} $flatColorsMoved = 0 $flatColorsDuplicates = 0 $flatColorsConverted = 0 $flatColorsResized = 0 $flatColorsMoveLog = [System.Collections.ArrayList]::new() foreach ($groupKey in $filesByColor.Keys) { $files = $filesByColor[$groupKey] # Extract color code and extension from group key # Group key format: #RRGGBB.ext or #RRGGBBAA.ext if ($groupKey -match '^(#[0-9A-F]{6,8})(\.[^.]+)$') { $colorCode = $Matches[1] $targetExtension = $Matches[2] } else { Write-Warning "Invalid group key format: $groupKey" continue } $targetFileName = "$colorCode$targetExtension" # Find the best file to keep $fileToKeep = $null # First, check if there's already a correct file in target location $existingTarget = Join-Path -Path $flatColorsPath -ChildPath $targetFileName if (Test-Path -Path $existingTarget) { $existingFile = Get-Item -Path $existingTarget # Check if it's already correct if ($existingFile.Extension -eq ".tga") { $fileToKeep = $existingFile } elseif ($existingFile.Extension -eq ".jpg" -and (Test-ImageSize -ImagePath $existingFile.FullName)) { $fileToKeep = $existingFile } } # If no existing correct file, find best source file if ($null -eq $fileToKeep) { # Prefer TGA file first (preserves transparency) $tgaFile = $files | Where-Object { $_.Extension -eq ".tga" -and (Test-Path -Path $_.FullName) } | Select-Object -First 1 if ($tgaFile) { $fileToKeep = $tgaFile } else { # If no TGA, prefer existing JPG file $jpgFile = $files | Where-Object { ($_.Extension -eq ".jpg" -or $_.Extension -eq ".jpeg") -and (Test-Path -Path $_.FullName) } | Select-Object -First 1 if ($jpgFile) { $fileToKeep = $jpgFile } else { # Otherwise, use the first available file $fileToKeep = $files | Where-Object { Test-Path -Path $_.FullName } | Select-Object -First 1 } } } if ($null -eq $fileToKeep) { Write-Warning " ${colorCode}: No valid file found to process" continue } $targetPath = Join-Path -Path $flatColorsPath -ChildPath $targetFileName # If target is TGA, preserve as-is (don't convert) if ($targetExtension -eq ".tga") { if ($fileToKeep.FullName -ne $targetPath) { # Move TGA to target location try { if (Test-Path -Path $targetPath) { Remove-Item -Path $targetPath -Force } $originalPath = $fileToKeep.FullName Move-Item -Path $originalPath -Destination $targetPath -Force $filesInFlatColors[$targetFileName] = $true $flatColorsMoved++ # Log the move $normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath) $normalizedNew = [System.IO.Path]::GetFullPath($targetPath) $null = $flatColorsMoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal NewPath = $normalizedNew Type = "moved" }) } catch { Write-Warning " ${colorCode}: Failed to move TGA: $($_.Exception.Message)" } } } else { # Target is JPG - convert/resize if needed $needsConversion = $false $needsResize = $false if ($fileToKeep.Extension -ne ".jpg" -and $fileToKeep.Extension -ne ".jpeg") { $needsConversion = $true } if (-not (Test-ImageSize -ImagePath $fileToKeep.FullName)) { $needsResize = $true } # Check if target already exists and is correct if ((Test-Path -Path $targetPath) -and (Get-Item -Path $targetPath).Extension -eq ".jpg" -and (Test-ImageSize -ImagePath $targetPath)) { # Target already exists and is correct, skip } elseif ($needsConversion -or $needsResize -or $fileToKeep.FullName -ne $targetPath) { # Convert/resize to target if (Convert-To16x16Jpg -SourcePath $fileToKeep.FullName -DestinationPath $targetPath) { if ($needsConversion -and $needsResize) { $flatColorsConverted++ $flatColorsResized++ } elseif ($needsConversion) { $flatColorsConverted++ } elseif ($needsResize) { $flatColorsResized++ } else { $flatColorsMoved++ } $filesInFlatColors[$targetFileName] = $true # Log the conversion/move $normalizedOriginal = [System.IO.Path]::GetFullPath($fileToKeep.FullName) $normalizedNew = [System.IO.Path]::GetFullPath($targetPath) $null = $flatColorsMoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal NewPath = $normalizedNew Type = "moved" }) } else { Write-Warning " ${colorCode}: Failed to convert/resize" continue } } else { # File is already correct format and size if ($fileToKeep.FullName -ne $targetPath) { try { if (Test-Path -Path $targetPath) { Remove-Item -Path $targetPath -Force } $originalPath = $fileToKeep.FullName Move-Item -Path $originalPath -Destination $targetPath -Force $filesInFlatColors[$targetFileName] = $true $flatColorsMoved++ # Log the move $normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath) $normalizedNew = [System.IO.Path]::GetFullPath($targetPath) $null = $flatColorsMoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal NewPath = $normalizedNew Type = "moved" }) } catch { Write-Warning " ${colorCode}: Failed to move: $($_.Exception.Message)" } } } } # Delete duplicate files (all files in the group except the target) foreach ($file in $files) { if ($file.FullName -ne $targetPath -and (Test-Path -Path $file.FullName)) { try { $originalPath = $file.FullName Remove-Item -Path $originalPath -Force $flatColorsDuplicates++ # Log the deletion $normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath) $normalizedReplacement = [System.IO.Path]::GetFullPath($targetPath) $null = $flatColorsMoveLog.Add([PSCustomObject]@{ OriginalPath = $normalizedOriginal ReplacedBy = $normalizedReplacement Type = "deleted" }) } catch { Write-Warning " Failed to delete duplicate: $($file.FullName) - $($_.Exception.Message)" } } } } Write-Host "Processed FlatColors: $flatColorsMoved moved, $flatColorsConverted converted, $flatColorsResized resized, $flatColorsDuplicates duplicate(s) deleted" -ForegroundColor Green } else { Write-Host "No FlatColors files found to process." -ForegroundColor Gray } Write-Host "" Write-Host "Pass 2 complete: $pass2Moved file(s) moved to \common, $pass2Duplicates duplicate(s) deleted" -ForegroundColor Green } # ============================================================================ # Save Move Log and Find Blend Files # ============================================================================ Write-Host "" Write-Host "Saving move log and finding blend files..." -ForegroundColor Yellow # Combine all move logs $allMoves = @() if ($null -ne $pass1MoveLog -and $pass1MoveLog.Count -gt 0) { $allMoves += $pass1MoveLog } if ($null -ne $pass2MoveLog -and $pass2MoveLog.Count -gt 0) { $allMoves += $pass2MoveLog } if ($null -ne $flatColorsMoveLog -and $flatColorsMoveLog.Count -gt 0) { $allMoves += $flatColorsMoveLog } # Get parent directory of texture folder $blendFileParentDir = Split-Path -Path $textureFolderPath -Parent # Find matching blend files # Match logic: blendfile folder name (e.g., "Beth") should appear in blend file name (e.g., "AM_Beth_v3.2.blend") $blendFileMappings = @() if ($null -ne $blendfileFolders -and $blendfileFolders.Count -gt 0) { # Get blendfile folder names $blendfileFolderNames = $blendfileFolders | ForEach-Object { $_.Name } # Find all .blend files in parent directory $blendFiles = Get-ChildItem -Path $blendFileParentDir -Filter "*.blend" -File -ErrorAction SilentlyContinue if ($null -ne $blendFiles -and $blendFiles.Count -gt 0) { foreach ($blendFile in $blendFiles) { $blendFileName = $blendFile.BaseName # Check if any blendfile folder name appears in the blend file name foreach ($folderName in $blendfileFolderNames) { # Match folder name when surrounded by underscores, dots, hyphens, or at start/end # This avoids partial matches (e.g., "Beth" in "Bethany") while allowing "Beth" in "AM_Beth_v3.2" $escapedFolderName = [regex]::Escape($folderName) if ($blendFileName -match "(^|[._-])$escapedFolderName([._-]|$)") { $blendFileMappings += [PSCustomObject]@{ BlendFile = [System.IO.Path]::GetFullPath($blendFile.FullName) BlendfileFolder = $folderName } Write-Host "Found matching blend file: $($blendFile.Name) -> $folderName" -ForegroundColor Gray break # Only match once per blend file } } } } } # Create move log object $moveLogData = [PSCustomObject]@{ TextureFolderPath = $textureFolderPath Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ") TotalMoves = ($allMoves | Where-Object { $_.Type -eq "moved" }).Count TotalDeletes = ($allMoves | Where-Object { $_.Type -eq "deleted" }).Count Moves = $allMoves BlendFileMappings = $blendFileMappings } # Save to JSON file $moveLogPath = Join-Path -Path $blendFileParentDir -ChildPath "texture_moves.json" try { $moveLogData | ConvertTo-Json -Depth 10 | Set-Content -Path $moveLogPath -Encoding UTF8 Write-Host "Move log saved to: $moveLogPath" -ForegroundColor Green Write-Host " Total moves: $($moveLogData.TotalMoves), Total deletes: $($moveLogData.TotalDeletes)" -ForegroundColor Gray Write-Host " Blend files found: $($blendFileMappings.Count)" -ForegroundColor Gray } catch { Write-Warning "Failed to save move log: $($_.Exception.Message)" } # ============================================================================ # Remap Texture Paths in Blend Files # ============================================================================ if ($blendFileMappings.Count -gt 0 -and (Test-Path -Path $moveLogPath)) { Write-Host "" Write-Host "Remapping texture paths in blend files..." -ForegroundColor Yellow # Find Blender executable from PATH $blenderExe = $null $blenderInPath = Get-Command blender -ErrorAction SilentlyContinue if ($null -ne $blenderInPath) { $blenderExe = $blenderInPath.Source } if ($null -eq $blenderExe) { Write-Warning "Blender executable not found. Skipping texture path remapping." Write-Host " Please install Blender or add it to your PATH." -ForegroundColor Yellow } else { Write-Host "Found Blender: $blenderExe" -ForegroundColor Gray # Get the remap script path (should be in the same directory as this script) $scriptDir = Split-Path -Path $MyInvocation.MyCommand.Path -Parent $remapScriptPath = Join-Path -Path $scriptDir -ChildPath "remap_texture_paths.py" if (-not (Test-Path -Path $remapScriptPath)) { Write-Warning "Remap script not found: $remapScriptPath" Write-Warning "Skipping texture path remapping." } else { $processedCount = 0 $failedCount = 0 Write-Host "Processing $($blendFileMappings.Count) blend file(s)..." -ForegroundColor Gray foreach ($mapping in $blendFileMappings) { $blendFilePath = $mapping.BlendFile $blendFileName = Split-Path -Path $blendFilePath -Leaf Write-Host " Processing: $blendFileName" -ForegroundColor Cyan # Check if blend file exists if (-not (Test-Path -Path $blendFilePath)) { Write-Warning " Blend file not found: $blendFilePath" $failedCount++ continue } # Run Blender with the remapping script (one file at a time) # Delete old output file Remove-Item -Path "blender_output.txt" -ErrorAction SilentlyContinue try { # Run Blender and capture output while displaying it (like compress_blend_files.bat) # Build command with proper argument array $blenderArgsArray = @( "--background", "--factory-startup", "--python", $remapScriptPath, "--", $blendFilePath, $moveLogPath ) # Execute and pipe output through Tee-Object to display and save & $blenderExe $blenderArgsArray 2>&1 | Tee-Object -FilePath "blender_output.txt" | ForEach-Object { Write-Host " $_" } # Check exit code $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { Write-Host " Successfully remapped texture paths" -ForegroundColor Green $processedCount++ } else { Write-Warning " Failed to remap texture paths (exit code: $exitCode)" $failedCount++ } } catch { Write-Warning " Exception while processing blend file: $($_.Exception.Message)" $failedCount++ } } # Clean up temporary output files Remove-Item -Path "blender_output.txt" -ErrorAction SilentlyContinue Remove-Item -Path "blender_error.txt" -ErrorAction SilentlyContinue Write-Host "" if ($processedCount -gt 0 -or $failedCount -gt 0) { Write-Host "Blend file remapping complete: $processedCount succeeded, $failedCount failed" -ForegroundColor $(if ($failedCount -eq 0) { "Green" } else { "Yellow" }) } } } } else { if ($blendFileMappings.Count -eq 0) { Write-Host "" Write-Host "No matching blend files found. Skipping texture path remapping." -ForegroundColor Gray } } # Function to remove empty folders recursively function Remove-EmptyFolders { param( [string]$Path, [string]$RootPath ) $removedCount = 0 # Get all subdirectories $subdirs = Get-ChildItem -Path $Path -Directory -ErrorAction SilentlyContinue if ($null -ne $subdirs) { foreach ($subdir in $subdirs) { # Recursively process subdirectories first $removedCount += Remove-EmptyFolders -Path $subdir.FullName -RootPath $RootPath # Check if this directory is now empty (after processing subdirectories) $items = Get-ChildItem -Path $subdir.FullName -ErrorAction SilentlyContinue if ($null -eq $items -or $items.Count -eq 0) { # Don't remove the root path if ($subdir.FullName -ne $RootPath) { try { Remove-Item -Path $subdir.FullName -Force -ErrorAction Stop $removedCount++ } catch { Write-Warning "Failed to remove empty folder: $($subdir.FullName) - $($_.Exception.Message)" } } } } } return $removedCount } # Remove empty folders Write-Host "" Write-Host "Removing empty folders..." -ForegroundColor Yellow $emptyFoldersRemoved = Remove-EmptyFolders -Path $textureFolderPath -RootPath $textureFolderPath if ($emptyFoldersRemoved -gt 0) { Write-Host "Removed $emptyFoldersRemoved empty folder(s)" -ForegroundColor Green } else { Write-Host "No empty folders found to remove." -ForegroundColor Gray } Write-Host "" Write-Host "File organization complete!" -ForegroundColor Green