amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__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 (181) hide show
  1. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5621
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.14.3.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -729
  180. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,506 +1,506 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
-
4
- from typing import Dict
5
-
6
- from gaia.mcp.blender_mcp_client import MCPClient
7
-
8
-
9
- class MaterialManager:
10
- """Manages Blender material operations."""
11
-
12
- def __init__(self, mcp: MCPClient):
13
- self.mcp = mcp
14
-
15
- def create_ground_material(
16
- self, ground_texture_name: str, maps_texture_name: str
17
- ) -> Dict:
18
- """Create the ground material with separate land and water shaders, using displacement."""
19
-
20
- def generate_code():
21
- return f"""
22
- import bpy
23
-
24
- # Get the Earth object
25
- earth = bpy.data.objects.get("Earth")
26
- if not earth:
27
- print("Error: Earth object not found")
28
- exit()
29
-
30
- # Create new material
31
- ground_mat = bpy.data.materials.new(name="ground")
32
- ground_mat.use_nodes = True
33
- earth.data.materials.append(ground_mat)
34
-
35
- # Get material nodes and links
36
- nodes = ground_mat.node_tree.nodes
37
- links = ground_mat.node_tree.links
38
-
39
- # Clear default nodes
40
- for node in nodes:
41
- nodes.remove(node)
42
-
43
- # Create nodes for ground material
44
- output = nodes.new(type='ShaderNodeOutputMaterial')
45
- output.location = (800, 0)
46
-
47
- # Add texture image for Earth ground
48
- tex_ground = nodes.new(type='ShaderNodeTexImage')
49
- tex_ground.location = (-600, 200)
50
- tex_ground.image = bpy.data.images.get("{ground_texture_name}")
51
- tex_ground.projection = 'SPHERE'
52
- tex_ground.interpolation = 'Linear' # Updated to Linear as shown in screenshot
53
-
54
- # Add texture coordinate
55
- tex_coord = nodes.new(type='ShaderNodeTexCoord')
56
- tex_coord.location = (-900, 0)
57
-
58
- # Earth maps (water mask and displacement)
59
- tex_maps = nodes.new(type='ShaderNodeTexImage')
60
- tex_maps.location = (-600, -200)
61
- tex_maps.image = bpy.data.images.get("{maps_texture_name}")
62
- tex_maps.projection = 'SPHERE'
63
- tex_maps.interpolation = 'Linear' # Already set to Linear
64
-
65
- # Separate RGB for maps
66
- separate_rgb = nodes.new(type='ShaderNodeSeparateRGB')
67
- separate_rgb.location = (-300, -200)
68
-
69
- # Single Principled BSDF with values matching screenshot
70
- principled = nodes.new(type='ShaderNodeBsdfPrincipled')
71
- principled.location = (400, 200)
72
- principled.inputs['Metallic'].default_value = 0.030 # As shown in screenshot
73
- principled.inputs['Roughness'].default_value = 0.500 # As shown in screenshot
74
- principled.inputs['IOR'].default_value = 1.500 # As shown in screenshot
75
- principled.inputs['Alpha'].default_value = 1.000 # As shown in screenshot
76
-
77
- # Alternative approach - keep both land and water shaders as before
78
- # Land material
79
- land_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
80
- land_shader.location = (100, 200)
81
- land_shader.inputs['Specular'].default_value = 0.0
82
- land_shader.inputs['Roughness'].default_value = 1.0
83
-
84
- # Water material
85
- water_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
86
- water_shader.location = (100, -100)
87
- water_shader.inputs['Roughness'].default_value = 0.4
88
- water_shader.inputs['IOR'].default_value = 1.333
89
-
90
- # Mix shader
91
- mix_shader = nodes.new(type='ShaderNodeMixShader')
92
- mix_shader.location = (500, 0)
93
-
94
- # Displacement node
95
- displace = nodes.new(type='ShaderNodeDisplacement')
96
- displace.location = (500, -300)
97
- displace.inputs['Scale'].default_value = 0.005 # Match tutorial value for displacement
98
-
99
- # Connect nodes
100
- # Option 1: Using single Principled BSDF (as shown in screenshot)
101
- links.new(tex_coord.outputs['Generated'], tex_ground.inputs['Vector'])
102
- links.new(tex_coord.outputs['Generated'], tex_maps.inputs['Vector'])
103
- links.new(tex_ground.outputs['Color'], principled.inputs['Base Color'])
104
- links.new(tex_maps.outputs['Color'], separate_rgb.inputs['Image'])
105
- links.new(separate_rgb.outputs['R'], displace.inputs['Height']) # R (red) channel is height
106
- links.new(principled.outputs['BSDF'], output.inputs['Surface'])
107
- links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
108
-
109
- # Set material displacement method to match tutorial
110
- ground_mat.displacement_method = 'DISPLACEMENT'
111
-
112
- print("Ground material created exactly as shown in screenshot")
113
- """
114
-
115
- return self.mcp.execute_code(generate_code())
116
-
117
- def create_atmosphere_material(self) -> Dict:
118
- """Create the atmosphere material with volume scatter."""
119
-
120
- def generate_code():
121
- return """
122
- import bpy
123
-
124
- # Create atmosphere material
125
- atm_mat = bpy.data.materials.new(name="atmosphere")
126
- atm_mat.use_nodes = True
127
-
128
- # Get material nodes
129
- nodes = atm_mat.node_tree.nodes
130
- links = atm_mat.node_tree.links
131
-
132
- # Clear default nodes
133
- for node in nodes:
134
- nodes.remove(node)
135
-
136
- # Create nodes for atmosphere material as shown in tutorial
137
- output = nodes.new(type='ShaderNodeOutputMaterial')
138
- output.location = (800, 0)
139
-
140
- # Add texture coordinate for atmosphere
141
- tex_coord = nodes.new(type='ShaderNodeTexCoord')
142
- tex_coord.location = (-900, 0)
143
-
144
- # Add volume scatter - match tutorial color exactly
145
- volume_scatter = nodes.new(type='ShaderNodeVolumeScatter')
146
- volume_scatter.location = (500, 200)
147
- volume_scatter.inputs['Color'].default_value = (0.3, 0.6, 1.0, 1.0) # Blue color from tutorial
148
-
149
- # Value for atmosphere thickness - 1% of planet radius as in tutorial
150
- thickness = nodes.new(type='ShaderNodeValue')
151
- thickness.location = (-600, -300)
152
- thickness.outputs[0].default_value = 0.01 # 1% of planet radius as specified in tutorial
153
-
154
- # Vector length for atmosphere density gradient
155
- vec_math = nodes.new(type='ShaderNodeVectorMath')
156
- vec_math.location = (-600, 0)
157
- vec_math.operation = 'LENGTH'
158
-
159
- # Subtract 1 to get 0 at surface level
160
- math_sub = nodes.new(type='ShaderNodeMath')
161
- math_sub.location = (-400, 0)
162
- math_sub.operation = 'SUBTRACT'
163
- math_sub.inputs[1].default_value = 1.0
164
-
165
- # Divide by thickness to normalize distance - exactly as in tutorial
166
- math_div = nodes.new(type='ShaderNodeMath')
167
- math_div.location = (-200, 0)
168
- math_div.operation = 'DIVIDE'
169
- math_div.use_clamp = True
170
-
171
- # Multiply by 15 for density adjustment - exact value from tutorial
172
- math_mul1 = nodes.new(type='ShaderNodeMath')
173
- math_mul1.location = (0, 0)
174
- math_mul1.operation = 'MULTIPLY'
175
- math_mul1.inputs[1].default_value = 15.0 # Tutorial uses exactly 15
176
-
177
- # Power for exponential falloff - uses e (Euler's number) in tutorial
178
- math_pow = nodes.new(type='ShaderNodeMath')
179
- math_pow.location = (200, 0)
180
- math_pow.operation = 'POWER'
181
- math_pow.inputs[1].default_value = 1.0 # Power of e (Euler's number)
182
-
183
- # Multiply by 0.05 for final density - exact value from tutorial
184
- math_mul2 = nodes.new(type='ShaderNodeMath')
185
- math_mul2.location = (400, 0)
186
- math_mul2.operation = 'MULTIPLY'
187
- math_mul2.inputs[1].default_value = 0.05 # Tutorial uses exactly 0.05
188
-
189
- # Displacement for atmosphere
190
- displace = nodes.new(type='ShaderNodeDisplacement')
191
- displace.location = (500, -200)
192
- displace.inputs['Scale'].default_value = 1.0
193
-
194
- # Connect nodes exactly as demonstrated in tutorial
195
- links.new(tex_coord.outputs['Object'], vec_math.inputs[0])
196
- links.new(vec_math.outputs[0], math_sub.inputs[0])
197
- links.new(math_sub.outputs[0], math_div.inputs[0])
198
- links.new(thickness.outputs[0], math_div.inputs[1])
199
- links.new(math_div.outputs[0], math_mul1.inputs[0])
200
- links.new(math_mul1.outputs[0], math_pow.inputs[0])
201
- links.new(math_pow.outputs[0], math_mul2.inputs[0])
202
- links.new(math_mul2.outputs[0], volume_scatter.inputs['Density'])
203
- links.new(thickness.outputs[0], displace.inputs['Scale'])
204
- links.new(volume_scatter.outputs['Volume'], output.inputs['Volume'])
205
- links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
206
-
207
- # Set material displacement method as in tutorial
208
- atm_mat.displacement_method = 'DISPLACEMENT'
209
-
210
- print("Atmosphere material created exactly as in tutorial")
211
- """
212
-
213
- return self.mcp.execute_code(generate_code())
214
-
215
- def create_clouds_material(self, clouds_texture_name: str) -> Dict:
216
- """Create the clouds material with subsurface scattering."""
217
-
218
- def generate_code():
219
- return f"""
220
- import bpy
221
-
222
- # Create clouds material
223
- clouds_mat = bpy.data.materials.new(name="clouds")
224
- clouds_mat.use_nodes = True
225
-
226
- # Get material nodes
227
- nodes = clouds_mat.node_tree.nodes
228
- links = clouds_mat.node_tree.links
229
-
230
- # Clear default nodes
231
- for node in nodes:
232
- nodes.remove(node)
233
-
234
- # Create nodes for clouds material - match tutorial exactly
235
- output = nodes.new(type='ShaderNodeOutputMaterial')
236
- output.location = (1000, 0)
237
-
238
- # Add texture coordinate for clouds
239
- tex_coord = nodes.new(type='ShaderNodeTexCoord')
240
- tex_coord.location = (-900, 0)
241
-
242
- # Add cloud texture
243
- tex_clouds = nodes.new(type='ShaderNodeTexImage')
244
- tex_clouds.location = (-600, 0)
245
- tex_clouds.image = bpy.data.images.get("{clouds_texture_name}")
246
- tex_clouds.projection = 'SPHERE'
247
- tex_clouds.interpolation = 'Linear' # Match tutorial setting
248
-
249
- # Gamma correction for cloud texture - exactly 0.5 as in tutorial
250
- gamma = nodes.new(type='ShaderNodeGamma')
251
- gamma.location = (-400, 0)
252
- gamma.inputs['Gamma'].default_value = 0.5 # Exactly as in tutorial
253
-
254
- # Second gamma correction - exactly 0.9 as in tutorial
255
- gamma2 = nodes.new(type='ShaderNodeGamma')
256
- gamma2.location = (-200, 0)
257
- gamma2.inputs['Gamma'].default_value = 0.9 # Exactly as in tutorial
258
-
259
- # Add Transparent BSDF
260
- transparent = nodes.new(type='ShaderNodeBsdfTransparent')
261
- transparent.location = (400, 100)
262
-
263
- # Add Subsurface Scattering BSDF - exactly as in tutorial
264
- subsurface = nodes.new(type='ShaderNodeSubsurfaceScattering')
265
- subsurface.location = (400, -100)
266
- subsurface.inputs['Radius'].default_value = (1, 1, 1) # Tutorial setting
267
-
268
- # Multiply for cloud intensity - exactly 5.0 as in tutorial
269
- math_mul = nodes.new(type='ShaderNodeMath')
270
- math_mul.location = (0, -200)
271
- math_mul.operation = 'MULTIPLY'
272
- math_mul.inputs[1].default_value = 5.0 # Exactly as in tutorial
273
-
274
- # Power for cloud exponential control - as in tutorial
275
- math_pow = nodes.new(type='ShaderNodeMath')
276
- math_pow.location = (200, -200)
277
- math_pow.operation = 'POWER'
278
- math_pow.inputs[1].default_value = 1.0 # Tutorial setting
279
-
280
- # Mix shader
281
- mix_shader = nodes.new(type='ShaderNodeMixShader')
282
- mix_shader.location = (700, 0)
283
-
284
- # Displacement node - exactly 0.005 as in tutorial
285
- displace = nodes.new(type='ShaderNodeDisplacement')
286
- displace.location = (700, -300)
287
- displace.inputs['Scale'].default_value = 0.005 # Exactly as in tutorial
288
-
289
- # Connect nodes exactly as in tutorial
290
- links.new(tex_coord.outputs['Generated'], tex_clouds.inputs['Vector'])
291
- links.new(tex_clouds.outputs['Color'], gamma.inputs['Color'])
292
- links.new(gamma.outputs['Color'], gamma2.inputs['Color'])
293
- links.new(gamma2.outputs['Color'], subsurface.inputs['Color'])
294
- links.new(gamma2.outputs['Color'], math_mul.inputs[0])
295
- links.new(math_mul.outputs[0], math_pow.inputs[0])
296
- links.new(math_pow.outputs[0], mix_shader.inputs['Fac'])
297
- links.new(transparent.outputs['BSDF'], mix_shader.inputs[1])
298
- links.new(subsurface.outputs['BSSRDF'], mix_shader.inputs[2])
299
- links.new(tex_clouds.outputs['Color'], displace.inputs['Height'])
300
- links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
301
- links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
302
-
303
- # Set material displacement method as in tutorial
304
- clouds_mat.displacement_method = 'BUMP' # Tutorial setting
305
-
306
- print("Clouds material created exactly as in tutorial")
307
- """
308
-
309
- return self.mcp.execute_code(generate_code())
310
-
311
- def set_material_color(self, object_name: str, color: tuple = (1, 0, 0, 1)) -> Dict:
312
- """
313
- Set the material color for an object. Creates a new material if one doesn't exist.
314
-
315
- Args:
316
- object_name: Name of the object to modify
317
- color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1
318
-
319
- Returns:
320
- Dictionary with the operation result
321
- """
322
-
323
- def generate_code():
324
- return f"""
325
- import bpy
326
-
327
- result = {{"status": "processing"}}
328
-
329
- # Get the object
330
- obj = bpy.data.objects.get("{object_name}")
331
- if not obj:
332
- result = {{"status": "error", "error": f"Object '{object_name}' not found"}}
333
- else:
334
- # Create a new material if needed
335
- mat_name = "{object_name}_material"
336
- if mat_name in bpy.data.materials:
337
- mat = bpy.data.materials[mat_name]
338
- else:
339
- mat = bpy.data.materials.new(name=mat_name)
340
-
341
- # Enable nodes for the material
342
- mat.use_nodes = True
343
- nodes = mat.node_tree.nodes
344
-
345
- # Clear existing nodes
346
- for node in nodes:
347
- nodes.remove(node)
348
-
349
- # Create new nodes
350
- output = nodes.new(type='ShaderNodeOutputMaterial')
351
- output.location = (300, 0)
352
-
353
- bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
354
- bsdf.location = (0, 0)
355
-
356
- # Set color - use correct format for Blender 4.0
357
- bsdf.inputs["Base Color"].default_value = {color}
358
-
359
- # Also set viewport display color for material
360
- mat.diffuse_color = {color}
361
-
362
- # Connect nodes
363
- mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
364
-
365
- # Assign material to object
366
- if len(obj.data.materials) == 0:
367
- obj.data.materials.append(mat)
368
- else:
369
- obj.data.materials[0] = mat
370
-
371
- # Set the active material slot
372
- obj.active_material_index = 0
373
- obj.active_material = mat
374
-
375
- # Set the render engine to show materials properly
376
- # For Blender 4.0, use valid enum values
377
- if hasattr(bpy.context.scene, 'render'):
378
- if hasattr(bpy.context.scene.render, 'engine'):
379
- current_engine = bpy.context.scene.render.engine
380
- if current_engine not in ['CYCLES', 'BLENDER_EEVEE_NEXT', 'BLENDER_WORKBENCH']:
381
- try:
382
- bpy.context.scene.render.engine = 'CYCLES'
383
- except Exception as e:
384
- print(f"Could not set render engine: {{e}}")
385
-
386
- # Force update of all 3D viewports
387
- for window in bpy.context.window_manager.windows:
388
- for area in window.screen.areas:
389
- if area.type == 'VIEW_3D':
390
- area.tag_redraw()
391
-
392
- result = {{
393
- "status": "success",
394
- "message": "Material color set successfully",
395
- "debug_info": {{
396
- "object_name": obj.name,
397
- "material_name": mat.name,
398
- "color_set": list({color}),
399
- "material_slots": len(obj.material_slots),
400
- "active_material": obj.active_material.name if obj.active_material else None,
401
- "render_engine": bpy.context.scene.render.engine if hasattr(bpy.context.scene, 'render') else "unknown"
402
- }}
403
- }}
404
-
405
- result
406
- """
407
-
408
- return self.mcp.execute_code(generate_code())
409
-
410
-
411
- def generate_material_assignment_code(
412
- object_name: str, material_name: str = None, color: tuple = (0.8, 0.8, 0.8, 1.0)
413
- ) -> str:
414
- """
415
- Generates Python code to create a material and assign it to an object with proper error handling.
416
-
417
- Args:
418
- object_name: Name of the object to assign material to
419
- material_name: Name for the new material (default: derived from object name)
420
- color: RGBA color tuple (r, g, b, a) with values from 0.0 to 1.0
421
-
422
- Returns:
423
- String containing Python code that can be executed in Blender
424
- """
425
- if material_name is None:
426
- material_name = f"{object_name}_material"
427
-
428
- # Build a code block with proper error handling
429
- code = f"""
430
- import bpy
431
-
432
- result = {{"status": "processing"}}
433
-
434
- # Get the object
435
- obj = bpy.data.objects.get('{object_name}')
436
- if not obj:
437
- result = {{"status": "error", "message": "Object '{object_name}' not found"}}
438
- else:
439
- # Create the material if it doesn't exist
440
- mat = bpy.data.materials.get('{material_name}')
441
- if not mat:
442
- mat = bpy.data.materials.new(name="{material_name}")
443
-
444
- # Set the color
445
- mat.diffuse_color = {color}
446
-
447
- # Assign material to object
448
- if len(obj.data.materials) == 0:
449
- obj.data.materials.append(mat)
450
- else:
451
- obj.data.materials[0] = mat
452
-
453
- result = {{
454
- "status": "success",
455
- "message": "Material created and assigned",
456
- "object": "{object_name}",
457
- "material": "{material_name}"
458
- }}
459
-
460
- result
461
- """
462
- return code
463
-
464
-
465
- def generate_materials_for_all_objects_code() -> str:
466
- """
467
- Generates Python code that creates default materials for all objects in the scene
468
- that don't already have materials assigned.
469
-
470
- Returns:
471
- String containing Python code that can be executed in Blender
472
- """
473
- code = """
474
- import bpy
475
-
476
- result = {"status": "processing", "created": 0, "objects": []}
477
-
478
- # Process all objects in the scene
479
- for obj in bpy.data.objects:
480
- # Skip objects that can't have materials or already have materials
481
- if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} or len(obj.material_slots) > 0 and obj.active_material:
482
- continue
483
-
484
- # Create a new material for this object
485
- mat_name = f"{obj.name}_material"
486
- mat = bpy.data.materials.new(name=mat_name)
487
-
488
- # Set default color based on object name (for consistent results)
489
- import hashlib
490
- name_hash = int(hashlib.md5(obj.name.encode()).hexdigest(), 16)
491
- r = ((name_hash & 0xFF0000) >> 16) / 255.0
492
- g = ((name_hash & 0x00FF00) >> 8) / 255.0
493
- b = (name_hash & 0x0000FF) / 255.0
494
- mat.diffuse_color = (r, g, b, 1.0)
495
-
496
- # Assign the material to the object
497
- obj.data.materials.append(mat)
498
-
499
- result["objects"].append({"name": obj.name, "material": mat_name})
500
- result["created"] += 1
501
-
502
- result["status"] = "success"
503
- result["message"] = f"Created materials for {result['created']} objects"
504
- result
505
- """
506
- return code
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Dict
5
+
6
+ from gaia.mcp.blender_mcp_client import MCPClient
7
+
8
+
9
+ class MaterialManager:
10
+ """Manages Blender material operations."""
11
+
12
+ def __init__(self, mcp: MCPClient):
13
+ self.mcp = mcp
14
+
15
+ def create_ground_material(
16
+ self, ground_texture_name: str, maps_texture_name: str
17
+ ) -> Dict:
18
+ """Create the ground material with separate land and water shaders, using displacement."""
19
+
20
+ def generate_code():
21
+ return f"""
22
+ import bpy
23
+
24
+ # Get the Earth object
25
+ earth = bpy.data.objects.get("Earth")
26
+ if not earth:
27
+ print("Error: Earth object not found")
28
+ exit()
29
+
30
+ # Create new material
31
+ ground_mat = bpy.data.materials.new(name="ground")
32
+ ground_mat.use_nodes = True
33
+ earth.data.materials.append(ground_mat)
34
+
35
+ # Get material nodes and links
36
+ nodes = ground_mat.node_tree.nodes
37
+ links = ground_mat.node_tree.links
38
+
39
+ # Clear default nodes
40
+ for node in nodes:
41
+ nodes.remove(node)
42
+
43
+ # Create nodes for ground material
44
+ output = nodes.new(type='ShaderNodeOutputMaterial')
45
+ output.location = (800, 0)
46
+
47
+ # Add texture image for Earth ground
48
+ tex_ground = nodes.new(type='ShaderNodeTexImage')
49
+ tex_ground.location = (-600, 200)
50
+ tex_ground.image = bpy.data.images.get("{ground_texture_name}")
51
+ tex_ground.projection = 'SPHERE'
52
+ tex_ground.interpolation = 'Linear' # Updated to Linear as shown in screenshot
53
+
54
+ # Add texture coordinate
55
+ tex_coord = nodes.new(type='ShaderNodeTexCoord')
56
+ tex_coord.location = (-900, 0)
57
+
58
+ # Earth maps (water mask and displacement)
59
+ tex_maps = nodes.new(type='ShaderNodeTexImage')
60
+ tex_maps.location = (-600, -200)
61
+ tex_maps.image = bpy.data.images.get("{maps_texture_name}")
62
+ tex_maps.projection = 'SPHERE'
63
+ tex_maps.interpolation = 'Linear' # Already set to Linear
64
+
65
+ # Separate RGB for maps
66
+ separate_rgb = nodes.new(type='ShaderNodeSeparateRGB')
67
+ separate_rgb.location = (-300, -200)
68
+
69
+ # Single Principled BSDF with values matching screenshot
70
+ principled = nodes.new(type='ShaderNodeBsdfPrincipled')
71
+ principled.location = (400, 200)
72
+ principled.inputs['Metallic'].default_value = 0.030 # As shown in screenshot
73
+ principled.inputs['Roughness'].default_value = 0.500 # As shown in screenshot
74
+ principled.inputs['IOR'].default_value = 1.500 # As shown in screenshot
75
+ principled.inputs['Alpha'].default_value = 1.000 # As shown in screenshot
76
+
77
+ # Alternative approach - keep both land and water shaders as before
78
+ # Land material
79
+ land_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
80
+ land_shader.location = (100, 200)
81
+ land_shader.inputs['Specular'].default_value = 0.0
82
+ land_shader.inputs['Roughness'].default_value = 1.0
83
+
84
+ # Water material
85
+ water_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
86
+ water_shader.location = (100, -100)
87
+ water_shader.inputs['Roughness'].default_value = 0.4
88
+ water_shader.inputs['IOR'].default_value = 1.333
89
+
90
+ # Mix shader
91
+ mix_shader = nodes.new(type='ShaderNodeMixShader')
92
+ mix_shader.location = (500, 0)
93
+
94
+ # Displacement node
95
+ displace = nodes.new(type='ShaderNodeDisplacement')
96
+ displace.location = (500, -300)
97
+ displace.inputs['Scale'].default_value = 0.005 # Match tutorial value for displacement
98
+
99
+ # Connect nodes
100
+ # Option 1: Using single Principled BSDF (as shown in screenshot)
101
+ links.new(tex_coord.outputs['Generated'], tex_ground.inputs['Vector'])
102
+ links.new(tex_coord.outputs['Generated'], tex_maps.inputs['Vector'])
103
+ links.new(tex_ground.outputs['Color'], principled.inputs['Base Color'])
104
+ links.new(tex_maps.outputs['Color'], separate_rgb.inputs['Image'])
105
+ links.new(separate_rgb.outputs['R'], displace.inputs['Height']) # R (red) channel is height
106
+ links.new(principled.outputs['BSDF'], output.inputs['Surface'])
107
+ links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
108
+
109
+ # Set material displacement method to match tutorial
110
+ ground_mat.displacement_method = 'DISPLACEMENT'
111
+
112
+ print("Ground material created exactly as shown in screenshot")
113
+ """
114
+
115
+ return self.mcp.execute_code(generate_code())
116
+
117
+ def create_atmosphere_material(self) -> Dict:
118
+ """Create the atmosphere material with volume scatter."""
119
+
120
+ def generate_code():
121
+ return """
122
+ import bpy
123
+
124
+ # Create atmosphere material
125
+ atm_mat = bpy.data.materials.new(name="atmosphere")
126
+ atm_mat.use_nodes = True
127
+
128
+ # Get material nodes
129
+ nodes = atm_mat.node_tree.nodes
130
+ links = atm_mat.node_tree.links
131
+
132
+ # Clear default nodes
133
+ for node in nodes:
134
+ nodes.remove(node)
135
+
136
+ # Create nodes for atmosphere material as shown in tutorial
137
+ output = nodes.new(type='ShaderNodeOutputMaterial')
138
+ output.location = (800, 0)
139
+
140
+ # Add texture coordinate for atmosphere
141
+ tex_coord = nodes.new(type='ShaderNodeTexCoord')
142
+ tex_coord.location = (-900, 0)
143
+
144
+ # Add volume scatter - match tutorial color exactly
145
+ volume_scatter = nodes.new(type='ShaderNodeVolumeScatter')
146
+ volume_scatter.location = (500, 200)
147
+ volume_scatter.inputs['Color'].default_value = (0.3, 0.6, 1.0, 1.0) # Blue color from tutorial
148
+
149
+ # Value for atmosphere thickness - 1% of planet radius as in tutorial
150
+ thickness = nodes.new(type='ShaderNodeValue')
151
+ thickness.location = (-600, -300)
152
+ thickness.outputs[0].default_value = 0.01 # 1% of planet radius as specified in tutorial
153
+
154
+ # Vector length for atmosphere density gradient
155
+ vec_math = nodes.new(type='ShaderNodeVectorMath')
156
+ vec_math.location = (-600, 0)
157
+ vec_math.operation = 'LENGTH'
158
+
159
+ # Subtract 1 to get 0 at surface level
160
+ math_sub = nodes.new(type='ShaderNodeMath')
161
+ math_sub.location = (-400, 0)
162
+ math_sub.operation = 'SUBTRACT'
163
+ math_sub.inputs[1].default_value = 1.0
164
+
165
+ # Divide by thickness to normalize distance - exactly as in tutorial
166
+ math_div = nodes.new(type='ShaderNodeMath')
167
+ math_div.location = (-200, 0)
168
+ math_div.operation = 'DIVIDE'
169
+ math_div.use_clamp = True
170
+
171
+ # Multiply by 15 for density adjustment - exact value from tutorial
172
+ math_mul1 = nodes.new(type='ShaderNodeMath')
173
+ math_mul1.location = (0, 0)
174
+ math_mul1.operation = 'MULTIPLY'
175
+ math_mul1.inputs[1].default_value = 15.0 # Tutorial uses exactly 15
176
+
177
+ # Power for exponential falloff - uses e (Euler's number) in tutorial
178
+ math_pow = nodes.new(type='ShaderNodeMath')
179
+ math_pow.location = (200, 0)
180
+ math_pow.operation = 'POWER'
181
+ math_pow.inputs[1].default_value = 1.0 # Power of e (Euler's number)
182
+
183
+ # Multiply by 0.05 for final density - exact value from tutorial
184
+ math_mul2 = nodes.new(type='ShaderNodeMath')
185
+ math_mul2.location = (400, 0)
186
+ math_mul2.operation = 'MULTIPLY'
187
+ math_mul2.inputs[1].default_value = 0.05 # Tutorial uses exactly 0.05
188
+
189
+ # Displacement for atmosphere
190
+ displace = nodes.new(type='ShaderNodeDisplacement')
191
+ displace.location = (500, -200)
192
+ displace.inputs['Scale'].default_value = 1.0
193
+
194
+ # Connect nodes exactly as demonstrated in tutorial
195
+ links.new(tex_coord.outputs['Object'], vec_math.inputs[0])
196
+ links.new(vec_math.outputs[0], math_sub.inputs[0])
197
+ links.new(math_sub.outputs[0], math_div.inputs[0])
198
+ links.new(thickness.outputs[0], math_div.inputs[1])
199
+ links.new(math_div.outputs[0], math_mul1.inputs[0])
200
+ links.new(math_mul1.outputs[0], math_pow.inputs[0])
201
+ links.new(math_pow.outputs[0], math_mul2.inputs[0])
202
+ links.new(math_mul2.outputs[0], volume_scatter.inputs['Density'])
203
+ links.new(thickness.outputs[0], displace.inputs['Scale'])
204
+ links.new(volume_scatter.outputs['Volume'], output.inputs['Volume'])
205
+ links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
206
+
207
+ # Set material displacement method as in tutorial
208
+ atm_mat.displacement_method = 'DISPLACEMENT'
209
+
210
+ print("Atmosphere material created exactly as in tutorial")
211
+ """
212
+
213
+ return self.mcp.execute_code(generate_code())
214
+
215
+ def create_clouds_material(self, clouds_texture_name: str) -> Dict:
216
+ """Create the clouds material with subsurface scattering."""
217
+
218
+ def generate_code():
219
+ return f"""
220
+ import bpy
221
+
222
+ # Create clouds material
223
+ clouds_mat = bpy.data.materials.new(name="clouds")
224
+ clouds_mat.use_nodes = True
225
+
226
+ # Get material nodes
227
+ nodes = clouds_mat.node_tree.nodes
228
+ links = clouds_mat.node_tree.links
229
+
230
+ # Clear default nodes
231
+ for node in nodes:
232
+ nodes.remove(node)
233
+
234
+ # Create nodes for clouds material - match tutorial exactly
235
+ output = nodes.new(type='ShaderNodeOutputMaterial')
236
+ output.location = (1000, 0)
237
+
238
+ # Add texture coordinate for clouds
239
+ tex_coord = nodes.new(type='ShaderNodeTexCoord')
240
+ tex_coord.location = (-900, 0)
241
+
242
+ # Add cloud texture
243
+ tex_clouds = nodes.new(type='ShaderNodeTexImage')
244
+ tex_clouds.location = (-600, 0)
245
+ tex_clouds.image = bpy.data.images.get("{clouds_texture_name}")
246
+ tex_clouds.projection = 'SPHERE'
247
+ tex_clouds.interpolation = 'Linear' # Match tutorial setting
248
+
249
+ # Gamma correction for cloud texture - exactly 0.5 as in tutorial
250
+ gamma = nodes.new(type='ShaderNodeGamma')
251
+ gamma.location = (-400, 0)
252
+ gamma.inputs['Gamma'].default_value = 0.5 # Exactly as in tutorial
253
+
254
+ # Second gamma correction - exactly 0.9 as in tutorial
255
+ gamma2 = nodes.new(type='ShaderNodeGamma')
256
+ gamma2.location = (-200, 0)
257
+ gamma2.inputs['Gamma'].default_value = 0.9 # Exactly as in tutorial
258
+
259
+ # Add Transparent BSDF
260
+ transparent = nodes.new(type='ShaderNodeBsdfTransparent')
261
+ transparent.location = (400, 100)
262
+
263
+ # Add Subsurface Scattering BSDF - exactly as in tutorial
264
+ subsurface = nodes.new(type='ShaderNodeSubsurfaceScattering')
265
+ subsurface.location = (400, -100)
266
+ subsurface.inputs['Radius'].default_value = (1, 1, 1) # Tutorial setting
267
+
268
+ # Multiply for cloud intensity - exactly 5.0 as in tutorial
269
+ math_mul = nodes.new(type='ShaderNodeMath')
270
+ math_mul.location = (0, -200)
271
+ math_mul.operation = 'MULTIPLY'
272
+ math_mul.inputs[1].default_value = 5.0 # Exactly as in tutorial
273
+
274
+ # Power for cloud exponential control - as in tutorial
275
+ math_pow = nodes.new(type='ShaderNodeMath')
276
+ math_pow.location = (200, -200)
277
+ math_pow.operation = 'POWER'
278
+ math_pow.inputs[1].default_value = 1.0 # Tutorial setting
279
+
280
+ # Mix shader
281
+ mix_shader = nodes.new(type='ShaderNodeMixShader')
282
+ mix_shader.location = (700, 0)
283
+
284
+ # Displacement node - exactly 0.005 as in tutorial
285
+ displace = nodes.new(type='ShaderNodeDisplacement')
286
+ displace.location = (700, -300)
287
+ displace.inputs['Scale'].default_value = 0.005 # Exactly as in tutorial
288
+
289
+ # Connect nodes exactly as in tutorial
290
+ links.new(tex_coord.outputs['Generated'], tex_clouds.inputs['Vector'])
291
+ links.new(tex_clouds.outputs['Color'], gamma.inputs['Color'])
292
+ links.new(gamma.outputs['Color'], gamma2.inputs['Color'])
293
+ links.new(gamma2.outputs['Color'], subsurface.inputs['Color'])
294
+ links.new(gamma2.outputs['Color'], math_mul.inputs[0])
295
+ links.new(math_mul.outputs[0], math_pow.inputs[0])
296
+ links.new(math_pow.outputs[0], mix_shader.inputs['Fac'])
297
+ links.new(transparent.outputs['BSDF'], mix_shader.inputs[1])
298
+ links.new(subsurface.outputs['BSSRDF'], mix_shader.inputs[2])
299
+ links.new(tex_clouds.outputs['Color'], displace.inputs['Height'])
300
+ links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
301
+ links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
302
+
303
+ # Set material displacement method as in tutorial
304
+ clouds_mat.displacement_method = 'BUMP' # Tutorial setting
305
+
306
+ print("Clouds material created exactly as in tutorial")
307
+ """
308
+
309
+ return self.mcp.execute_code(generate_code())
310
+
311
+ def set_material_color(self, object_name: str, color: tuple = (1, 0, 0, 1)) -> Dict:
312
+ """
313
+ Set the material color for an object. Creates a new material if one doesn't exist.
314
+
315
+ Args:
316
+ object_name: Name of the object to modify
317
+ color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1
318
+
319
+ Returns:
320
+ Dictionary with the operation result
321
+ """
322
+
323
+ def generate_code():
324
+ return f"""
325
+ import bpy
326
+
327
+ result = {{"status": "processing"}}
328
+
329
+ # Get the object
330
+ obj = bpy.data.objects.get("{object_name}")
331
+ if not obj:
332
+ result = {{"status": "error", "error": f"Object '{object_name}' not found"}}
333
+ else:
334
+ # Create a new material if needed
335
+ mat_name = "{object_name}_material"
336
+ if mat_name in bpy.data.materials:
337
+ mat = bpy.data.materials[mat_name]
338
+ else:
339
+ mat = bpy.data.materials.new(name=mat_name)
340
+
341
+ # Enable nodes for the material
342
+ mat.use_nodes = True
343
+ nodes = mat.node_tree.nodes
344
+
345
+ # Clear existing nodes
346
+ for node in nodes:
347
+ nodes.remove(node)
348
+
349
+ # Create new nodes
350
+ output = nodes.new(type='ShaderNodeOutputMaterial')
351
+ output.location = (300, 0)
352
+
353
+ bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
354
+ bsdf.location = (0, 0)
355
+
356
+ # Set color - use correct format for Blender 4.0
357
+ bsdf.inputs["Base Color"].default_value = {color}
358
+
359
+ # Also set viewport display color for material
360
+ mat.diffuse_color = {color}
361
+
362
+ # Connect nodes
363
+ mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
364
+
365
+ # Assign material to object
366
+ if len(obj.data.materials) == 0:
367
+ obj.data.materials.append(mat)
368
+ else:
369
+ obj.data.materials[0] = mat
370
+
371
+ # Set the active material slot
372
+ obj.active_material_index = 0
373
+ obj.active_material = mat
374
+
375
+ # Set the render engine to show materials properly
376
+ # For Blender 4.0, use valid enum values
377
+ if hasattr(bpy.context.scene, 'render'):
378
+ if hasattr(bpy.context.scene.render, 'engine'):
379
+ current_engine = bpy.context.scene.render.engine
380
+ if current_engine not in ['CYCLES', 'BLENDER_EEVEE_NEXT', 'BLENDER_WORKBENCH']:
381
+ try:
382
+ bpy.context.scene.render.engine = 'CYCLES'
383
+ except Exception as e:
384
+ print(f"Could not set render engine: {{e}}")
385
+
386
+ # Force update of all 3D viewports
387
+ for window in bpy.context.window_manager.windows:
388
+ for area in window.screen.areas:
389
+ if area.type == 'VIEW_3D':
390
+ area.tag_redraw()
391
+
392
+ result = {{
393
+ "status": "success",
394
+ "message": "Material color set successfully",
395
+ "debug_info": {{
396
+ "object_name": obj.name,
397
+ "material_name": mat.name,
398
+ "color_set": list({color}),
399
+ "material_slots": len(obj.material_slots),
400
+ "active_material": obj.active_material.name if obj.active_material else None,
401
+ "render_engine": bpy.context.scene.render.engine if hasattr(bpy.context.scene, 'render') else "unknown"
402
+ }}
403
+ }}
404
+
405
+ result
406
+ """
407
+
408
+ return self.mcp.execute_code(generate_code())
409
+
410
+
411
+ def generate_material_assignment_code(
412
+ object_name: str, material_name: str = None, color: tuple = (0.8, 0.8, 0.8, 1.0)
413
+ ) -> str:
414
+ """
415
+ Generates Python code to create a material and assign it to an object with proper error handling.
416
+
417
+ Args:
418
+ object_name: Name of the object to assign material to
419
+ material_name: Name for the new material (default: derived from object name)
420
+ color: RGBA color tuple (r, g, b, a) with values from 0.0 to 1.0
421
+
422
+ Returns:
423
+ String containing Python code that can be executed in Blender
424
+ """
425
+ if material_name is None:
426
+ material_name = f"{object_name}_material"
427
+
428
+ # Build a code block with proper error handling
429
+ code = f"""
430
+ import bpy
431
+
432
+ result = {{"status": "processing"}}
433
+
434
+ # Get the object
435
+ obj = bpy.data.objects.get('{object_name}')
436
+ if not obj:
437
+ result = {{"status": "error", "message": "Object '{object_name}' not found"}}
438
+ else:
439
+ # Create the material if it doesn't exist
440
+ mat = bpy.data.materials.get('{material_name}')
441
+ if not mat:
442
+ mat = bpy.data.materials.new(name="{material_name}")
443
+
444
+ # Set the color
445
+ mat.diffuse_color = {color}
446
+
447
+ # Assign material to object
448
+ if len(obj.data.materials) == 0:
449
+ obj.data.materials.append(mat)
450
+ else:
451
+ obj.data.materials[0] = mat
452
+
453
+ result = {{
454
+ "status": "success",
455
+ "message": "Material created and assigned",
456
+ "object": "{object_name}",
457
+ "material": "{material_name}"
458
+ }}
459
+
460
+ result
461
+ """
462
+ return code
463
+
464
+
465
+ def generate_materials_for_all_objects_code() -> str:
466
+ """
467
+ Generates Python code that creates default materials for all objects in the scene
468
+ that don't already have materials assigned.
469
+
470
+ Returns:
471
+ String containing Python code that can be executed in Blender
472
+ """
473
+ code = """
474
+ import bpy
475
+
476
+ result = {"status": "processing", "created": 0, "objects": []}
477
+
478
+ # Process all objects in the scene
479
+ for obj in bpy.data.objects:
480
+ # Skip objects that can't have materials or already have materials
481
+ if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} or len(obj.material_slots) > 0 and obj.active_material:
482
+ continue
483
+
484
+ # Create a new material for this object
485
+ mat_name = f"{obj.name}_material"
486
+ mat = bpy.data.materials.new(name=mat_name)
487
+
488
+ # Set default color based on object name (for consistent results)
489
+ import hashlib
490
+ name_hash = int(hashlib.md5(obj.name.encode()).hexdigest(), 16)
491
+ r = ((name_hash & 0xFF0000) >> 16) / 255.0
492
+ g = ((name_hash & 0x00FF00) >> 8) / 255.0
493
+ b = (name_hash & 0x0000FF) / 255.0
494
+ mat.diffuse_color = (r, g, b, 1.0)
495
+
496
+ # Assign the material to the object
497
+ obj.data.materials.append(mat)
498
+
499
+ result["objects"].append({"name": obj.name, "material": mat_name})
500
+ result["created"] += 1
501
+
502
+ result["status"] = "success"
503
+ result["message"] = f"Created materials for {result['created']} objects"
504
+ result
505
+ """
506
+ return code