import bpy, math
from mathutils import Vector

# Reset scene completely
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for ng in list(bpy.data.node_groups):
    if ng.bl_idname == 'GeometryNodeTree':
        bpy.data.node_groups.remove(ng)

scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 120
scene.render.fps = 24
scene.frame_current = 45
scene.render.engine = 'BLENDER_EEVEE'
scene.render.resolution_x = 700
scene.render.resolution_y = 700
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False
scene.view_settings.look = 'AgX - Medium High Contrast'

# World stage
world = bpy.data.worlds.get('World_WaveField') or bpy.data.worlds.new('World_WaveField')
scene.world = world
world.use_nodes = True
wbg = world.node_tree.nodes.get('Background')
wbg.inputs['Color'].default_value = (0.003, 0.008, 0.018, 1)
wbg.inputs['Strength'].default_value = 0.12

# Materials
def principled_material(name, base, metallic=0.0, roughness=0.4, emission=None, emission_strength=0.0):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True
    nt = m.node_tree
    nt.nodes.clear()
    out = nt.nodes.new('ShaderNodeOutputMaterial'); out.location=(420,0)
    bs = nt.nodes.new('ShaderNodeBsdfPrincipled'); bs.location=(120,0)
    bs.inputs['Base Color'].default_value=(*base,1)
    bs.inputs['Metallic'].default_value=metallic
    bs.inputs['Roughness'].default_value=roughness
    if emission is not None:
        em = bs.inputs.get('Emission Color') or bs.inputs.get('Emission')
        if em: em.default_value=(*emission,1)
        es = bs.inputs.get('Emission Strength')
        if es: es.default_value=emission_strength
    nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
    return m

tile_mat = bpy.data.materials.get('MAT_Tiles_WaveColor') or bpy.data.materials.new('MAT_Tiles_WaveColor')
tile_mat.use_nodes=True
nt=tile_mat.node_tree
nt.nodes.clear()
out=nt.nodes.new('ShaderNodeOutputMaterial'); out.location=(720,0)
bs=nt.nodes.new('ShaderNodeBsdfPrincipled'); bs.location=(430,0)
bs.inputs['Metallic'].default_value=0.28
bs.inputs['Roughness'].default_value=0.24
if bs.inputs.get('Coat Weight'): bs.inputs['Coat Weight'].default_value=0.18
attr=nt.nodes.new('ShaderNodeAttribute'); attr.location=(-620,80); attr.attribute_name='wave_mask'
ramp=nt.nodes.new('ShaderNodeValToRGB'); ramp.location=(-360,100); ramp.label='Wave mask to restrained palette'
cr=ramp.color_ramp
cr.elements.remove(cr.elements[1])
e0=cr.elements[0]; e0.position=0.0; e0.color=(0.008,0.028,0.065,1)
e1=cr.elements.new(0.34); e1.color=(0.018,0.16,0.23,1)
e2=cr.elements.new(0.72); e2.color=(0.035,0.46,0.64,1)
e3=cr.elements.new(0.93); e3.color=(0.20,0.82,0.93,1)
e4=cr.elements.new(1.0); e4.color=(1.0,0.32,0.055,1)
for e in cr.elements: e.interpolation='EASE'
nt.links.new(attr.outputs['Fac'],ramp.inputs['Fac'])
nt.links.new(ramp.outputs['Color'],bs.inputs['Base Color'])
if bs.inputs.get('Emission Color'):
    nt.links.new(ramp.outputs['Color'],bs.inputs['Emission Color'])
else:
    nt.links.new(ramp.outputs['Color'],bs.inputs['Emission'])
mult=nt.nodes.new('ShaderNodeMath'); mult.operation='MULTIPLY'; mult.location=(-110,-120); mult.inputs[1].default_value=5.0
nt.links.new(attr.outputs['Fac'],mult.inputs[0])
add=nt.nodes.new('ShaderNodeMath'); add.operation='ADD'; add.location=(100,-120); add.inputs[1].default_value=0.045
nt.links.new(mult.outputs[0],add.inputs[0])
if bs.inputs.get('Emission Strength'): nt.links.new(add.outputs[0],bs.inputs['Emission Strength'])
nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
tile_mat.diffuse_color=(0.02,0.18,0.26,1)

platform_mat=principled_material('MAT_Platform',(0.008,0.018,0.038),metallic=0.55,roughness=0.25)
floor_mat=principled_material('MAT_Floor',(0.0025,0.005,0.012),metallic=0.1,roughness=0.32)
edge_mat=principled_material('MAT_EdgeAccent',(0.012,0.08,0.12),metallic=0.6,roughness=0.2,emission=(0.0,0.12,0.2),emission_strength=0.25)

# Geometry Nodes graph
ng=bpy.data.node_groups.get('GN_DIAGONAL_WAVE_INSTANCES') or bpy.data.node_groups.new('GN_DIAGONAL_WAVE_INSTANCES','GeometryNodeTree')
# clear interface if existing
for item in list(ng.interface.items_tree):
    ng.interface.remove(item)
ng.nodes.clear()
ng.interface.new_socket(name='Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry')
s_amp=ng.interface.new_socket(name='Amplitude', in_out='INPUT', socket_type='NodeSocketFloat'); s_amp.default_value=1.45; s_amp.min_value=0.0; s_amp.max_value=4.0
s_base=ng.interface.new_socket(name='Base Height', in_out='INPUT', socket_type='NodeSocketFloat'); s_base.default_value=0.16; s_base.min_value=0.02; s_base.max_value=1.0
s_cycle=ng.interface.new_socket(name='Cycle Frames', in_out='INPUT', socket_type='NodeSocketFloat'); s_cycle.default_value=119.0; s_cycle.min_value=1.0; s_cycle.max_value=10000.0
nodes=ng.nodes; links=ng.links
inp=nodes.new('NodeGroupInput'); inp.location=(-1100,-420); inp.label='Static design controls'
outp=nodes.new('NodeGroupOutput'); outp.location=(800,100)

grid=nodes.new('GeometryNodeMeshGrid'); grid.location=(-1100,260); grid.label='35 x 35 tile field'
for key,val in [('Size X',18.0),('Size Y',18.0),('Vertices X',35),('Vertices Y',35)]:
    if grid.inputs.get(key): grid.inputs[key].default_value=val
m2p=nodes.new('GeometryNodeMeshToPoints'); m2p.location=(-850,260); m2p.mode='VERTICES'; m2p.label='One point per tile'
if m2p.inputs.get('Radius'): m2p.inputs['Radius'].default_value=0.2
links.new(grid.outputs['Mesh'],m2p.inputs['Mesh'])

pos=nodes.new('GeometryNodeInputPosition'); pos.location=(-1080,-80)
sep=nodes.new('ShaderNodeSeparateXYZ'); sep.location=(-900,-80)
links.new(pos.outputs['Position'],sep.inputs['Vector'])
mx=nodes.new('ShaderNodeMath'); mx.operation='MULTIPLY'; mx.location=(-700,-60); mx.inputs[1].default_value=0.95
my=nodes.new('ShaderNodeMath'); my.operation='MULTIPLY'; my.location=(-700,-150); my.inputs[1].default_value=0.30
links.new(sep.outputs['X'],mx.inputs[0]); links.new(sep.outputs['Y'],my.inputs[0])
addxy=nodes.new('ShaderNodeMath'); addxy.operation='ADD'; addxy.location=(-510,-90)
links.new(mx.outputs[0],addxy.inputs[0]); links.new(my.outputs[0],addxy.inputs[1])
divdiag=nodes.new('ShaderNodeMath'); divdiag.operation='DIVIDE'; divdiag.location=(-330,-90); divdiag.inputs[1].default_value=22.5
links.new(addxy.outputs[0],divdiag.inputs[0])
addhalf=nodes.new('ShaderNodeMath'); addhalf.operation='ADD'; addhalf.location=(-150,-90); addhalf.inputs[1].default_value=0.5
links.new(divdiag.outputs[0],addhalf.inputs[0])

stime=nodes.new('GeometryNodeInputSceneTime'); stime.location=(-700,-370); stime.label='Scene Time (no keyframes)'
sub=nodes.new('ShaderNodeMath'); sub.operation='SUBTRACT'; sub.location=(-500,-350); sub.inputs[1].default_value=1.0
links.new(stime.outputs['Frame'],sub.inputs[0])
divtime=nodes.new('ShaderNodeMath'); divtime.operation='DIVIDE'; divtime.location=(-300,-350)
links.new(sub.outputs[0],divtime.inputs[0]); links.new(inp.outputs['Cycle Frames'],divtime.inputs[1])
phase=nodes.new('ShaderNodeMath'); phase.operation='SUBTRACT'; phase.location=(20,-160); phase.label='Diagonal phase = position - time'
links.new(addhalf.outputs[0],phase.inputs[0]); links.new(divtime.outputs[0],phase.inputs[1])
ang=nodes.new('ShaderNodeMath'); ang.operation='MULTIPLY'; ang.location=(180,-160); ang.inputs[1].default_value=2*math.pi
links.new(phase.outputs[0],ang.inputs[0])
sine=nodes.new('ShaderNodeMath'); sine.operation='SINE'; sine.location=(350,-160)
links.new(ang.outputs[0],sine.inputs[0])
posonly=nodes.new('ShaderNodeMath'); posonly.operation='MAXIMUM'; posonly.location=(500,-160); posonly.inputs[1].default_value=0.0
links.new(sine.outputs[0],posonly.inputs[0])
pow4=nodes.new('ShaderNodeMath'); pow4.operation='POWER'; pow4.location=(650,-160); pow4.inputs[1].default_value=4.0; pow4.label='Soft pulse crest'
links.new(posonly.outputs[0],pow4.inputs[0])

ampmul=nodes.new('ShaderNodeMath'); ampmul.operation='MULTIPLY'; ampmul.location=(10,-440)
links.new(pow4.outputs[0],ampmul.inputs[0]); links.new(inp.outputs['Amplitude'],ampmul.inputs[1])
height=nodes.new('ShaderNodeMath'); height.operation='ADD'; height.location=(190,-440)
links.new(ampmul.outputs[0],height.inputs[0]); links.new(inp.outputs['Base Height'],height.inputs[1])
halfheight=nodes.new('ShaderNodeMath'); halfheight.operation='MULTIPLY'; halfheight.location=(370,-440); halfheight.inputs[1].default_value=0.5
links.new(height.outputs[0],halfheight.inputs[0])
offset=nodes.new('ShaderNodeCombineXYZ'); offset.location=(550,-400); offset.inputs['X'].default_value=0; offset.inputs['Y'].default_value=0
links.new(halfheight.outputs[0],offset.inputs['Z'])

store=nodes.new('GeometryNodeStoreNamedAttribute'); store.location=(-30,260); store.data_type='FLOAT'; store.domain='POINT'; store.label='Store crest for material'; store.inputs['Name'].default_value='wave_mask'
links.new(m2p.outputs['Points'],store.inputs['Geometry']); links.new(pow4.outputs[0],store.inputs['Value'])
setpos=nodes.new('GeometryNodeSetPosition'); setpos.location=(200,260); setpos.label='Raise tiles from their base'
links.new(store.outputs['Geometry'],setpos.inputs['Geometry']); links.new(offset.outputs['Vector'],setpos.inputs['Offset'])
cube=nodes.new('GeometryNodeMeshCube'); cube.location=(-120,40); cube.label='Cube tile'
if cube.inputs.get('Size'): cube.inputs['Size'].default_value=(1.0,1.0,1.0)
inst=nodes.new('GeometryNodeInstanceOnPoints'); inst.location=(430,260); inst.label='Instance one cube per point'
links.new(setpos.outputs['Geometry'],inst.inputs['Points']); links.new(cube.outputs['Mesh'],inst.inputs['Instance'])
scale=nodes.new('ShaderNodeCombineXYZ'); scale.location=(180,40); scale.label='tile width / animated Z'
scale.inputs['X'].default_value=0.40; scale.inputs['Y'].default_value=0.40
links.new(height,scale.inputs['Z'])
links.new(scale.outputs['Vector'],inst.inputs['Scale'])
real=nodes.new('GeometryNodeRealizeInstances'); real.location=(610,260); real.label='Real geometry for export'
links.new(inst.outputs['Instances'],real.inputs['Geometry'])
setmat=nodes.new('GeometryNodeSetMaterial'); setmat.location=(800,260); setmat.label='Wave color + emission'; setmat.inputs['Material'].default_value=tile_mat
links.new(real.outputs['Geometry'],setmat.inputs['Geometry']); links.new(setmat.outputs['Geometry'],outp.inputs['Geometry'])

# Host object
grid_mesh=bpy.data.meshes.new('WaveField_ControlMesh')
wave_obj=bpy.data.objects.new('GN_WaveField_35x35',grid_mesh)
bpy.context.collection.objects.link(wave_obj)
mod=wave_obj.modifiers.new('GEOMETRY_NODES_Wave_From_Scene_Time','NODES'); mod.node_group=ng
wave_obj['System']='Geometry Nodes wave instancing'
wave_obj['Instances']='35 x 35 cubes (1225)'
wave_obj['Wave_Driver']='Scene Time frame / Cycle Frames'
wave_obj['Wave_Direction']='Diagonal projection 0.95 X + 0.30 Y'
wave_obj['Loop']='Frames 1-120; frame 120 equals frame 1'
wave_obj['No_Individual_Keyframes']=True
bev=wave_obj.modifiers.new('Softened_Tile_Edges','BEVEL'); bev.width=0.035; bev.segments=2; bev.limit_method='ANGLE'

# stage helpers
def add_box(name, loc, scale_dims, mat, bevel=0.0):
    bpy.ops.mesh.primitive_cube_add(location=loc)
    o=bpy.context.object; o.name=name; o.dimensions=scale_dims
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if mat: o.data.materials.append(mat)
    if bevel:
        b=o.modifiers.new('Rounded edges','BEVEL'); b.width=bevel; b.segments=3
    return o
platform=add_box('Stage_Platform',(0,0,-0.30),(19.8,19.8,0.48),platform_mat,0.22)
platform['Purpose']='quiet architectural plinth for the animated field'
panel=add_box('Stage_Inset',(0,0,-0.045),(18.65,18.65,0.08),floor_mat,0.12)
edge_z=-0.005
add_box('Frame_North',(0,9.35,edge_z),(18.9,0.075,0.07),edge_mat,0.025)
add_box('Frame_South',(0,-9.35,edge_z),(18.9,0.075,0.07),edge_mat,0.025)
add_box('Frame_East',(9.35,0,edge_z),(0.075,18.9,0.07),edge_mat,0.025)
add_box('Frame_West',(-9.35,0,edge_z),(0.075,18.9,0.07),edge_mat,0.025)
bpy.ops.mesh.primitive_plane_add(size=70, location=(0,0,-0.56))
floor=bpy.context.object; floor.name='Ground_Plane'; floor.data.materials.append(floor_mat)

# camera
bpy.ops.object.camera_add(location=(22.5,-25.5,21.5))
cam=bpy.context.object; cam.name='CAM_Presentation_ThreeQuarter'; cam.data.lens=53; cam.data.sensor_width=36
target=Vector((0,0,0.35)); cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler(); scene.camera=cam
cam['Composition']='Three-quarter elevated view: grid, gaps, and crest height'

# lights
def add_area(name, loc, energy, color, size, target=(0,0,0)):
    data=bpy.data.lights.new(name,'AREA'); data.energy=energy; data.color=color; data.shape='DISK'; data.size=size
    o=bpy.data.objects.new(name,data); bpy.context.collection.objects.link(o); o.location=loc
    o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler(); return o
add_area('Key_Softbox',(5,-11,25),1450,(0.72,0.88,1.0),10,(0,0,0))
add_area('Fill_Cool',(-16,-4,12),900,(0.25,0.55,1.0),8,(0,0,0))
add_area('Rim_Warm',(13,15,16),1250,(1.0,0.28,0.10),7,(2,2,0.4))
bpy.ops.object.light_add(type='AREA', location=(0,2,30))
top=bpy.context.object; top.name='Overhead_Clarity'; top.data.energy=650; top.data.color=(0.32,0.48,0.60); top.data.shape='DISK'; top.data.size=16

# compositor fog glow, native render only
scene.use_nodes=True
cn=scene.node_tree.nodes; cl=scene.node_tree.links; cn.clear()
rl=cn.new('CompositorNodeRLayers'); rl.location=(-260,0)
gl=cn.new('CompositorNodeGlare'); gl.location=(0,0); gl.glare_type='FOG_GLOW'; gl.quality='HIGH'; gl.threshold=1.1; gl.size=6
comp=cn.new('CompositorNodeComposite'); comp.location=(260,0)
cl.new(rl.outputs['Image'],gl.inputs['Image']); cl.new(gl.outputs['Image'],comp.inputs['Image'])

scene.render.filepath='/tmp/wave_field_preview.png'; scene.render.image_settings.file_format='PNG'; scene.render.resolution_percentage=100
scene['Artwork_Title']='DIAGONAL PHASE / 01'
scene['Animation_Statement']='A seamless scene-time-driven crest travels diagonally across 1225 instanced cubes.'
scene['Loop_Range']='1–120 (119-frame normalized period; endpoints coincide)'
scene['Geometry_Nodes_Only_For_Field']=True
bpy.context.view_layer.objects.active=wave_obj; wave_obj.select_set(True)
for o in list(bpy.context.selected_objects):
    if o!=wave_obj: o.select_set(False)
scene.frame_set(45)
bpy.ops.wm.save_as_mainfile(filepath='/tmp/diagonal_wave_field.blend')
result={'scene_objects':len(bpy.data.objects),'node_group':ng.name,'gn_nodes':len(ng.nodes),'frame':scene.frame_current,'render_engine':scene.render.engine,'camera':scene.camera.name,'materials':[m.name for m in bpy.data.materials],'custom_loop':wave_obj['Loop']}


Traceback (most recent call last):
  File "<repo>\.runner\blender-mcp-v1.0.0\addon\mcp_to_blender_server.py", line 230, in _execute_code
    exec(code, namespace)
    ~~~~^^^^^^^^^^^^^^^^^
  File "<string>", line 69, in <module>
AttributeError: 'ColorRampElement' object has no attribute 'interpolation'