mcpforunityserver 9.4.0b20260203025228__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. utils/module_discovery.py +55 -0
@@ -0,0 +1,581 @@
1
+ """
2
+ Defines the manage_texture tool for procedural texture generation in Unity.
3
+ """
4
+ import base64
5
+ import json
6
+ from typing import Annotated, Any, Literal
7
+
8
+ from fastmcp import Context
9
+ from mcp.types import ToolAnnotations
10
+
11
+ from services.registry import mcp_for_unity_tool
12
+ from services.tools import get_unity_instance_from_context
13
+ from services.tools.utils import parse_json_payload, coerce_bool, coerce_int, normalize_color
14
+ from transport.unity_transport import send_with_unity_instance
15
+ from transport.legacy.unity_connection import async_send_command_with_retry
16
+ from services.tools.preflight import preflight
17
+
18
+
19
+ def _normalize_dimension(value: Any, name: str, default: int = 64) -> tuple[int | None, str | None]:
20
+ if value is None:
21
+ return default, None
22
+ coerced = coerce_int(value)
23
+ if coerced is None:
24
+ return None, f"{name} must be an integer"
25
+ if coerced <= 0:
26
+ return None, f"{name} must be positive"
27
+ return coerced, None
28
+
29
+
30
+ def _normalize_positive_int(value: Any, name: str) -> tuple[int | None, str | None]:
31
+ if value is None:
32
+ return None, None
33
+ coerced = coerce_int(value)
34
+ if coerced is None or coerced <= 0:
35
+ return None, f"{name} must be a positive integer"
36
+ return coerced, None
37
+
38
+
39
+ def _normalize_color_int(value: Any) -> tuple[list[int] | None, str | None]:
40
+ """Thin wrapper for normalize_color with int output for texture operations."""
41
+ return normalize_color(value, output_range="int")
42
+
43
+
44
+ def _normalize_palette(value: Any) -> tuple[list[list[int]] | None, str | None]:
45
+ """
46
+ Normalize color palette to list of [r, g, b, a] colors (0-255).
47
+ Returns (parsed_palette, error_message).
48
+ """
49
+ if value is None:
50
+ return None, None
51
+
52
+ # Try parsing as string first
53
+ if isinstance(value, str):
54
+ if value in ("[object Object]", "undefined", "null", ""):
55
+ return None, f"palette received invalid value: '{value}'"
56
+ value = parse_json_payload(value)
57
+
58
+ if not isinstance(value, list):
59
+ return None, f"palette must be a list of colors, got {type(value).__name__}"
60
+
61
+ normalized = []
62
+ for i, color in enumerate(value):
63
+ parsed, error = _normalize_color_int(color)
64
+ if error:
65
+ return None, f"palette[{i}]: {error}"
66
+ normalized.append(parsed)
67
+
68
+ return normalized, None
69
+
70
+
71
+ def _normalize_pixels(value: Any, width: int, height: int) -> tuple[list[list[int]] | str | None, str | None]:
72
+ """
73
+ Normalize pixel data to list of [r, g, b, a] colors or base64 string.
74
+ Returns (pixels, error_message).
75
+ """
76
+ if value is None:
77
+ return None, None
78
+
79
+ # Base64 string
80
+ if isinstance(value, str):
81
+ if value.startswith("base64:"):
82
+ return value, None # Pass through for Unity to decode
83
+ # Try parsing as JSON array
84
+ parsed = parse_json_payload(value)
85
+ if isinstance(parsed, list):
86
+ value = parsed
87
+ else:
88
+ # Assume it's raw base64
89
+ return f"base64:{value}", None
90
+
91
+ if isinstance(value, list):
92
+ expected_count = width * height
93
+ if len(value) != expected_count:
94
+ return None, f"pixels array must have {expected_count} entries for {width}x{height} texture, got {len(value)}"
95
+
96
+ normalized = []
97
+ for i, pixel in enumerate(value):
98
+ parsed, error = _normalize_color_int(pixel)
99
+ if error:
100
+ return None, f"pixels[{i}]: {error}"
101
+ normalized.append(parsed)
102
+ return normalized, None
103
+
104
+ return None, f"pixels must be a list or base64 string, got {type(value).__name__}"
105
+
106
+
107
+ def _normalize_sprite_settings(value: Any) -> tuple[dict | None, str | None]:
108
+ """
109
+ Normalize sprite settings.
110
+ Returns (settings, error_message).
111
+ """
112
+ if value is None:
113
+ return None, None
114
+
115
+ if isinstance(value, str):
116
+ value = parse_json_payload(value)
117
+
118
+ if isinstance(value, dict):
119
+ result = {}
120
+ if "pivot" in value:
121
+ pivot = value["pivot"]
122
+ if isinstance(pivot, (list, tuple)) and len(pivot) == 2:
123
+ result["pivot"] = [float(pivot[0]), float(pivot[1])]
124
+ else:
125
+ return None, f"sprite pivot must be [x, y], got {pivot}"
126
+ if "pixels_per_unit" in value:
127
+ result["pixelsPerUnit"] = float(value["pixels_per_unit"])
128
+ elif "pixelsPerUnit" in value:
129
+ result["pixelsPerUnit"] = float(value["pixelsPerUnit"])
130
+ return result, None
131
+
132
+ if isinstance(value, bool) and value:
133
+ # Just enable sprite mode with defaults
134
+ return {"pivot": [0.5, 0.5], "pixelsPerUnit": 100}, None
135
+
136
+ return None, f"as_sprite must be a dict or boolean, got {type(value).__name__}"
137
+
138
+
139
+ # Valid values for import settings enums
140
+ _TEXTURE_TYPES = {
141
+ "default": "Default",
142
+ "normal_map": "NormalMap",
143
+ "editor_gui": "GUI",
144
+ "sprite": "Sprite",
145
+ "cursor": "Cursor",
146
+ "cookie": "Cookie",
147
+ "lightmap": "Lightmap",
148
+ "directional_lightmap": "DirectionalLightmap",
149
+ "shadow_mask": "Shadowmask",
150
+ "single_channel": "SingleChannel",
151
+ }
152
+
153
+ _TEXTURE_SHAPES = {"2d": "Texture2D", "cube": "TextureCube"}
154
+
155
+ _ALPHA_SOURCES = {
156
+ "none": "None",
157
+ "from_input": "FromInput",
158
+ "from_gray_scale": "FromGrayScale",
159
+ }
160
+
161
+ _WRAP_MODES = {
162
+ "repeat": "Repeat",
163
+ "clamp": "Clamp",
164
+ "mirror": "Mirror",
165
+ "mirror_once": "MirrorOnce",
166
+ }
167
+
168
+ _FILTER_MODES = {"point": "Point", "bilinear": "Bilinear", "trilinear": "Trilinear"}
169
+
170
+ _COMPRESSIONS = {
171
+ "none": "Uncompressed",
172
+ "low_quality": "CompressedLQ",
173
+ "normal_quality": "Compressed",
174
+ "high_quality": "CompressedHQ",
175
+ }
176
+
177
+ _SPRITE_MODES = {"single": "Single", "multiple": "Multiple", "polygon": "Polygon"}
178
+
179
+ _SPRITE_MESH_TYPES = {"full_rect": "FullRect", "tight": "Tight"}
180
+
181
+ _MIPMAP_FILTERS = {"box": "BoxFilter", "kaiser": "KaiserFilter"}
182
+
183
+
184
+ def _normalize_bool_setting(value: Any, name: str) -> tuple[bool | None, str | None]:
185
+ """
186
+ Normalize boolean settings.
187
+ Returns (bool_value, error_message).
188
+ """
189
+ if value is None:
190
+ return None, None
191
+
192
+ if isinstance(value, bool):
193
+ return value, None
194
+
195
+ if isinstance(value, (int, float)):
196
+ if value in (0, 1, 0.0, 1.0):
197
+ return bool(value), None
198
+ return None, f"{name} must be a boolean"
199
+
200
+ if isinstance(value, str):
201
+ coerced = coerce_bool(value, default=None)
202
+ if coerced is None:
203
+ return None, f"{name} must be a boolean"
204
+ return coerced, None
205
+
206
+ return None, f"{name} must be a boolean"
207
+
208
+
209
+ def _normalize_import_settings(value: Any) -> tuple[dict | None, str | None]:
210
+ """
211
+ Normalize TextureImporter settings.
212
+ Converts snake_case keys to camelCase and validates enum values.
213
+ Returns (settings, error_message).
214
+ """
215
+ if value is None:
216
+ return None, None
217
+
218
+ if isinstance(value, str):
219
+ value = parse_json_payload(value)
220
+
221
+ if not isinstance(value, dict):
222
+ return None, f"import_settings must be a dict, got {type(value).__name__}"
223
+
224
+ result = {}
225
+
226
+ # Texture type
227
+ if "texture_type" in value:
228
+ tt = value["texture_type"].lower() if isinstance(value["texture_type"], str) else value["texture_type"]
229
+ if tt not in _TEXTURE_TYPES:
230
+ return None, f"Invalid texture_type '{tt}'. Valid: {list(_TEXTURE_TYPES.keys())}"
231
+ result["textureType"] = _TEXTURE_TYPES[tt]
232
+
233
+ # Texture shape
234
+ if "texture_shape" in value:
235
+ ts = value["texture_shape"].lower() if isinstance(value["texture_shape"], str) else value["texture_shape"]
236
+ if ts not in _TEXTURE_SHAPES:
237
+ return None, f"Invalid texture_shape '{ts}'. Valid: {list(_TEXTURE_SHAPES.keys())}"
238
+ result["textureShape"] = _TEXTURE_SHAPES[ts]
239
+
240
+ # Boolean settings
241
+ for snake, camel in [
242
+ ("srgb", "sRGBTexture"),
243
+ ("alpha_is_transparency", "alphaIsTransparency"),
244
+ ("readable", "isReadable"),
245
+ ("generate_mipmaps", "mipmapEnabled"),
246
+ ("compression_crunched", "crunchedCompression"),
247
+ ]:
248
+ if snake in value:
249
+ bool_value, bool_error = _normalize_bool_setting(value[snake], snake)
250
+ if bool_error:
251
+ return None, bool_error
252
+ if bool_value is not None:
253
+ result[camel] = bool_value
254
+
255
+ # Alpha source
256
+ if "alpha_source" in value:
257
+ alpha = value["alpha_source"].lower() if isinstance(value["alpha_source"], str) else value["alpha_source"]
258
+ if alpha not in _ALPHA_SOURCES:
259
+ return None, f"Invalid alpha_source '{alpha}'. Valid: {list(_ALPHA_SOURCES.keys())}"
260
+ result["alphaSource"] = _ALPHA_SOURCES[alpha]
261
+
262
+ # Wrap modes
263
+ for snake, camel in [("wrap_mode", "wrapMode"), ("wrap_mode_u", "wrapModeU"), ("wrap_mode_v", "wrapModeV")]:
264
+ if snake in value:
265
+ wm = value[snake].lower() if isinstance(value[snake], str) else value[snake]
266
+ if wm not in _WRAP_MODES:
267
+ return None, f"Invalid {snake} '{wm}'. Valid: {list(_WRAP_MODES.keys())}"
268
+ result[camel] = _WRAP_MODES[wm]
269
+
270
+ # Filter mode
271
+ if "filter_mode" in value:
272
+ fm = value["filter_mode"].lower() if isinstance(value["filter_mode"], str) else value["filter_mode"]
273
+ if fm not in _FILTER_MODES:
274
+ return None, f"Invalid filter_mode '{fm}'. Valid: {list(_FILTER_MODES.keys())}"
275
+ result["filterMode"] = _FILTER_MODES[fm]
276
+
277
+ # Mipmap filter
278
+ if "mipmap_filter" in value:
279
+ mf = value["mipmap_filter"].lower() if isinstance(value["mipmap_filter"], str) else value["mipmap_filter"]
280
+ if mf not in _MIPMAP_FILTERS:
281
+ return None, f"Invalid mipmap_filter '{mf}'. Valid: {list(_MIPMAP_FILTERS.keys())}"
282
+ result["mipmapFilter"] = _MIPMAP_FILTERS[mf]
283
+
284
+ # Compression
285
+ if "compression" in value:
286
+ comp = value["compression"].lower() if isinstance(value["compression"], str) else value["compression"]
287
+ if comp not in _COMPRESSIONS:
288
+ return None, f"Invalid compression '{comp}'. Valid: {list(_COMPRESSIONS.keys())}"
289
+ result["textureCompression"] = _COMPRESSIONS[comp]
290
+
291
+ # Integer settings
292
+ if "aniso_level" in value:
293
+ raw = value["aniso_level"]
294
+ level = coerce_int(raw)
295
+ if level is None:
296
+ if raw is not None:
297
+ return None, f"aniso_level must be an integer, got {raw}"
298
+ else:
299
+ if not 0 <= level <= 16:
300
+ return None, f"aniso_level must be 0-16, got {level}"
301
+ result["anisoLevel"] = level
302
+
303
+ if "max_texture_size" in value:
304
+ raw = value["max_texture_size"]
305
+ size = coerce_int(raw)
306
+ if size is None:
307
+ if raw is not None:
308
+ return None, f"max_texture_size must be an integer, got {raw}"
309
+ else:
310
+ valid_sizes = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]
311
+ if size not in valid_sizes:
312
+ return None, f"max_texture_size must be one of {valid_sizes}, got {size}"
313
+ result["maxTextureSize"] = size
314
+
315
+ if "compression_quality" in value:
316
+ raw = value["compression_quality"]
317
+ quality = coerce_int(raw)
318
+ if quality is None:
319
+ if raw is not None:
320
+ return None, f"compression_quality must be an integer, got {raw}"
321
+ else:
322
+ if not 0 <= quality <= 100:
323
+ return None, f"compression_quality must be 0-100, got {quality}"
324
+ result["compressionQuality"] = quality
325
+
326
+ # Sprite-specific settings
327
+ if "sprite_mode" in value:
328
+ sm = value["sprite_mode"].lower() if isinstance(value["sprite_mode"], str) else value["sprite_mode"]
329
+ if sm not in _SPRITE_MODES:
330
+ return None, f"Invalid sprite_mode '{sm}'. Valid: {list(_SPRITE_MODES.keys())}"
331
+ result["spriteImportMode"] = _SPRITE_MODES[sm]
332
+
333
+ if "sprite_pixels_per_unit" in value:
334
+ raw = value["sprite_pixels_per_unit"]
335
+ try:
336
+ result["spritePixelsPerUnit"] = float(raw)
337
+ except (TypeError, ValueError):
338
+ return None, f"sprite_pixels_per_unit must be a number, got {raw}"
339
+
340
+ if "sprite_pivot" in value:
341
+ pivot = value["sprite_pivot"]
342
+ if isinstance(pivot, (list, tuple)) and len(pivot) == 2:
343
+ result["spritePivot"] = [float(pivot[0]), float(pivot[1])]
344
+ else:
345
+ return None, f"sprite_pivot must be [x, y], got {pivot}"
346
+
347
+ if "sprite_mesh_type" in value:
348
+ mt = value["sprite_mesh_type"].lower() if isinstance(value["sprite_mesh_type"], str) else value["sprite_mesh_type"]
349
+ if mt not in _SPRITE_MESH_TYPES:
350
+ return None, f"Invalid sprite_mesh_type '{mt}'. Valid: {list(_SPRITE_MESH_TYPES.keys())}"
351
+ result["spriteMeshType"] = _SPRITE_MESH_TYPES[mt]
352
+
353
+ if "sprite_extrude" in value:
354
+ raw = value["sprite_extrude"]
355
+ extrude = coerce_int(raw)
356
+ if extrude is None:
357
+ if raw is not None:
358
+ return None, f"sprite_extrude must be an integer, got {raw}"
359
+ else:
360
+ if not 0 <= extrude <= 32:
361
+ return None, f"sprite_extrude must be 0-32, got {extrude}"
362
+ result["spriteExtrude"] = extrude
363
+
364
+ return result, None
365
+
366
+
367
+ @mcp_for_unity_tool(
368
+ description=(
369
+ "Procedural texture generation for Unity. Creates textures with solid fills, "
370
+ "patterns (checkerboard, stripes, dots, grid, brick), gradients, and noise. "
371
+ "Actions: create, modify, delete, create_sprite, apply_pattern, apply_gradient, apply_noise"
372
+ ),
373
+ annotations=ToolAnnotations(
374
+ title="Manage Texture",
375
+ destructiveHint=True,
376
+ ),
377
+ )
378
+ async def manage_texture(
379
+ ctx: Context,
380
+ action: Annotated[Literal[
381
+ "create",
382
+ "modify",
383
+ "delete",
384
+ "create_sprite",
385
+ "apply_pattern",
386
+ "apply_gradient",
387
+ "apply_noise"
388
+ ], "Action to perform."],
389
+
390
+ # Required for most actions
391
+ path: Annotated[str,
392
+ "Output texture path (e.g., 'Assets/Textures/MyTexture.png')"] | None = None,
393
+
394
+ # Dimensions (defaults to 64x64)
395
+ width: Annotated[int, "Texture width in pixels (default: 64)"] | None = None,
396
+ height: Annotated[int, "Texture height in pixels (default: 64)"] | None = None,
397
+
398
+ # Solid fill (accepts both 0-255 integers and 0.0-1.0 normalized floats)
399
+ fill_color: Annotated[list[int | float] | dict[str, int | float] | str,
400
+ "Fill color as [r, g, b] or [r, g, b, a] array, {r, g, b, a} object, or hex string. Accepts both 0-255 range (e.g., [255, 0, 0]) or 0.0-1.0 normalized range (e.g., [1.0, 0, 0])"] | None = None,
401
+
402
+ # Pattern-based generation
403
+ pattern: Annotated[Literal[
404
+ "checkerboard", "stripes", "stripes_h", "stripes_v", "stripes_diag",
405
+ "dots", "grid", "brick"
406
+ ], "Pattern type for apply_pattern action"] | None = None,
407
+
408
+ palette: Annotated[list[list[int | float]],
409
+ "Color palette as [[r,g,b,a], ...]. Accepts both 0-255 range or 0.0-1.0 normalized range"] | None = None,
410
+
411
+ pattern_size: Annotated[int,
412
+ "Pattern cell size in pixels (default: 8)"] | None = None,
413
+
414
+ # Direct pixel data
415
+ pixels: Annotated[list[list[int]] | str,
416
+ "Pixel data as JSON array of [r,g,b,a] values or base64 string"] | None = None,
417
+
418
+ image_path: Annotated[str,
419
+ "Source image file path for create/create_sprite (PNG/JPG)."] | None = None,
420
+
421
+ # Gradient settings
422
+ gradient_type: Annotated[Literal["linear", "radial"],
423
+ "Gradient type (default: linear)"] | None = None,
424
+ gradient_angle: Annotated[float,
425
+ "Gradient angle in degrees for linear gradient (default: 0)"] | None = None,
426
+
427
+ # Noise settings
428
+ noise_scale: Annotated[float,
429
+ "Noise scale/frequency (default: 0.1)"] | None = None,
430
+ octaves: Annotated[int,
431
+ "Number of noise octaves for detail (default: 1)"] | None = None,
432
+
433
+ # Modify action
434
+ set_pixels: Annotated[dict,
435
+ "Region to modify: {x, y, width, height, color or pixels}"] | None = None,
436
+
437
+ # Sprite creation (legacy, prefer import_settings)
438
+ as_sprite: Annotated[dict | bool,
439
+ "Configure as sprite: {pivot: [x,y], pixels_per_unit: 100} or true for defaults"] | None = None,
440
+
441
+ # TextureImporter settings
442
+ import_settings: Annotated[dict,
443
+ "TextureImporter settings dict. Keys: texture_type (default/normal_map/sprite/etc), "
444
+ "texture_shape (2d/cube), srgb (bool), alpha_source (none/from_input/from_gray_scale), "
445
+ "alpha_is_transparency (bool), readable (bool), generate_mipmaps (bool), "
446
+ "wrap_mode/wrap_mode_u/wrap_mode_v (repeat/clamp/mirror/mirror_once), "
447
+ "filter_mode (point/bilinear/trilinear), aniso_level (0-16), max_texture_size (32-16384), "
448
+ "compression (none/low_quality/normal_quality/high_quality), compression_quality (0-100), "
449
+ "sprite_mode (single/multiple/polygon), sprite_pixels_per_unit, sprite_pivot, "
450
+ "sprite_mesh_type (full_rect/tight), sprite_extrude (0-32)"] | None = None,
451
+
452
+ ) -> dict[str, Any]:
453
+ unity_instance = get_unity_instance_from_context(ctx)
454
+
455
+ # Preflight check
456
+ gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
457
+ if gate is not None:
458
+ return gate.model_dump()
459
+
460
+ # --- Normalize parameters ---
461
+ fill_color, fill_error = _normalize_color_int(fill_color)
462
+ if fill_error:
463
+ return {"success": False, "message": fill_error}
464
+
465
+ action_lower = action.lower()
466
+
467
+ if image_path is not None and action_lower not in ("create", "create_sprite"):
468
+ return {"success": False, "message": "image_path is only supported for create/create_sprite."}
469
+
470
+ if image_path is not None and (fill_color is not None or pattern is not None or pixels is not None):
471
+ return {"success": False, "message": "image_path cannot be combined with fill_color, pattern, or pixels."}
472
+
473
+ # Default to white for create action if nothing else specified
474
+ if action == "create" and fill_color is None and pattern is None and pixels is None and image_path is None:
475
+ fill_color = [255, 255, 255, 255]
476
+
477
+ palette, palette_error = _normalize_palette(palette)
478
+ if palette_error:
479
+ return {"success": False, "message": palette_error}
480
+
481
+ if image_path is None:
482
+ # Normalize dimensions
483
+ width, width_error = _normalize_dimension(width, "width")
484
+ if width_error:
485
+ return {"success": False, "message": width_error}
486
+ height, height_error = _normalize_dimension(height, "height")
487
+ if height_error:
488
+ return {"success": False, "message": height_error}
489
+ pattern_size, pattern_error = _normalize_positive_int(pattern_size, "pattern_size")
490
+ if pattern_error:
491
+ return {"success": False, "message": pattern_error}
492
+
493
+ octaves, octaves_error = _normalize_positive_int(octaves, "octaves")
494
+ if octaves_error:
495
+ return {"success": False, "message": octaves_error}
496
+ else:
497
+ width = None
498
+ height = None
499
+
500
+ # Normalize pixels if provided
501
+ pixels_normalized = None
502
+ if pixels is not None:
503
+ pixels_normalized, pixels_error = _normalize_pixels(pixels, width, height)
504
+ if pixels_error:
505
+ return {"success": False, "message": pixels_error}
506
+
507
+ # Normalize sprite settings
508
+ sprite_settings, sprite_error = _normalize_sprite_settings(as_sprite)
509
+ if sprite_error:
510
+ return {"success": False, "message": sprite_error}
511
+
512
+ # Normalize import settings
513
+ import_settings_normalized, import_error = _normalize_import_settings(import_settings)
514
+ if import_error:
515
+ return {"success": False, "message": import_error}
516
+
517
+ # Normalize set_pixels for modify action
518
+ set_pixels_normalized = None
519
+ if set_pixels is not None:
520
+ if isinstance(set_pixels, str):
521
+ parsed = parse_json_payload(set_pixels)
522
+ if not isinstance(parsed, dict):
523
+ return {"success": False, "message": "set_pixels must be a JSON object"}
524
+ set_pixels = parsed
525
+ if not isinstance(set_pixels, dict):
526
+ return {"success": False, "message": "set_pixels must be a JSON object"}
527
+
528
+ set_pixels_normalized = set_pixels.copy()
529
+ if "color" in set_pixels_normalized:
530
+ color, error = _normalize_color_int(set_pixels_normalized["color"])
531
+ if error:
532
+ return {"success": False, "message": f"set_pixels.color: {error}"}
533
+ set_pixels_normalized["color"] = color
534
+ if "pixels" in set_pixels_normalized:
535
+ region_width = coerce_int(set_pixels_normalized.get("width"))
536
+ region_height = coerce_int(set_pixels_normalized.get("height"))
537
+ if region_width is None or region_height is None or region_width <= 0 or region_height <= 0:
538
+ return {"success": False, "message": "set_pixels width and height must be positive integers"}
539
+ pixels_normalized, pixels_error = _normalize_pixels(
540
+ set_pixels_normalized["pixels"], region_width, region_height
541
+ )
542
+ if pixels_error:
543
+ return {"success": False, "message": f"set_pixels.pixels: {pixels_error}"}
544
+ set_pixels_normalized["pixels"] = pixels_normalized
545
+
546
+ # --- Build params for Unity ---
547
+ params_dict = {
548
+ "action": action.lower(),
549
+ "path": path,
550
+ "width": width,
551
+ "height": height,
552
+ "fillColor": fill_color,
553
+ "pattern": pattern,
554
+ "palette": palette,
555
+ "patternSize": pattern_size,
556
+ "pixels": pixels_normalized,
557
+ "imagePath": image_path,
558
+ "gradientType": gradient_type,
559
+ "gradientAngle": gradient_angle,
560
+ "noiseScale": noise_scale,
561
+ "octaves": octaves,
562
+ "setPixels": set_pixels_normalized,
563
+ "spriteSettings": sprite_settings,
564
+ "importSettings": import_settings_normalized,
565
+ }
566
+
567
+ # Remove None values
568
+ params_dict = {k: v for k, v in params_dict.items() if v is not None}
569
+
570
+ # Send to Unity
571
+ result = await send_with_unity_instance(
572
+ async_send_command_with_retry,
573
+ unity_instance,
574
+ "manage_texture",
575
+ params_dict,
576
+ )
577
+
578
+ if isinstance(result, dict):
579
+ result["_debug_params"] = params_dict
580
+
581
+ return result if isinstance(result, dict) else {"success": False, "message": str(result)}