Compare commits

..

14 Commits

Author SHA1 Message Date
Nathan
6f46b3fce3 make paths relative 2025-12-19 17:24:53 -07:00
Nathan
4c8d6b20b3 step 3 actually working, can't believe it first try 2025-12-19 17:03:08 -07:00
Nathan
1bafe5fe85 remove debug 2025-12-19 15:40:25 -07:00
Nathan
7896059a86 reorganize FlatColors and remove empty folders 2025-12-19 15:34:07 -07:00
Nathan
5a7d1f66b9 remove duplicate files 2025-12-19 13:20:58 -07:00
Nathan
5232e7c2cc strip blendfile prefixes from common imgs 2025-12-19 13:17:03 -07:00
Nathan
c0d625da13 separate behavior for inter- and intra-blendfile commonalities 2025-12-19 11:57:12 -07:00
Nathan
92e5e57f19 fix lint 2025-12-19 11:50:02 -07:00
Nathan
4d1b554f8f fix executionpolicy with pwsh 2025-12-19 11:33:04 -07:00
Nathan
658052cae4 remove example path 2025-12-19 11:31:47 -07:00
Nathan
ee317463f1 pause 2025-12-19 11:30:55 -07:00
Nathan
adafff58f0 begin reconfig orgtxt 2025-12-19 11:26:50 -07:00
Nathan
456d3b6411 cfgloader batch 2025-12-19 11:26:29 -07:00
Nathan
706c710dd2 append old organize_textures 2025-12-19 10:59:09 -07:00
6 changed files with 21548 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

22
ConfigLoader.bat Normal file
View File

@@ -0,0 +1,22 @@
@echo off
setlocal EnableExtensions
set "script_dir=%~dp0"
set "ps1_path=%script_dir%ConfigLoader.ps1"
if not exist "%ps1_path%" (
echo [ERROR] ConfigLoader.ps1 not found at %ps1_path%
exit /b 1
)
echo Running ConfigLoader...
powershell -NoProfile -ExecutionPolicy Bypass -File "%ps1_path%"
set "rc=%errorlevel%"
if %rc% neq 0 (
echo.
echo Script exited with error code %rc%
pause
)
exit /b %rc%

23
organize_textures.bat Normal file
View File

@@ -0,0 +1,23 @@
@echo off
setlocal EnableExtensions
set "script_dir=%~dp0"
set "ps1_path=%script_dir%organize_textures.ps1"
if not exist "%ps1_path%" (
echo [ERROR] organize_textures.ps1 not found at %ps1_path%
exit /b 1
)
echo Running texture organizer...
pwsh -NoProfile -ExecutionPolicy Bypass -File "%ps1_path%"
set "rc=%errorlevel%"
if %rc% neq 0 (
echo.
echo Script exited with error code %rc%
)
echo.
pause
exit /b %rc%

734
organize_textures.ps1 Normal file
View File

@@ -0,0 +1,734 @@
# 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
# 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
}
}
# ============================================================================
# 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: merge duplicates and move to \common\FlatColors
Write-Host "Processing FlatColors files (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
# Calculate checksums for FlatColors files
Write-Host "Calculating checksums for FlatColors files..." -ForegroundColor Yellow
$flatColorsWithChecksums = Get-FilesWithChecksums -Files $flatColorsFiles
# Group by checksum to find duplicates
$flatColorsGroupedByChecksum = $flatColorsWithChecksums | Group-Object -Property Hash
Write-Host "Found $($flatColorsGroupedByChecksum.Count) unique FlatColors file(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
$flatColorsMoveLog = [System.Collections.ArrayList]::new()
foreach ($group in $flatColorsGroupedByChecksum) {
$files = $group.Group
if ($files.Count -eq 1) {
# Single file - move to FlatColors folder
$fileObj = $files[0].File
$fileName = $fileObj.Name
# Strip blendfile prefix if present
$strippedName = Get-FileNameWithoutPrefix -FileName $fileName -ValidPrefixes $validBlendfilePrefixes
if ($strippedName -ne $fileName) {
$fileName = $strippedName
}
$destinationPath = Join-Path -Path $flatColorsPath -ChildPath $fileName
# Handle name conflicts (shouldn't happen for unique files, but just in case)
if ($filesInFlatColors.ContainsKey($fileName) -or (Test-Path -Path $destinationPath)) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$extension = [System.IO.Path]::GetExtension($fileName)
$counter = 1
do {
$newFileName = "${baseName}_${counter}${extension}"
$destinationPath = Join-Path -Path $flatColorsPath -ChildPath $newFileName
$counter++
} while ($filesInFlatColors.ContainsKey($newFileName) -or (Test-Path -Path $destinationPath))
$fileName = $newFileName
}
try {
$originalPath = $fileObj.FullName
Move-Item -Path $originalPath -Destination $destinationPath -Force
$filesInFlatColors[$fileName] = $true
$flatColorsMoved++
# Log the move
$normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath)
$normalizedNew = [System.IO.Path]::GetFullPath($destinationPath)
$null = $flatColorsMoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
NewPath = $normalizedNew
Type = "moved"
})
} catch {
Write-Warning "Failed to move FlatColors file: $($fileObj.FullName) - $($_.Exception.Message)"
}
} else {
# Multiple files with same checksum (duplicates) - merge them
# Use Process-DuplicateGroup to handle merging
$result = Process-DuplicateGroup -Files $files -CommonPath $flatColorsPath -FilesInCommon $filesInFlatColors -StripPrefix -ValidPrefixes $validBlendfilePrefixes -MoveLog $flatColorsMoveLog
$flatColorsMoved += $result.MovedCount
$flatColorsDuplicates += $result.DuplicateCount
}
}
Write-Host "Moved $flatColorsMoved unique FlatColors file(s) to \common\FlatColors, deleted $flatColorsDuplicates duplicate(s)" -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
$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) {
# Extract prefix from blend file name (e.g., "Chan_v4.3.blend" -> "Chan")
$blendFileName = $blendFile.BaseName
$parts = $blendFileName.Split('_', 2)
if ($parts.Length -ge 1) {
$prefix = $parts[0]
# Check if prefix matches any blendfile folder name
if ($blendfileFolderNames -contains $prefix) {
$blendFileMappings += [PSCustomObject]@{
BlendFile = [System.IO.Path]::GetFullPath($blendFile.FullName)
BlendfileFolder = $prefix
}
Write-Host "Found matching blend file: $($blendFile.Name) -> $prefix" -ForegroundColor Gray
}
}
}
}
}
# 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

275
remap_texture_paths.py Normal file
View File

@@ -0,0 +1,275 @@
"""
Blender script to remap texture paths after texture organization.
Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path]
"""
import bpy
import json
import os
import sys
def normalize_path(path):
"""Normalize a path for comparison (handle case, separators, etc.)"""
if not path:
return ""
# Convert to absolute path and normalize
abs_path = os.path.abspath(path)
# Normalize separators (Windows uses backslash, but we'll compare case-insensitively)
normalized = os.path.normpath(abs_path).replace('\\', '/')
return normalized.lower()
def load_move_log(json_path):
"""Load the move log JSON file and build lookup dictionaries"""
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Debug: Check what keys are in the data
print(f" JSON keys: {list(data.keys())}")
# PowerShell ConvertTo-Json uses PascalCase by default, so check both cases
moves = data.get('moves', []) or data.get('Moves', [])
print(f" Found {len(moves)} move entries in JSON")
if len(moves) > 0:
# Debug: Show first move entry structure
print(f" First move entry keys: {list(moves[0].keys())}")
print(f" First move entry: {moves[0]}")
# Build lookup dictionaries: normalized original path -> actual new path
original_to_new = {}
original_to_replacement = {}
for move in moves:
# Try both camelCase and PascalCase property names
move_type = move.get('type', '') or move.get('Type', '')
original = move.get('originalPath', '') or move.get('OriginalPath', '')
if not original:
continue
orig_norm = normalize_path(original)
if move_type == 'moved' or move_type == 'Moved':
new_path = move.get('newPath', '') or move.get('NewPath', '')
if new_path:
original_to_new[orig_norm] = new_path
elif move_type == 'deleted' or move_type == 'Deleted':
replacement = move.get('replacedBy', '') or move.get('ReplacedBy', '')
if replacement:
original_to_replacement[orig_norm] = replacement
print(f" Built {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings")
return original_to_new, original_to_replacement, data.get('textureFolderPath', '') or data.get('TextureFolderPath', '')
except Exception as e:
print(f"ERROR: Failed to load move log: {e}")
import traceback
traceback.print_exc()
return {}, {}, ""
def remap_texture_paths(blend_file_path, move_log_path):
"""Remap all texture paths in the blend file"""
print(f"\n=== REMAPPING TEXTURE PATHS ===")
print(f"Blend file: {blend_file_path}")
print(f"Move log: {move_log_path}")
# Load move log
original_to_new, original_to_replacement, texture_folder_path = load_move_log(move_log_path)
if not original_to_new and not original_to_replacement:
print("WARNING: No moves found in move log. Nothing to remap.")
return 0
print(f"Loaded {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings")
# Get blend file directory for relative path conversion
blend_file_dir = os.path.dirname(os.path.abspath(blend_file_path))
remapped_count = 0
not_found_count = 0
# Remap paths in image datablocks
print("\nRemapping image datablock paths...")
for image in bpy.data.images:
if not image.filepath:
continue
# Convert to absolute path for comparison
abs_path = bpy.path.abspath(image.filepath)
if not abs_path:
continue
abs_norm = normalize_path(abs_path)
was_relative = not os.path.isabs(image.filepath)
# Check if this path needs remapping
new_path = None
# First check moved files
if abs_norm in original_to_new:
new_path = original_to_new[abs_norm]
# Then check deleted files (replaced by)
elif abs_norm in original_to_replacement:
new_path = original_to_replacement[abs_norm]
if new_path:
# Convert to relative path if original was relative
if was_relative:
try:
rel_path = bpy.path.relpath(new_path, blend_file_dir)
image.filepath = rel_path
except:
# Fallback to absolute if relative conversion fails
image.filepath = new_path
else:
image.filepath = new_path
print(f" Remapped: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}")
remapped_count += 1
else:
# Check if file exists - if not, it might be a broken reference
if not os.path.exists(abs_path):
not_found_count += 1
# Also check material node trees for image texture nodes
# Note: Most image texture nodes reference the image datablock (which we already remapped above),
# but we check node.filepath for completeness in case any nodes have direct file paths
print("\nRemapping material node texture paths...")
node_remapped_count = 0
for material in bpy.data.materials:
if not material.use_nodes or not material.node_tree:
continue
for node in material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and hasattr(node, 'filepath') and node.filepath:
abs_path = bpy.path.abspath(node.filepath)
if abs_path:
abs_norm = normalize_path(abs_path)
new_path = None
if abs_norm in original_to_new:
new_path = original_to_new[abs_norm]
elif abs_norm in original_to_replacement:
new_path = original_to_replacement[abs_norm]
if new_path:
node.filepath = new_path
print(f" Remapped node texture: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}")
node_remapped_count += 1
if node_remapped_count > 0:
remapped_count += node_remapped_count
# Convert any remaining absolute paths in image datablocks to relative
# (ignoring images from linked files)
print("\nConverting absolute image paths to relative...")
abs_to_rel_count = 0
for image in bpy.data.images:
# Skip images from linked files (library datablocks)
if image.library:
continue
if not image.filepath:
continue
# Check if path is absolute
if os.path.isabs(image.filepath):
try:
# Convert to relative path
rel_path = bpy.path.relpath(image.filepath, blend_file_dir)
if rel_path != image.filepath:
image.filepath = rel_path
print(f" Converted to relative: {os.path.basename(image.filepath)}")
abs_to_rel_count += 1
except Exception as e:
# If conversion fails, skip it (might be a network path or other issue)
pass
if abs_to_rel_count > 0:
remapped_count += abs_to_rel_count
print(f"\n=== REMAPPING SUMMARY ===")
print(f"Paths remapped: {remapped_count}")
print(f"Broken references (not found): {not_found_count}")
# Make all paths relative after remapping (this handles other datablock types)
try:
print(f"\nMaking paths relative...")
bpy.ops.file.make_paths_relative()
print(f" Converted absolute paths to relative paths")
except Exception as e:
print(f" Warning: Failed to make paths relative: {e}")
# Save the blend file
try:
print(f"\nSaving blend file...")
# Ensure we use an absolute path
abs_blend_path = os.path.abspath(blend_file_path)
print(f" Saving to: {abs_blend_path}")
bpy.ops.wm.save_mainfile(filepath=abs_blend_path)
# Verify the file was actually saved
if os.path.exists(abs_blend_path):
file_time = os.path.getmtime(abs_blend_path)
print(f"Successfully saved: {abs_blend_path}")
print(f" File modified time: {file_time}")
return remapped_count
else:
print(f"ERROR: File was not created at {abs_blend_path}")
return -1
except Exception as e:
print(f"ERROR: Failed to save blend file: {e}")
import traceback
traceback.print_exc()
return -1
def main():
# Get command line arguments (after --)
if '--' in sys.argv:
args = sys.argv[sys.argv.index('--') + 1:]
else:
args = sys.argv[1:]
if len(args) < 2:
print("ERROR: Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path]")
return 1
blend_file_path = args[0]
move_log_path = args[1]
# Validate paths
if not os.path.exists(blend_file_path):
print(f"ERROR: Blend file not found: {blend_file_path}")
return 1
if not os.path.exists(move_log_path):
print(f"ERROR: Move log file not found: {move_log_path}")
return 1
# Load the blend file
try:
print(f"Loading blend file: {blend_file_path}")
bpy.ops.wm.open_mainfile(filepath=blend_file_path)
except Exception as e:
print(f"ERROR: Failed to load blend file: {e}")
return 1
# Remap texture paths
result = remap_texture_paths(blend_file_path, move_log_path)
if result >= 0:
print("\nTexture path remapping completed successfully!")
return 0
else:
print("\nTexture path remapping failed!")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)