1026 lines
38 KiB
C#
1026 lines
38 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using Newtonsoft.Json.Linq;
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEngine;
|
||
|
|
using MCPForUnity.Editor.Helpers;
|
||
|
|
|
||
|
|
namespace MCPForUnity.Editor.Tools
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// Handles procedural texture generation operations.
|
||
|
|
/// Supports patterns (checkerboard, stripes, dots, grid, brick),
|
||
|
|
/// gradients, noise, and direct pixel manipulation.
|
||
|
|
/// </summary>
|
||
|
|
[McpForUnityTool("manage_texture", AutoRegister = false)]
|
||
|
|
public static class ManageTexture
|
||
|
|
{
|
||
|
|
private const int MaxTextureDimension = 1024;
|
||
|
|
private const int MaxTexturePixels = 1024 * 1024;
|
||
|
|
private const int MaxNoiseWork = 4000000;
|
||
|
|
private static readonly List<string> ValidActions = new List<string>
|
||
|
|
{
|
||
|
|
"create",
|
||
|
|
"modify",
|
||
|
|
"delete",
|
||
|
|
"create_sprite",
|
||
|
|
"apply_pattern",
|
||
|
|
"apply_gradient",
|
||
|
|
"apply_noise"
|
||
|
|
};
|
||
|
|
|
||
|
|
private static ErrorResponse ValidateDimensions(int width, int height, List<string> warnings)
|
||
|
|
{
|
||
|
|
if (width <= 0 || height <= 0)
|
||
|
|
return new ErrorResponse($"Invalid dimensions: {width}x{height}. Must be positive.");
|
||
|
|
if (width > MaxTextureDimension || height > MaxTextureDimension)
|
||
|
|
warnings.Add($"Dimensions exceed recommended max {MaxTextureDimension} per side (got {width}x{height}).");
|
||
|
|
long totalPixels = (long)width * height;
|
||
|
|
if (totalPixels > MaxTexturePixels)
|
||
|
|
warnings.Add($"Total pixels exceed recommended max {MaxTexturePixels} (got {width}x{height}).");
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
public static object HandleCommand(JObject @params)
|
||
|
|
{
|
||
|
|
string action = @params["action"]?.ToString()?.ToLowerInvariant();
|
||
|
|
if (string.IsNullOrEmpty(action))
|
||
|
|
{
|
||
|
|
return new ErrorResponse("Action parameter is required.");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ValidActions.Contains(action))
|
||
|
|
{
|
||
|
|
string validActionsList = string.Join(", ", ValidActions);
|
||
|
|
return new ErrorResponse(
|
||
|
|
$"Unknown action: '{action}'. Valid actions are: {validActionsList}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
string path = @params["path"]?.ToString();
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
switch (action)
|
||
|
|
{
|
||
|
|
case "create":
|
||
|
|
case "create_sprite":
|
||
|
|
return CreateTexture(@params, action == "create_sprite");
|
||
|
|
case "modify":
|
||
|
|
return ModifyTexture(@params);
|
||
|
|
case "delete":
|
||
|
|
return DeleteTexture(path);
|
||
|
|
case "apply_pattern":
|
||
|
|
return ApplyPattern(@params);
|
||
|
|
case "apply_gradient":
|
||
|
|
return ApplyGradient(@params);
|
||
|
|
case "apply_noise":
|
||
|
|
return ApplyNoise(@params);
|
||
|
|
default:
|
||
|
|
return new ErrorResponse($"Unknown action: '{action}'");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
McpLog.Error($"[ManageTexture] Action '{action}' failed: {e}");
|
||
|
|
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Action Implementations ---
|
||
|
|
|
||
|
|
private static object CreateTexture(JObject @params, bool asSprite)
|
||
|
|
{
|
||
|
|
string path = @params["path"]?.ToString();
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return new ErrorResponse("'path' is required for create.");
|
||
|
|
|
||
|
|
string imagePath = @params["imagePath"]?.ToString();
|
||
|
|
bool hasImage = !string.IsNullOrEmpty(imagePath);
|
||
|
|
|
||
|
|
int width = @params["width"]?.ToObject<int>() ?? 64;
|
||
|
|
int height = @params["height"]?.ToObject<int>() ?? 64;
|
||
|
|
List<string> warnings = new List<string>();
|
||
|
|
|
||
|
|
// Validate dimensions
|
||
|
|
if (!hasImage)
|
||
|
|
{
|
||
|
|
var dimensionError = ValidateDimensions(width, height, warnings);
|
||
|
|
if (dimensionError != null)
|
||
|
|
return dimensionError;
|
||
|
|
}
|
||
|
|
|
||
|
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||
|
|
EnsureDirectoryExists(fullPath);
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var fillColorToken = @params["fillColor"];
|
||
|
|
var patternToken = @params["pattern"];
|
||
|
|
var pixelsToken = @params["pixels"];
|
||
|
|
|
||
|
|
if (hasImage && (fillColorToken != null || patternToken != null || pixelsToken != null))
|
||
|
|
{
|
||
|
|
return new ErrorResponse("imagePath cannot be combined with fillColor, pattern, or pixels.");
|
||
|
|
}
|
||
|
|
|
||
|
|
int patternSize = 8;
|
||
|
|
if (!hasImage && patternToken != null)
|
||
|
|
{
|
||
|
|
patternSize = @params["patternSize"]?.ToObject<int>() ?? 8;
|
||
|
|
if (patternSize <= 0)
|
||
|
|
return new ErrorResponse("patternSize must be greater than 0.");
|
||
|
|
}
|
||
|
|
|
||
|
|
Texture2D texture;
|
||
|
|
if (hasImage)
|
||
|
|
{
|
||
|
|
string resolvedImagePath = ResolveImagePath(imagePath);
|
||
|
|
if (!File.Exists(resolvedImagePath))
|
||
|
|
return new ErrorResponse($"Image file not found at '{imagePath}'.");
|
||
|
|
|
||
|
|
byte[] imageBytes = File.ReadAllBytes(resolvedImagePath);
|
||
|
|
texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||
|
|
if (!texture.LoadImage(imageBytes))
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
return new ErrorResponse($"Failed to load image from '{imagePath}'.");
|
||
|
|
}
|
||
|
|
|
||
|
|
width = texture.width;
|
||
|
|
height = texture.height;
|
||
|
|
var imageDimensionError = ValidateDimensions(width, height, warnings);
|
||
|
|
if (imageDimensionError != null)
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
return imageDimensionError;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||
|
|
|
||
|
|
// Check for fill color
|
||
|
|
if (fillColorToken != null && fillColorToken.Type == JTokenType.Array)
|
||
|
|
{
|
||
|
|
Color32 fillColor = TextureOps.ParseColor32(fillColorToken as JArray);
|
||
|
|
TextureOps.FillTexture(texture, fillColor);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for pattern
|
||
|
|
if (patternToken != null)
|
||
|
|
{
|
||
|
|
string pattern = patternToken.ToString();
|
||
|
|
var palette = TextureOps.ParsePalette(@params["palette"] as JArray);
|
||
|
|
ApplyPatternToTexture(texture, pattern, palette, patternSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for direct pixel data
|
||
|
|
if (pixelsToken != null)
|
||
|
|
{
|
||
|
|
TextureOps.ApplyPixelData(texture, pixelsToken, width, height);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If nothing specified, create transparent texture
|
||
|
|
if (fillColorToken == null && patternToken == null && pixelsToken == null)
|
||
|
|
{
|
||
|
|
TextureOps.FillTexture(texture, new Color32(0, 0, 0, 0));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
texture.Apply();
|
||
|
|
|
||
|
|
// Save to disk
|
||
|
|
byte[] imageData = TextureOps.EncodeTexture(texture, fullPath);
|
||
|
|
if (imageData == null || imageData.Length == 0)
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
|
||
|
|
}
|
||
|
|
File.WriteAllBytes(GetAbsolutePath(fullPath), imageData);
|
||
|
|
|
||
|
|
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
// Configure texture importer settings if provided
|
||
|
|
JToken importSettingsToken = @params["importSettings"];
|
||
|
|
JToken spriteSettingsToken = @params["spriteSettings"];
|
||
|
|
|
||
|
|
if (importSettingsToken != null)
|
||
|
|
{
|
||
|
|
ConfigureTextureImporter(fullPath, importSettingsToken);
|
||
|
|
}
|
||
|
|
else if (asSprite || spriteSettingsToken != null)
|
||
|
|
{
|
||
|
|
// Legacy sprite configuration
|
||
|
|
ConfigureAsSprite(fullPath, spriteSettingsToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up memory
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
foreach (var warning in warnings)
|
||
|
|
{
|
||
|
|
McpLog.Warn($"[ManageTexture] {warning}");
|
||
|
|
}
|
||
|
|
|
||
|
|
return new SuccessResponse(
|
||
|
|
$"Texture created at '{fullPath}' ({width}x{height})" + (asSprite ? " as sprite" : ""),
|
||
|
|
new
|
||
|
|
{
|
||
|
|
path = fullPath,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
asSprite = asSprite || spriteSettingsToken != null || (importSettingsToken?["textureType"]?.ToString() == "Sprite"),
|
||
|
|
warnings = warnings.Count > 0 ? warnings : null
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to create texture: {e.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static object ModifyTexture(JObject @params)
|
||
|
|
{
|
||
|
|
string path = @params["path"]?.ToString();
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return new ErrorResponse("'path' is required for modify.");
|
||
|
|
|
||
|
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||
|
|
if (!AssetExists(fullPath))
|
||
|
|
return new ErrorResponse($"Texture not found at path: {fullPath}");
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(fullPath);
|
||
|
|
if (texture == null)
|
||
|
|
return new ErrorResponse($"Failed to load texture at path: {fullPath}");
|
||
|
|
|
||
|
|
// Make the texture readable
|
||
|
|
string absolutePath = GetAbsolutePath(fullPath);
|
||
|
|
byte[] fileData = File.ReadAllBytes(absolutePath);
|
||
|
|
Texture2D editableTexture = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
|
||
|
|
editableTexture.LoadImage(fileData);
|
||
|
|
|
||
|
|
// Apply modifications
|
||
|
|
var setPixelsToken = @params["setPixels"] as JObject;
|
||
|
|
if (setPixelsToken != null)
|
||
|
|
{
|
||
|
|
int x = setPixelsToken["x"]?.ToObject<int>() ?? 0;
|
||
|
|
int y = setPixelsToken["y"]?.ToObject<int>() ?? 0;
|
||
|
|
int w = setPixelsToken["width"]?.ToObject<int>() ?? 1;
|
||
|
|
int h = setPixelsToken["height"]?.ToObject<int>() ?? 1;
|
||
|
|
|
||
|
|
if (w <= 0 || h <= 0)
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(editableTexture);
|
||
|
|
return new ErrorResponse("setPixels width and height must be positive.");
|
||
|
|
}
|
||
|
|
|
||
|
|
var pixelsToken = setPixelsToken["pixels"];
|
||
|
|
var colorToken = setPixelsToken["color"];
|
||
|
|
|
||
|
|
if (pixelsToken != null)
|
||
|
|
{
|
||
|
|
TextureOps.ApplyPixelDataToRegion(editableTexture, pixelsToken, x, y, w, h);
|
||
|
|
}
|
||
|
|
else if (colorToken != null)
|
||
|
|
{
|
||
|
|
Color32 color = TextureOps.ParseColor32(colorToken as JArray);
|
||
|
|
int startX = Mathf.Max(0, x);
|
||
|
|
int startY = Mathf.Max(0, y);
|
||
|
|
int endX = Mathf.Min(x + w, editableTexture.width);
|
||
|
|
int endY = Mathf.Min(y + h, editableTexture.height);
|
||
|
|
|
||
|
|
for (int py = startY; py < endY; py++)
|
||
|
|
{
|
||
|
|
for (int px = startX; px < endX; px++)
|
||
|
|
{
|
||
|
|
editableTexture.SetPixel(px, py, color);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(editableTexture);
|
||
|
|
return new ErrorResponse("setPixels requires 'color' or 'pixels'.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
editableTexture.Apply();
|
||
|
|
|
||
|
|
// Save back to disk
|
||
|
|
byte[] imageData = TextureOps.EncodeTexture(editableTexture, fullPath);
|
||
|
|
if (imageData == null || imageData.Length == 0)
|
||
|
|
{
|
||
|
|
UnityEngine.Object.DestroyImmediate(editableTexture);
|
||
|
|
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
|
||
|
|
}
|
||
|
|
File.WriteAllBytes(absolutePath, imageData);
|
||
|
|
|
||
|
|
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
UnityEngine.Object.DestroyImmediate(editableTexture);
|
||
|
|
|
||
|
|
return new SuccessResponse($"Texture modified: {fullPath}");
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to modify texture: {e.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static object DeleteTexture(string path)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return new ErrorResponse("'path' is required for delete.");
|
||
|
|
|
||
|
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||
|
|
if (!AssetExists(fullPath))
|
||
|
|
return new ErrorResponse($"Texture not found at path: {fullPath}");
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
bool success = AssetDatabase.DeleteAsset(fullPath);
|
||
|
|
if (success)
|
||
|
|
return new SuccessResponse($"Texture deleted: {fullPath}");
|
||
|
|
else
|
||
|
|
return new ErrorResponse($"Failed to delete texture: {fullPath}");
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Error deleting texture: {e.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static object ApplyPattern(JObject @params)
|
||
|
|
{
|
||
|
|
// Reuse CreateTexture with pattern
|
||
|
|
return CreateTexture(@params, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static object ApplyGradient(JObject @params)
|
||
|
|
{
|
||
|
|
string path = @params["path"]?.ToString();
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return new ErrorResponse("'path' is required for apply_gradient.");
|
||
|
|
|
||
|
|
int width = @params["width"]?.ToObject<int>() ?? 64;
|
||
|
|
int height = @params["height"]?.ToObject<int>() ?? 64;
|
||
|
|
List<string> warnings = new List<string>();
|
||
|
|
var dimensionError = ValidateDimensions(width, height, warnings);
|
||
|
|
if (dimensionError != null)
|
||
|
|
return dimensionError;
|
||
|
|
string gradientType = @params["gradientType"]?.ToString() ?? "linear";
|
||
|
|
float angle = @params["gradientAngle"]?.ToObject<float>() ?? 0f;
|
||
|
|
|
||
|
|
var palette = TextureOps.ParsePalette(@params["palette"] as JArray);
|
||
|
|
if (palette == null || palette.Count < 2)
|
||
|
|
{
|
||
|
|
// Default gradient palette
|
||
|
|
palette = new List<Color32> { new Color32(0, 0, 0, 255), new Color32(255, 255, 255, 255) };
|
||
|
|
}
|
||
|
|
|
||
|
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||
|
|
EnsureDirectoryExists(fullPath);
|
||
|
|
|
||
|
|
Texture2D texture = null;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||
|
|
|
||
|
|
if (gradientType == "radial")
|
||
|
|
{
|
||
|
|
ApplyRadialGradient(texture, palette);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ApplyLinearGradient(texture, palette, angle);
|
||
|
|
}
|
||
|
|
|
||
|
|
texture.Apply();
|
||
|
|
|
||
|
|
byte[] imageData = TextureOps.EncodeTexture(texture, fullPath);
|
||
|
|
if (imageData == null || imageData.Length == 0)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
|
||
|
|
}
|
||
|
|
File.WriteAllBytes(GetAbsolutePath(fullPath), imageData);
|
||
|
|
|
||
|
|
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
// Configure as sprite if requested
|
||
|
|
JToken spriteSettingsToken = @params["spriteSettings"];
|
||
|
|
if (spriteSettingsToken != null)
|
||
|
|
{
|
||
|
|
ConfigureAsSprite(fullPath, spriteSettingsToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var warning in warnings)
|
||
|
|
{
|
||
|
|
McpLog.Warn($"[ManageTexture] {warning}");
|
||
|
|
}
|
||
|
|
|
||
|
|
return new SuccessResponse(
|
||
|
|
$"Gradient texture created at '{fullPath}' ({width}x{height})",
|
||
|
|
new
|
||
|
|
{
|
||
|
|
path = fullPath,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
gradientType,
|
||
|
|
warnings = warnings.Count > 0 ? warnings : null
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to create gradient texture: {e.Message}");
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
if (texture != null)
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static object ApplyNoise(JObject @params)
|
||
|
|
{
|
||
|
|
string path = @params["path"]?.ToString();
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return new ErrorResponse("'path' is required for apply_noise.");
|
||
|
|
|
||
|
|
int width = @params["width"]?.ToObject<int>() ?? 64;
|
||
|
|
int height = @params["height"]?.ToObject<int>() ?? 64;
|
||
|
|
List<string> warnings = new List<string>();
|
||
|
|
var dimensionError = ValidateDimensions(width, height, warnings);
|
||
|
|
if (dimensionError != null)
|
||
|
|
return dimensionError;
|
||
|
|
float scale = @params["noiseScale"]?.ToObject<float>() ?? 0.1f;
|
||
|
|
int octaves = @params["octaves"]?.ToObject<int>() ?? 1;
|
||
|
|
if (octaves <= 0)
|
||
|
|
return new ErrorResponse("octaves must be greater than 0.");
|
||
|
|
long noiseWork = (long)width * height * octaves;
|
||
|
|
if (noiseWork > MaxNoiseWork)
|
||
|
|
warnings.Add($"Noise workload exceeds recommended max {MaxNoiseWork} (got {width}x{height}x{octaves}).");
|
||
|
|
|
||
|
|
var palette = TextureOps.ParsePalette(@params["palette"] as JArray);
|
||
|
|
if (palette == null || palette.Count < 2)
|
||
|
|
{
|
||
|
|
palette = new List<Color32> { new Color32(0, 0, 0, 255), new Color32(255, 255, 255, 255) };
|
||
|
|
}
|
||
|
|
|
||
|
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||
|
|
EnsureDirectoryExists(fullPath);
|
||
|
|
|
||
|
|
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
ApplyPerlinNoise(texture, palette, scale, octaves);
|
||
|
|
|
||
|
|
texture.Apply();
|
||
|
|
|
||
|
|
byte[] imageData = TextureOps.EncodeTexture(texture, fullPath);
|
||
|
|
if (imageData == null || imageData.Length == 0)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
|
||
|
|
}
|
||
|
|
File.WriteAllBytes(GetAbsolutePath(fullPath), imageData);
|
||
|
|
|
||
|
|
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
// Configure as sprite if requested
|
||
|
|
JToken spriteSettingsToken = @params["spriteSettings"];
|
||
|
|
if (spriteSettingsToken != null)
|
||
|
|
{
|
||
|
|
ConfigureAsSprite(fullPath, spriteSettingsToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var warning in warnings)
|
||
|
|
{
|
||
|
|
McpLog.Warn($"[ManageTexture] {warning}");
|
||
|
|
}
|
||
|
|
|
||
|
|
return new SuccessResponse(
|
||
|
|
$"Noise texture created at '{fullPath}' ({width}x{height})",
|
||
|
|
new
|
||
|
|
{
|
||
|
|
path = fullPath,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
noiseScale = scale,
|
||
|
|
octaves,
|
||
|
|
warnings = warnings.Count > 0 ? warnings : null
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
return new ErrorResponse($"Failed to create noise texture: {e.Message}");
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
if (texture != null)
|
||
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Pattern Helpers ---
|
||
|
|
|
||
|
|
private static void ApplyPatternToTexture(Texture2D texture, string pattern, List<Color32> palette, int patternSize)
|
||
|
|
{
|
||
|
|
if (palette == null || palette.Count == 0)
|
||
|
|
{
|
||
|
|
palette = new List<Color32> { new Color32(255, 255, 255, 255), new Color32(0, 0, 0, 255) };
|
||
|
|
}
|
||
|
|
|
||
|
|
int width = texture.width;
|
||
|
|
int height = texture.height;
|
||
|
|
|
||
|
|
for (int y = 0; y < height; y++)
|
||
|
|
{
|
||
|
|
for (int x = 0; x < width; x++)
|
||
|
|
{
|
||
|
|
Color32 color = GetPatternColor(x, y, pattern, palette, patternSize, width, height);
|
||
|
|
texture.SetPixel(x, y, color);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Color32 GetPatternColor(int x, int y, string pattern, List<Color32> palette, int size, int width, int height)
|
||
|
|
{
|
||
|
|
int colorIndex = 0;
|
||
|
|
|
||
|
|
switch (pattern.ToLower())
|
||
|
|
{
|
||
|
|
case "checkerboard":
|
||
|
|
colorIndex = ((x / size) + (y / size)) % 2;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "stripes":
|
||
|
|
case "stripes_v":
|
||
|
|
colorIndex = (x / size) % palette.Count;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "stripes_h":
|
||
|
|
colorIndex = (y / size) % palette.Count;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "stripes_diag":
|
||
|
|
colorIndex = ((x + y) / size) % palette.Count;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "dots":
|
||
|
|
int cx = (x % (size * 2)) - size;
|
||
|
|
int cy = (y % (size * 2)) - size;
|
||
|
|
bool inDot = (cx * cx + cy * cy) < (size * size / 4);
|
||
|
|
colorIndex = inDot ? 1 : 0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "grid":
|
||
|
|
bool onGridLine = (x % size == 0) || (y % size == 0);
|
||
|
|
colorIndex = onGridLine ? 1 : 0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "brick":
|
||
|
|
int row = y / size;
|
||
|
|
int offset = (row % 2) * (size / 2);
|
||
|
|
bool onBorder = ((x + offset) % size == 0) || (y % size == 0);
|
||
|
|
colorIndex = onBorder ? 1 : 0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
colorIndex = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return palette[Mathf.Clamp(colorIndex, 0, palette.Count - 1)];
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Gradient Helpers ---
|
||
|
|
|
||
|
|
private static void ApplyLinearGradient(Texture2D texture, List<Color32> palette, float angle)
|
||
|
|
{
|
||
|
|
int width = texture.width;
|
||
|
|
int height = texture.height;
|
||
|
|
float radians = angle * Mathf.Deg2Rad;
|
||
|
|
Vector2 dir = new Vector2(Mathf.Cos(radians), Mathf.Sin(radians));
|
||
|
|
float denomX = Mathf.Max(1, width - 1);
|
||
|
|
float denomY = Mathf.Max(1, height - 1);
|
||
|
|
|
||
|
|
for (int y = 0; y < height; y++)
|
||
|
|
{
|
||
|
|
for (int x = 0; x < width; x++)
|
||
|
|
{
|
||
|
|
float nx = x / denomX;
|
||
|
|
float ny = y / denomY;
|
||
|
|
float t = Vector2.Dot(new Vector2(nx, ny), dir);
|
||
|
|
t = Mathf.Clamp01((t + 1f) / 2f);
|
||
|
|
|
||
|
|
Color32 color = LerpPalette(palette, t);
|
||
|
|
texture.SetPixel(x, y, color);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ApplyRadialGradient(Texture2D texture, List<Color32> palette)
|
||
|
|
{
|
||
|
|
int width = texture.width;
|
||
|
|
int height = texture.height;
|
||
|
|
float cx = width / 2f;
|
||
|
|
float cy = height / 2f;
|
||
|
|
float maxDist = Mathf.Sqrt(cx * cx + cy * cy);
|
||
|
|
|
||
|
|
for (int y = 0; y < height; y++)
|
||
|
|
{
|
||
|
|
for (int x = 0; x < width; x++)
|
||
|
|
{
|
||
|
|
float dx = x - cx;
|
||
|
|
float dy = y - cy;
|
||
|
|
float dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||
|
|
float t = Mathf.Clamp01(dist / maxDist);
|
||
|
|
|
||
|
|
Color32 color = LerpPalette(palette, t);
|
||
|
|
texture.SetPixel(x, y, color);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Color32 LerpPalette(List<Color32> palette, float t)
|
||
|
|
{
|
||
|
|
if (palette.Count == 1) return palette[0];
|
||
|
|
if (t <= 0) return palette[0];
|
||
|
|
if (t >= 1) return palette[palette.Count - 1];
|
||
|
|
|
||
|
|
float scaledT = t * (palette.Count - 1);
|
||
|
|
int index = Mathf.FloorToInt(scaledT);
|
||
|
|
float localT = scaledT - index;
|
||
|
|
|
||
|
|
if (index >= palette.Count - 1)
|
||
|
|
return palette[palette.Count - 1];
|
||
|
|
|
||
|
|
Color c1 = palette[index];
|
||
|
|
Color c2 = palette[index + 1];
|
||
|
|
return Color.Lerp(c1, c2, localT);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Noise Helpers ---
|
||
|
|
|
||
|
|
private static void ApplyPerlinNoise(Texture2D texture, List<Color32> palette, float scale, int octaves)
|
||
|
|
{
|
||
|
|
int width = texture.width;
|
||
|
|
int height = texture.height;
|
||
|
|
|
||
|
|
// Random offset to ensure different patterns
|
||
|
|
float offsetX = UnityEngine.Random.Range(0f, 1000f);
|
||
|
|
float offsetY = UnityEngine.Random.Range(0f, 1000f);
|
||
|
|
|
||
|
|
for (int y = 0; y < height; y++)
|
||
|
|
{
|
||
|
|
for (int x = 0; x < width; x++)
|
||
|
|
{
|
||
|
|
float noiseValue = 0f;
|
||
|
|
float amplitude = 1f;
|
||
|
|
float frequency = 1f;
|
||
|
|
float maxValue = 0f;
|
||
|
|
|
||
|
|
for (int o = 0; o < octaves; o++)
|
||
|
|
{
|
||
|
|
float sampleX = (x + offsetX) * scale * frequency;
|
||
|
|
float sampleY = (y + offsetY) * scale * frequency;
|
||
|
|
noiseValue += Mathf.PerlinNoise(sampleX, sampleY) * amplitude;
|
||
|
|
maxValue += amplitude;
|
||
|
|
amplitude *= 0.5f;
|
||
|
|
frequency *= 2f;
|
||
|
|
}
|
||
|
|
|
||
|
|
float t = Mathf.Clamp01(noiseValue / maxValue);
|
||
|
|
Color32 color = LerpPalette(palette, t);
|
||
|
|
texture.SetPixel(x, y, color);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ConfigureAsSprite(string path, JToken spriteSettings)
|
||
|
|
{
|
||
|
|
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||
|
|
if (importer == null)
|
||
|
|
{
|
||
|
|
McpLog.Warn($"[ManageTexture] Could not get TextureImporter for {path}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
importer.textureType = TextureImporterType.Sprite;
|
||
|
|
importer.spriteImportMode = SpriteImportMode.Single;
|
||
|
|
|
||
|
|
if (spriteSettings != null && spriteSettings.Type == JTokenType.Object)
|
||
|
|
{
|
||
|
|
var settings = spriteSettings as JObject;
|
||
|
|
|
||
|
|
// Pivot
|
||
|
|
var pivotToken = settings["pivot"];
|
||
|
|
if (pivotToken is JArray pivotArray && pivotArray.Count >= 2)
|
||
|
|
{
|
||
|
|
importer.spritePivot = new Vector2(
|
||
|
|
pivotArray[0].ToObject<float>(),
|
||
|
|
pivotArray[1].ToObject<float>()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Pixels per unit
|
||
|
|
var ppuToken = settings["pixelsPerUnit"];
|
||
|
|
if (ppuToken != null)
|
||
|
|
{
|
||
|
|
importer.spritePixelsPerUnit = ppuToken.ToObject<float>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
importer.SaveAndReimport();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ConfigureTextureImporter(string path, JToken importSettings)
|
||
|
|
{
|
||
|
|
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||
|
|
if (importer == null)
|
||
|
|
{
|
||
|
|
McpLog.Warn($"[ManageTexture] Could not get TextureImporter for {path}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (importSettings == null || importSettings.Type != JTokenType.Object)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var settings = importSettings as JObject;
|
||
|
|
|
||
|
|
// Texture Type
|
||
|
|
var textureTypeToken = settings["textureType"];
|
||
|
|
if (textureTypeToken != null)
|
||
|
|
{
|
||
|
|
string typeStr = textureTypeToken.ToString();
|
||
|
|
if (TryParseEnum<TextureImporterType>(typeStr, out var textureType))
|
||
|
|
{
|
||
|
|
importer.textureType = textureType;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Texture Shape
|
||
|
|
var textureShapeToken = settings["textureShape"];
|
||
|
|
if (textureShapeToken != null)
|
||
|
|
{
|
||
|
|
string shapeStr = textureShapeToken.ToString();
|
||
|
|
if (TryParseEnum<TextureImporterShape>(shapeStr, out var textureShape))
|
||
|
|
{
|
||
|
|
importer.textureShape = textureShape;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// sRGB
|
||
|
|
var srgbToken = settings["sRGBTexture"];
|
||
|
|
if (srgbToken != null)
|
||
|
|
{
|
||
|
|
importer.sRGBTexture = srgbToken.ToObject<bool>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alpha Source
|
||
|
|
var alphaSourceToken = settings["alphaSource"];
|
||
|
|
if (alphaSourceToken != null)
|
||
|
|
{
|
||
|
|
string alphaStr = alphaSourceToken.ToString();
|
||
|
|
if (TryParseEnum<TextureImporterAlphaSource>(alphaStr, out var alphaSource))
|
||
|
|
{
|
||
|
|
importer.alphaSource = alphaSource;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alpha Is Transparency
|
||
|
|
var alphaTransToken = settings["alphaIsTransparency"];
|
||
|
|
if (alphaTransToken != null)
|
||
|
|
{
|
||
|
|
importer.alphaIsTransparency = alphaTransToken.ToObject<bool>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Readable
|
||
|
|
var readableToken = settings["isReadable"];
|
||
|
|
if (readableToken != null)
|
||
|
|
{
|
||
|
|
importer.isReadable = readableToken.ToObject<bool>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mipmaps
|
||
|
|
var mipmapToken = settings["mipmapEnabled"];
|
||
|
|
if (mipmapToken != null)
|
||
|
|
{
|
||
|
|
importer.mipmapEnabled = mipmapToken.ToObject<bool>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mipmap Filter
|
||
|
|
var mipmapFilterToken = settings["mipmapFilter"];
|
||
|
|
if (mipmapFilterToken != null)
|
||
|
|
{
|
||
|
|
string filterStr = mipmapFilterToken.ToString();
|
||
|
|
if (TryParseEnum<TextureImporterMipFilter>(filterStr, out var mipmapFilter))
|
||
|
|
{
|
||
|
|
importer.mipmapFilter = mipmapFilter;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wrap Mode
|
||
|
|
var wrapModeToken = settings["wrapMode"];
|
||
|
|
if (wrapModeToken != null)
|
||
|
|
{
|
||
|
|
string wrapStr = wrapModeToken.ToString();
|
||
|
|
if (TryParseEnum<TextureWrapMode>(wrapStr, out var wrapMode))
|
||
|
|
{
|
||
|
|
importer.wrapMode = wrapMode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wrap Mode U
|
||
|
|
var wrapModeUToken = settings["wrapModeU"];
|
||
|
|
if (wrapModeUToken != null)
|
||
|
|
{
|
||
|
|
string wrapStr = wrapModeUToken.ToString();
|
||
|
|
if (TryParseEnum<TextureWrapMode>(wrapStr, out var wrapMode))
|
||
|
|
{
|
||
|
|
importer.wrapModeU = wrapMode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wrap Mode V
|
||
|
|
var wrapModeVToken = settings["wrapModeV"];
|
||
|
|
if (wrapModeVToken != null)
|
||
|
|
{
|
||
|
|
string wrapStr = wrapModeVToken.ToString();
|
||
|
|
if (TryParseEnum<TextureWrapMode>(wrapStr, out var wrapMode))
|
||
|
|
{
|
||
|
|
importer.wrapModeV = wrapMode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filter Mode
|
||
|
|
var filterModeToken = settings["filterMode"];
|
||
|
|
if (filterModeToken != null)
|
||
|
|
{
|
||
|
|
string filterStr = filterModeToken.ToString();
|
||
|
|
if (TryParseEnum<FilterMode>(filterStr, out var filterMode))
|
||
|
|
{
|
||
|
|
importer.filterMode = filterMode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aniso Level
|
||
|
|
var anisoToken = settings["anisoLevel"];
|
||
|
|
if (anisoToken != null)
|
||
|
|
{
|
||
|
|
importer.anisoLevel = anisoToken.ToObject<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Max Texture Size
|
||
|
|
var maxSizeToken = settings["maxTextureSize"];
|
||
|
|
if (maxSizeToken != null)
|
||
|
|
{
|
||
|
|
importer.maxTextureSize = maxSizeToken.ToObject<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compression
|
||
|
|
var compressionToken = settings["textureCompression"];
|
||
|
|
if (compressionToken != null)
|
||
|
|
{
|
||
|
|
string compStr = compressionToken.ToString();
|
||
|
|
if (TryParseEnum<TextureImporterCompression>(compStr, out var compression))
|
||
|
|
{
|
||
|
|
importer.textureCompression = compression;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Crunched Compression
|
||
|
|
var crunchedToken = settings["crunchedCompression"];
|
||
|
|
if (crunchedToken != null)
|
||
|
|
{
|
||
|
|
importer.crunchedCompression = crunchedToken.ToObject<bool>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compression Quality
|
||
|
|
var qualityToken = settings["compressionQuality"];
|
||
|
|
if (qualityToken != null)
|
||
|
|
{
|
||
|
|
importer.compressionQuality = qualityToken.ToObject<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Sprite-specific settings ---
|
||
|
|
|
||
|
|
// Sprite Import Mode
|
||
|
|
var spriteModeToken = settings["spriteImportMode"];
|
||
|
|
if (spriteModeToken != null)
|
||
|
|
{
|
||
|
|
string modeStr = spriteModeToken.ToString();
|
||
|
|
if (TryParseEnum<SpriteImportMode>(modeStr, out var spriteMode))
|
||
|
|
{
|
||
|
|
importer.spriteImportMode = spriteMode;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sprite Pixels Per Unit
|
||
|
|
var ppuToken = settings["spritePixelsPerUnit"];
|
||
|
|
if (ppuToken != null)
|
||
|
|
{
|
||
|
|
importer.spritePixelsPerUnit = ppuToken.ToObject<float>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sprite Pivot
|
||
|
|
var pivotToken = settings["spritePivot"];
|
||
|
|
if (pivotToken is JArray pivotArray && pivotArray.Count >= 2)
|
||
|
|
{
|
||
|
|
importer.spritePivot = new Vector2(
|
||
|
|
pivotArray[0].ToObject<float>(),
|
||
|
|
pivotArray[1].ToObject<float>()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply sprite settings using TextureImporterSettings helper
|
||
|
|
TextureImporterSettings importerSettings = new TextureImporterSettings();
|
||
|
|
importer.ReadTextureSettings(importerSettings);
|
||
|
|
|
||
|
|
bool settingsChanged = false;
|
||
|
|
|
||
|
|
// Sprite Mesh Type
|
||
|
|
var meshTypeToken = settings["spriteMeshType"];
|
||
|
|
if (meshTypeToken != null)
|
||
|
|
{
|
||
|
|
string meshStr = meshTypeToken.ToString();
|
||
|
|
if (TryParseEnum<SpriteMeshType>(meshStr, out var meshType))
|
||
|
|
{
|
||
|
|
importerSettings.spriteMeshType = meshType;
|
||
|
|
settingsChanged = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sprite Extrude
|
||
|
|
var extrudeToken = settings["spriteExtrude"];
|
||
|
|
if (extrudeToken != null)
|
||
|
|
{
|
||
|
|
importerSettings.spriteExtrude = (uint)extrudeToken.ToObject<int>();
|
||
|
|
settingsChanged = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (settingsChanged)
|
||
|
|
{
|
||
|
|
importer.SetTextureSettings(importerSettings);
|
||
|
|
}
|
||
|
|
|
||
|
|
importer.SaveAndReimport();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool TryParseEnum<T>(string value, out T result) where T : struct
|
||
|
|
{
|
||
|
|
// Try exact match first
|
||
|
|
if (Enum.TryParse<T>(value, true, out result))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try without common prefixes/suffixes
|
||
|
|
string cleanValue = value.Replace("_", "").Replace("-", "");
|
||
|
|
if (Enum.TryParse<T>(cleanValue, true, out result))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
result = default;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool AssetExists(string path)
|
||
|
|
{
|
||
|
|
return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(path));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void EnsureDirectoryExists(string assetPath)
|
||
|
|
{
|
||
|
|
string directory = Path.GetDirectoryName(assetPath);
|
||
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(GetAbsolutePath(directory)))
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(GetAbsolutePath(directory));
|
||
|
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string GetAbsolutePath(string assetPath)
|
||
|
|
{
|
||
|
|
return Path.Combine(Directory.GetCurrentDirectory(), assetPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string ResolveImagePath(string imagePath)
|
||
|
|
{
|
||
|
|
if (Path.IsPathRooted(imagePath))
|
||
|
|
return imagePath;
|
||
|
|
|
||
|
|
return Path.Combine(Directory.GetCurrentDirectory(), imagePath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|