amd-gaia 0.15.0__py3-none-any.whl → 0.15.2__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 (185) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
  2. amd_gaia-0.15.2.dist-info/RECORD +182 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
  5. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
  6. gaia/__init__.py +29 -29
  7. gaia/agents/__init__.py +19 -19
  8. gaia/agents/base/__init__.py +9 -9
  9. gaia/agents/base/agent.py +2132 -2177
  10. gaia/agents/base/api_agent.py +119 -120
  11. gaia/agents/base/console.py +1967 -1841
  12. gaia/agents/base/errors.py +237 -237
  13. gaia/agents/base/mcp_agent.py +86 -86
  14. gaia/agents/base/tools.py +88 -83
  15. gaia/agents/blender/__init__.py +7 -0
  16. gaia/agents/blender/agent.py +553 -556
  17. gaia/agents/blender/agent_simple.py +133 -135
  18. gaia/agents/blender/app.py +211 -211
  19. gaia/agents/blender/app_simple.py +41 -41
  20. gaia/agents/blender/core/__init__.py +16 -16
  21. gaia/agents/blender/core/materials.py +506 -506
  22. gaia/agents/blender/core/objects.py +316 -316
  23. gaia/agents/blender/core/rendering.py +225 -225
  24. gaia/agents/blender/core/scene.py +220 -220
  25. gaia/agents/blender/core/view.py +146 -146
  26. gaia/agents/chat/__init__.py +9 -9
  27. gaia/agents/chat/agent.py +809 -835
  28. gaia/agents/chat/app.py +1065 -1058
  29. gaia/agents/chat/session.py +508 -508
  30. gaia/agents/chat/tools/__init__.py +15 -15
  31. gaia/agents/chat/tools/file_tools.py +96 -96
  32. gaia/agents/chat/tools/rag_tools.py +1744 -1729
  33. gaia/agents/chat/tools/shell_tools.py +437 -436
  34. gaia/agents/code/__init__.py +7 -7
  35. gaia/agents/code/agent.py +549 -549
  36. gaia/agents/code/cli.py +377 -0
  37. gaia/agents/code/models.py +135 -135
  38. gaia/agents/code/orchestration/__init__.py +24 -24
  39. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  40. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  41. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  42. gaia/agents/code/orchestration/factories/base.py +63 -63
  43. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  44. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  45. gaia/agents/code/orchestration/orchestrator.py +841 -841
  46. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  47. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  48. gaia/agents/code/orchestration/steps/base.py +188 -188
  49. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  50. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  51. gaia/agents/code/orchestration/steps/python.py +307 -307
  52. gaia/agents/code/orchestration/template_catalog.py +469 -469
  53. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  54. gaia/agents/code/orchestration/workflows/base.py +80 -80
  55. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  56. gaia/agents/code/orchestration/workflows/python.py +94 -94
  57. gaia/agents/code/prompts/__init__.py +11 -11
  58. gaia/agents/code/prompts/base_prompt.py +77 -77
  59. gaia/agents/code/prompts/code_patterns.py +2034 -2036
  60. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  61. gaia/agents/code/prompts/python_prompt.py +109 -109
  62. gaia/agents/code/schema_inference.py +365 -365
  63. gaia/agents/code/system_prompt.py +41 -41
  64. gaia/agents/code/tools/__init__.py +42 -42
  65. gaia/agents/code/tools/cli_tools.py +1138 -1138
  66. gaia/agents/code/tools/code_formatting.py +319 -319
  67. gaia/agents/code/tools/code_tools.py +769 -769
  68. gaia/agents/code/tools/error_fixing.py +1347 -1347
  69. gaia/agents/code/tools/external_tools.py +180 -180
  70. gaia/agents/code/tools/file_io.py +845 -845
  71. gaia/agents/code/tools/prisma_tools.py +190 -190
  72. gaia/agents/code/tools/project_management.py +1016 -1016
  73. gaia/agents/code/tools/testing.py +321 -321
  74. gaia/agents/code/tools/typescript_tools.py +122 -122
  75. gaia/agents/code/tools/validation_parsing.py +461 -461
  76. gaia/agents/code/tools/validation_tools.py +806 -806
  77. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  78. gaia/agents/code/validators/__init__.py +16 -16
  79. gaia/agents/code/validators/antipattern_checker.py +241 -241
  80. gaia/agents/code/validators/ast_analyzer.py +197 -197
  81. gaia/agents/code/validators/requirements_validator.py +145 -145
  82. gaia/agents/code/validators/syntax_validator.py +171 -171
  83. gaia/agents/docker/__init__.py +7 -7
  84. gaia/agents/docker/agent.py +643 -642
  85. gaia/agents/emr/__init__.py +8 -8
  86. gaia/agents/emr/agent.py +1504 -1506
  87. gaia/agents/emr/cli.py +1322 -1322
  88. gaia/agents/emr/constants.py +475 -475
  89. gaia/agents/emr/dashboard/__init__.py +4 -4
  90. gaia/agents/emr/dashboard/server.py +1972 -1974
  91. gaia/agents/jira/__init__.py +11 -11
  92. gaia/agents/jira/agent.py +894 -894
  93. gaia/agents/jira/jql_templates.py +299 -299
  94. gaia/agents/routing/__init__.py +7 -7
  95. gaia/agents/routing/agent.py +567 -570
  96. gaia/agents/routing/system_prompt.py +75 -75
  97. gaia/agents/summarize/__init__.py +11 -0
  98. gaia/agents/summarize/agent.py +885 -0
  99. gaia/agents/summarize/prompts.py +129 -0
  100. gaia/api/__init__.py +23 -23
  101. gaia/api/agent_registry.py +238 -238
  102. gaia/api/app.py +305 -305
  103. gaia/api/openai_server.py +575 -575
  104. gaia/api/schemas.py +186 -186
  105. gaia/api/sse_handler.py +373 -373
  106. gaia/apps/__init__.py +4 -4
  107. gaia/apps/llm/__init__.py +6 -6
  108. gaia/apps/llm/app.py +184 -169
  109. gaia/apps/summarize/app.py +116 -633
  110. gaia/apps/summarize/html_viewer.py +133 -133
  111. gaia/apps/summarize/pdf_formatter.py +284 -284
  112. gaia/audio/__init__.py +2 -2
  113. gaia/audio/audio_client.py +439 -439
  114. gaia/audio/audio_recorder.py +269 -269
  115. gaia/audio/kokoro_tts.py +599 -599
  116. gaia/audio/whisper_asr.py +432 -432
  117. gaia/chat/__init__.py +16 -16
  118. gaia/chat/app.py +428 -430
  119. gaia/chat/prompts.py +522 -522
  120. gaia/chat/sdk.py +1228 -1225
  121. gaia/cli.py +5659 -5632
  122. gaia/database/__init__.py +10 -10
  123. gaia/database/agent.py +176 -176
  124. gaia/database/mixin.py +290 -290
  125. gaia/database/testing.py +64 -64
  126. gaia/eval/batch_experiment.py +2332 -2332
  127. gaia/eval/claude.py +542 -542
  128. gaia/eval/config.py +37 -37
  129. gaia/eval/email_generator.py +512 -512
  130. gaia/eval/eval.py +3179 -3179
  131. gaia/eval/groundtruth.py +1130 -1130
  132. gaia/eval/transcript_generator.py +582 -582
  133. gaia/eval/webapp/README.md +167 -167
  134. gaia/eval/webapp/package-lock.json +875 -875
  135. gaia/eval/webapp/package.json +20 -20
  136. gaia/eval/webapp/public/app.js +3402 -3402
  137. gaia/eval/webapp/public/index.html +87 -87
  138. gaia/eval/webapp/public/styles.css +3661 -3661
  139. gaia/eval/webapp/server.js +415 -415
  140. gaia/eval/webapp/test-setup.js +72 -72
  141. gaia/installer/__init__.py +23 -0
  142. gaia/installer/init_command.py +1275 -0
  143. gaia/installer/lemonade_installer.py +619 -0
  144. gaia/llm/__init__.py +10 -2
  145. gaia/llm/base_client.py +60 -0
  146. gaia/llm/exceptions.py +12 -0
  147. gaia/llm/factory.py +70 -0
  148. gaia/llm/lemonade_client.py +3421 -3221
  149. gaia/llm/lemonade_manager.py +294 -294
  150. gaia/llm/providers/__init__.py +9 -0
  151. gaia/llm/providers/claude.py +108 -0
  152. gaia/llm/providers/lemonade.py +118 -0
  153. gaia/llm/providers/openai_provider.py +79 -0
  154. gaia/llm/vlm_client.py +382 -382
  155. gaia/logger.py +189 -189
  156. gaia/mcp/agent_mcp_server.py +245 -245
  157. gaia/mcp/blender_mcp_client.py +138 -138
  158. gaia/mcp/blender_mcp_server.py +648 -648
  159. gaia/mcp/context7_cache.py +332 -332
  160. gaia/mcp/external_services.py +518 -518
  161. gaia/mcp/mcp_bridge.py +811 -550
  162. gaia/mcp/servers/__init__.py +6 -6
  163. gaia/mcp/servers/docker_mcp.py +83 -83
  164. gaia/perf_analysis.py +361 -0
  165. gaia/rag/__init__.py +10 -10
  166. gaia/rag/app.py +293 -293
  167. gaia/rag/demo.py +304 -304
  168. gaia/rag/pdf_utils.py +235 -235
  169. gaia/rag/sdk.py +2194 -2194
  170. gaia/security.py +183 -163
  171. gaia/talk/app.py +287 -289
  172. gaia/talk/sdk.py +538 -538
  173. gaia/testing/__init__.py +87 -87
  174. gaia/testing/assertions.py +330 -330
  175. gaia/testing/fixtures.py +333 -333
  176. gaia/testing/mocks.py +493 -493
  177. gaia/util.py +46 -46
  178. gaia/utils/__init__.py +33 -33
  179. gaia/utils/file_watcher.py +675 -675
  180. gaia/utils/parsing.py +223 -223
  181. gaia/version.py +100 -100
  182. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  183. gaia/agents/code/app.py +0 -266
  184. gaia/llm/llm_client.py +0 -723
  185. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.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