Skip to content

Commit 11dd3e2

Browse files
authored
Merge pull request #982 from zaferdace/beta
feat: add set_import_settings action to manage_texture tool
2 parents 49b749a + 7505094 commit 11dd3e2

3 files changed

Lines changed: 264 additions & 34 deletions

File tree

MCPForUnity/Editor/Tools/ManageTexture.cs

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public static class ManageTexture
2727
"create_sprite",
2828
"apply_pattern",
2929
"apply_gradient",
30-
"apply_noise"
30+
"apply_noise",
31+
"set_import_settings"
3132
};
3233

3334
private static ErrorResponse ValidateDimensions(int width, int height, List<string> warnings)
@@ -78,6 +79,8 @@ public static object HandleCommand(JObject @params)
7879
return ApplyGradient(@params);
7980
case "apply_noise":
8081
return ApplyNoise(@params);
82+
case "set_import_settings":
83+
return SetImportSettings(@params);
8184
default:
8285
return new ErrorResponse($"Unknown action: '{action}'");
8386
}
@@ -254,20 +257,36 @@ private static object ModifyTexture(JObject @params)
254257

255258
try
256259
{
257-
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(fullPath);
258-
if (texture == null)
259-
return new ErrorResponse($"Failed to load texture at path: {fullPath}");
260+
var setPixelsToken = @params["setPixels"] as JObject;
261+
bool hasImportSettings = HasImportSettingsParams(@params);
260262

261-
// Make the texture readable
262-
string absolutePath = GetAbsolutePath(fullPath);
263-
byte[] fileData = File.ReadAllBytes(absolutePath);
264-
Texture2D editableTexture = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
265-
editableTexture.LoadImage(fileData);
263+
// Validate import settings before any writes
264+
if (hasImportSettings)
265+
{
266+
var validationError = ValidateImportSettingsParams(@params);
267+
if (validationError != null) return validationError;
268+
}
266269

267-
// Apply modifications
268-
var setPixelsToken = @params["setPixels"] as JObject;
270+
// Fast path: only import settings, no pixel changes
271+
if (setPixelsToken == null && hasImportSettings)
272+
{
273+
var error = ApplyImportSettingsParams(fullPath, @params);
274+
if (error != null) return error;
275+
return new SuccessResponse($"Texture modified: {fullPath}");
276+
}
277+
278+
// Pixel modification path
269279
if (setPixelsToken != null)
270280
{
281+
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(fullPath);
282+
if (texture == null)
283+
return new ErrorResponse($"Failed to load texture at path: {fullPath}");
284+
285+
string absolutePath = GetAbsolutePath(fullPath);
286+
byte[] fileData = File.ReadAllBytes(absolutePath);
287+
Texture2D editableTexture = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
288+
editableTexture.LoadImage(fileData);
289+
271290
int x = setPixelsToken["x"]?.ToObject<int>() ?? 0;
272291
int y = setPixelsToken["y"]?.ToObject<int>() ?? 0;
273292
int w = setPixelsToken["width"]?.ToObject<int>() ?? 1;
@@ -307,22 +326,25 @@ private static object ModifyTexture(JObject @params)
307326
UnityEngine.Object.DestroyImmediate(editableTexture);
308327
return new ErrorResponse("setPixels requires 'color' or 'pixels'.");
309328
}
310-
}
311329

312-
editableTexture.Apply();
330+
editableTexture.Apply();
313331

314-
// Save back to disk
315-
byte[] imageData = TextureOps.EncodeTexture(editableTexture, fullPath);
316-
if (imageData == null || imageData.Length == 0)
317-
{
332+
byte[] imageData = TextureOps.EncodeTexture(editableTexture, fullPath);
333+
if (imageData == null || imageData.Length == 0)
334+
{
335+
UnityEngine.Object.DestroyImmediate(editableTexture);
336+
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
337+
}
338+
File.WriteAllBytes(absolutePath, imageData);
339+
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
318340
UnityEngine.Object.DestroyImmediate(editableTexture);
319-
return new ErrorResponse($"Failed to encode texture for '{fullPath}'");
320341
}
321-
File.WriteAllBytes(absolutePath, imageData);
322342

323-
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
324-
325-
UnityEngine.Object.DestroyImmediate(editableTexture);
343+
if (hasImportSettings)
344+
{
345+
var importError = ApplyImportSettingsParams(fullPath, @params);
346+
if (importError != null) return importError;
347+
}
326348

327349
return new SuccessResponse($"Texture modified: {fullPath}");
328350
}
@@ -703,6 +725,85 @@ private static void ApplyPerlinNoise(Texture2D texture, List<Color32> palette, f
703725
}
704726
}
705727

728+
private static bool HasImportSettingsParams(JObject @params)
729+
{
730+
JToken importSettingsToken = @params["import_settings"] ?? @params["importSettings"];
731+
JToken asSpriteToken = @params["as_sprite"] ?? @params["spriteSettings"];
732+
733+
bool hasImportSettings = importSettingsToken is JObject importObject && importObject.HasValues;
734+
bool hasSpriteSettings = (asSpriteToken is JObject spriteObject && spriteObject.HasValues)
735+
|| (asSpriteToken?.Type == JTokenType.Boolean && asSpriteToken.ToObject<bool>());
736+
737+
return hasImportSettings || hasSpriteSettings;
738+
}
739+
740+
private static object ValidateImportSettingsParams(JObject @params)
741+
{
742+
JToken importSettingsToken = @params["import_settings"] ?? @params["importSettings"];
743+
JToken asSpriteToken = @params["as_sprite"] ?? @params["spriteSettings"];
744+
745+
if (importSettingsToken != null && asSpriteToken != null)
746+
{
747+
return new ErrorResponse("Cannot specify both 'import_settings' and 'as_sprite'.");
748+
}
749+
return null;
750+
}
751+
752+
private static object ApplyImportSettingsParams(string fullPath, JObject @params)
753+
{
754+
JToken importSettingsToken = @params["import_settings"] ?? @params["importSettings"];
755+
JToken asSpriteToken = @params["as_sprite"] ?? @params["spriteSettings"];
756+
757+
if (importSettingsToken != null && asSpriteToken != null)
758+
{
759+
return new ErrorResponse(
760+
"Cannot specify both 'import_settings' and 'as_sprite'. " +
761+
"Use 'import_settings' with textureType='Sprite' instead.");
762+
}
763+
764+
if (importSettingsToken != null)
765+
{
766+
ConfigureTextureImporter(fullPath, importSettingsToken);
767+
}
768+
else if (asSpriteToken != null &&
769+
(asSpriteToken.Type == JTokenType.Boolean ? asSpriteToken.ToObject<bool>() : true))
770+
{
771+
ConfigureAsSprite(fullPath, asSpriteToken.Type == JTokenType.Object ? asSpriteToken : null);
772+
}
773+
774+
return null;
775+
}
776+
777+
private static object SetImportSettings(JObject @params)
778+
{
779+
var toolParams = new MCPForUnity.Editor.Helpers.ToolParams(@params);
780+
var pathResult = toolParams.GetRequired("path", "'path' is required for set_import_settings.");
781+
if (!pathResult.IsSuccess)
782+
return new ErrorResponse(pathResult.ErrorMessage);
783+
string path = pathResult.Value;
784+
785+
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
786+
if (!AssetExists(fullPath))
787+
return new ErrorResponse($"Texture not found at path: {fullPath}");
788+
789+
try
790+
{
791+
if (!HasImportSettingsParams(@params))
792+
{
793+
return new ErrorResponse("Either 'import_settings' or 'as_sprite' is required.");
794+
}
795+
796+
var error = ApplyImportSettingsParams(fullPath, @params);
797+
if (error != null) return error;
798+
799+
return new SuccessResponse($"Import settings updated for: {fullPath}", new { path = fullPath });
800+
}
801+
catch (Exception e)
802+
{
803+
return new ErrorResponse($"Failed to set import settings: {e.Message}");
804+
}
805+
}
806+
706807
private static void ConfigureAsSprite(string path, JToken spriteSettings)
707808
{
708809
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;

Server/src/cli/commands/texture.py

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -481,29 +481,113 @@ def sprite(path: str, width: int, height: int, image_path: Optional[str], color:
481481
print_success(f"Created sprite: {path}")
482482

483483

484+
def _build_import_settings_from_flags(
485+
texture_type: Optional[str],
486+
sprite_mode: Optional[str],
487+
sprite_ppu: Optional[float],
488+
max_size: Optional[str],
489+
compression: Optional[str],
490+
generate_mipmaps: Optional[bool],
491+
srgb: Optional[bool],
492+
readable: Optional[bool],
493+
) -> dict[str, Any]:
494+
"""Build importSettings dict from CLI flags. Returns empty dict if no flags set."""
495+
import_settings: dict[str, Any] = {}
496+
if texture_type:
497+
import_settings["textureType"] = _TEXTURE_TYPES[texture_type]
498+
if sprite_mode:
499+
import_settings["spriteImportMode"] = _SPRITE_MODES[sprite_mode]
500+
if sprite_ppu is not None:
501+
import_settings["spritePixelsPerUnit"] = sprite_ppu
502+
if max_size:
503+
import_settings["maxTextureSize"] = int(max_size)
504+
if compression:
505+
import_settings["textureCompression"] = _COMPRESSIONS[compression]
506+
if generate_mipmaps is not None:
507+
import_settings["mipmapEnabled"] = generate_mipmaps
508+
if srgb is not None:
509+
import_settings["sRGBTexture"] = srgb
510+
if readable is not None:
511+
import_settings["isReadable"] = readable
512+
return import_settings
513+
514+
515+
def _apply_import_flags_to_params(
516+
params: dict[str, Any],
517+
texture_type: Optional[str],
518+
sprite_mode: Optional[str],
519+
sprite_ppu: Optional[float],
520+
max_size: Optional[str],
521+
compression: Optional[str],
522+
generate_mipmaps: Optional[bool],
523+
srgb: Optional[bool],
524+
readable: Optional[bool],
525+
as_sprite: bool,
526+
) -> bool:
527+
"""Validate and apply import-setting flags to params dict. Returns True if any import setting present."""
528+
has_other_flags = any(v is not None for v in (
529+
texture_type, sprite_mode, sprite_ppu, max_size, compression, generate_mipmaps, srgb, readable))
530+
531+
if as_sprite:
532+
if has_other_flags:
533+
print_error("--as-sprite cannot be combined with other import-setting flags")
534+
sys.exit(1)
535+
params["spriteSettings"] = {"pivot": [0.5, 0.5], "pixelsPerUnit": 100}
536+
return True
537+
538+
if has_other_flags:
539+
import_settings = _build_import_settings_from_flags(
540+
texture_type, sprite_mode, sprite_ppu, max_size, compression,
541+
generate_mipmaps, srgb, readable)
542+
if import_settings:
543+
params["importSettings"] = import_settings
544+
return True
545+
546+
return False
547+
548+
484549
@texture.command("modify")
485550
@click.argument("path")
486-
@click.option("--set-pixels", required=True, help="Modification args as JSON")
551+
@click.option("--set-pixels", default=None, help="Modification args as JSON")
552+
@click.option("--texture-type", type=click.Choice(list(_TEXTURE_TYPES.keys())), help="Texture type")
553+
@click.option("--sprite-mode", type=click.Choice(list(_SPRITE_MODES.keys())), help="Sprite import mode")
554+
@click.option("--sprite-ppu", type=float, help="Sprite pixels per unit")
555+
@click.option("--max-size", type=click.Choice(["32", "64", "128", "256", "512", "1024", "2048", "4096", "8192", "16384"]), help="Max texture size")
556+
@click.option("--compression", type=click.Choice(list(_COMPRESSIONS.keys())), help="Compression quality")
557+
@click.option("--generate-mipmaps/--no-mipmaps", default=None, help="Generate mipmaps")
558+
@click.option("--srgb/--linear", default=None, help="sRGB color texture")
559+
@click.option("--readable/--no-readable", default=None, help="Read/Write enabled")
560+
@click.option("--as-sprite", is_flag=True, help="Shorthand: set texture type to Sprite with defaults")
487561
@handle_unity_errors
488-
def modify(path: str, set_pixels: str):
562+
def modify(path: str, set_pixels: Optional[str], texture_type: Optional[str], sprite_mode: Optional[str],
563+
sprite_ppu: Optional[float], max_size: Optional[str], compression: Optional[str],
564+
generate_mipmaps: Optional[bool], srgb: Optional[bool], readable: Optional[bool],
565+
as_sprite: bool):
489566
"""Modify an existing texture.
490567
491568
\b
492569
Examples:
493570
unity-mcp texture modify Assets/Tex.png --set-pixels '{"x":0,"y":0,"width":10,"height":10,"color":[255,0,0]}'
494571
unity-mcp texture modify Assets/Tex.png --set-pixels '{"x":0,"y":0,"width":2,"height":2,"pixels":[[255,0,0,255],[0,255,0,255],[0,0,255,255],[255,255,0,255]]}'
572+
unity-mcp texture modify Assets/UI/icon.png --as-sprite
573+
unity-mcp texture modify Assets/UI/bg.png --texture-type sprite --max-size 2048
495574
"""
496575
config = get_config()
497576

498-
params: dict[str, Any] = {
499-
"action": "modify",
500-
"path": path,
501-
}
577+
params: dict[str, Any] = {"action": "modify", "path": path}
502578

503-
try:
504-
params["setPixels"] = _normalize_set_pixels(set_pixels)
505-
except ValueError as e:
506-
print_error(str(e))
579+
has_import = _apply_import_flags_to_params(
580+
params, texture_type, sprite_mode, sprite_ppu, max_size,
581+
compression, generate_mipmaps, srgb, readable, as_sprite)
582+
583+
if set_pixels is not None:
584+
try:
585+
params["setPixels"] = _normalize_set_pixels(set_pixels)
586+
except ValueError as e:
587+
print_error(str(e))
588+
sys.exit(1)
589+
elif not has_import:
590+
print_error("At least one of --set-pixels or an import-setting flag must be provided")
507591
sys.exit(1)
508592

509593
result = run_command("manage_texture", params, config)
@@ -538,3 +622,46 @@ def delete(path: str, force: bool):
538622
click.echo(format_output(result, config.format))
539623
if result.get("success"):
540624
print_success(f"Deleted texture: {path}")
625+
626+
627+
@texture.command("set-import-settings")
628+
@click.argument("path")
629+
@click.option("--texture-type", type=click.Choice(list(_TEXTURE_TYPES.keys())), help="Texture type")
630+
@click.option("--sprite-mode", type=click.Choice(list(_SPRITE_MODES.keys())), help="Sprite import mode")
631+
@click.option("--sprite-ppu", type=float, help="Sprite pixels per unit")
632+
@click.option("--max-size", type=click.Choice(["32", "64", "128", "256", "512", "1024", "2048", "4096", "8192", "16384"]), help="Max texture size")
633+
@click.option("--compression", type=click.Choice(list(_COMPRESSIONS.keys())), help="Compression quality")
634+
@click.option("--generate-mipmaps/--no-mipmaps", default=None, help="Generate mipmaps")
635+
@click.option("--srgb/--linear", default=None, help="sRGB color texture")
636+
@click.option("--readable/--no-readable", default=None, help="Read/Write enabled")
637+
@click.option("--as-sprite", is_flag=True, help="Shorthand: set texture type to Sprite with defaults")
638+
@handle_unity_errors
639+
def set_import_settings(path: str, texture_type: Optional[str], sprite_mode: Optional[str],
640+
sprite_ppu: Optional[float], max_size: Optional[str],
641+
compression: Optional[str], generate_mipmaps: Optional[bool],
642+
srgb: Optional[bool], readable: Optional[bool], as_sprite: bool):
643+
"""Change import settings on an existing texture.
644+
645+
\b
646+
Examples:
647+
unity-mcp texture set-import-settings Assets/UI/icon.png --texture-type sprite
648+
unity-mcp texture set-import-settings Assets/UI/icon.png --as-sprite
649+
unity-mcp texture set-import-settings Assets/UI/icon.png --texture-type sprite --sprite-mode single --sprite-ppu 100
650+
unity-mcp texture set-import-settings Assets/UI/bg.png --max-size 2048 --compression high_quality
651+
"""
652+
config = get_config()
653+
654+
params: dict[str, Any] = {"action": "set_import_settings", "path": path}
655+
656+
has_import = _apply_import_flags_to_params(
657+
params, texture_type, sprite_mode, sprite_ppu, max_size,
658+
compression, generate_mipmaps, srgb, readable, as_sprite)
659+
660+
if not has_import:
661+
print_error("At least one import setting must be specified")
662+
sys.exit(1)
663+
664+
result = run_command("manage_texture", params, config)
665+
click.echo(format_output(result, config.format))
666+
if result.get("success"):
667+
print_success(f"Import settings updated: {path}")

Server/src/services/tools/manage_texture.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ def _normalize_import_settings(value: Any) -> tuple[dict | None, str | None]:
378378
description=(
379379
"Procedural texture generation for Unity. Creates textures with solid fills, "
380380
"patterns (checkerboard, stripes, dots, grid, brick), gradients, and noise. "
381-
"Actions: create, modify, delete, create_sprite, apply_pattern, apply_gradient, apply_noise"
381+
"Actions: create, modify, delete, create_sprite, apply_pattern, apply_gradient, apply_noise, "
382+
"set_import_settings"
382383
),
383384
annotations=ToolAnnotations(
384385
title="Manage Texture",
@@ -394,7 +395,8 @@ async def manage_texture(
394395
"create_sprite",
395396
"apply_pattern",
396397
"apply_gradient",
397-
"apply_noise"
398+
"apply_noise",
399+
"set_import_settings"
398400
], "Action to perform."],
399401

400402
# Required for most actions

0 commit comments

Comments
 (0)