import bpy, math
from mathutils import Vector

# Clear the default scene completely.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for datablocks in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.cameras, bpy.data.lights):
    for block in list(datablocks):
        if block.users == 0:
            datablocks.remove(block)

scene = bpy.context.scene
scene.name = 'Red Sphere Presentation'

# Organize the scene into clear collections.
root = scene.collection
for child in list(root.children):
    root.children.unlink(child)
objects_col = bpy.data.collections.new('SCENE_OBJECTS')
lights_col = bpy.data.collections.new('PRESENTATION_LIGHTS')
root.children.link(objects_col)
root.children.link(lights_col)

# Helper to move an object from the active collection into a named collection.
def move_to_collection(obj, collection):
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    collection.objects.link(obj)

def look_at(obj, target):
    direction = Vector(target) - obj.location
    obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()

def make_principled_material(name, color, roughness, spec_level, coat_weight=0.0, coat_roughness=0.1):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    mat.diffuse_color = (*color[:3], 1.0)
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()
    out = nodes.new('ShaderNodeOutputMaterial')
    out.location = (320, 0)
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.location = (0, 0)
    bsdf.inputs['Base Color'].default_value = (*color[:3], 1.0)
    bsdf.inputs['Roughness'].default_value = roughness
    if bsdf.inputs.get('IOR'):
        bsdf.inputs['IOR'].default_value = 1.45
    if bsdf.inputs.get('Specular IOR Level'):
        bsdf.inputs['Specular IOR Level'].default_value = spec_level
    elif bsdf.inputs.get('Specular'):
        bsdf.inputs['Specular'].default_value = spec_level
    if bsdf.inputs.get('Coat Weight'):
        bsdf.inputs['Coat Weight'].default_value = coat_weight
    elif bsdf.inputs.get('Clearcoat'):
        bsdf.inputs['Clearcoat'].default_value = coat_weight
    if bsdf.inputs.get('Coat Roughness'):
        bsdf.inputs['Coat Roughness'].default_value = coat_roughness
    elif bsdf.inputs.get('Clearcoat Roughness'):
        bsdf.inputs['Clearcoat Roughness'].default_value = coat_roughness
    links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return mat

# Materials: restrained, plain colors with physically useful roughness.
red_mat = make_principled_material(
    'MAT_Red_Plain_Gloss', (0.62, 0.008, 0.012, 1.0),
    roughness=0.22, spec_level=0.5, coat_weight=0.06, coat_roughness=0.12)
white_mat = make_principled_material(
    'MAT_White_Ground_Plain', (0.82, 0.82, 0.82, 1.0),
    roughness=0.52, spec_level=0.28, coat_weight=0.0, coat_roughness=0.2)

# Ground: a true plane at z=0.
bpy.ops.mesh.primitive_plane_add(size=60.0, location=(0.0, 0.0, 0.0))
floor = bpy.context.object
floor.name = 'White_Ground_Plane_At_Z0'
floor.data.name = 'White_Ground_Plane_Mesh'
floor.data.materials.append(white_mat)
move_to_collection(floor, objects_col)
floor['surface_role'] = 'white ground plane'
floor['surface_height_m'] = 0.0

# Ball: UV sphere radius 0.5 m, with its lowest point exactly at z=0.
bpy.ops.mesh.primitive_uv_sphere_add(
    segments=96, ring_count=64, radius=0.5, location=(0.0, 0.0, 0.5))
sphere = bpy.context.object
sphere.name = 'Red_Sphere_Diameter_1m'
sphere.data.name = 'Red_Sphere_Mesh_Diameter_1m'
sphere.data.materials.append(red_mat)
move_to_collection(sphere, objects_col)
for poly in sphere.data.polygons:
    poly.use_smooth = True
sphere['surface_role'] = 'red sphere'
sphere['diameter_m'] = 1.0
sphere['radius_m'] = 0.5
sphere['resting_plane_z_m'] = 0.0
sphere['contact_condition'] = 'tangent contact: lowest point z=0.0'

# Presentation lights. Large area sources create a soft, readable contact shadow.
def add_area(name, location, energy, size, color, target=(0.0, 0.0, 0.35)):
    data = bpy.data.lights.new(name, type='AREA')
    data.energy = energy
    data.shape = 'DISK'
    data.size = size
    data.color = color
    data.use_shadow = True
    obj = bpy.data.objects.new(name, data)
    lights_col.objects.link(obj)
    obj.location = location
    look_at(obj, target)
    return obj

key = add_area('Key_Softbox_Large', (-3.2, -4.2, 5.4), 850.0, 3.2, (1.0, 0.96, 0.92))
key['lighting_role'] = 'large soft key for the floor shadow'
fill = add_area('Fill_Softbox_Subtle', (3.8, -1.0, 3.2), 230.0, 3.0, (0.88, 0.93, 1.0))
fill['lighting_role'] = 'subtle fill to retain red form'
rim = add_area('Top_Rim_Softbox', (-1.0, 3.8, 5.8), 300.0, 2.4, (1.0, 1.0, 1.0))
rim['lighting_role'] = 'soft rim highlight'

# Neutral, low-level environment so the white ground stays clean and the red stays saturated.
world = bpy.data.worlds.new('World_Neutral_Studio') if scene.world is None else scene.world
scene.world = world
world.use_nodes = True
wnodes = world.node_tree.nodes
wlinks = world.node_tree.links
wnodes.clear()
wout = wnodes.new('ShaderNodeOutputWorld')
wout.location = (260, 0)
wbg = wnodes.new('ShaderNodeBackground')
wbg.location = (0, 0)
wbg.inputs['Color'].default_value = (0.018, 0.022, 0.030, 1.0)
wbg.inputs['Strength'].default_value = 0.28
wlinks.new(wbg.outputs['Background'], wout.inputs['Surface'])
world.color = (0.018, 0.022, 0.030)

# Presentation camera: a low three-quarter view that clearly shows the tangent contact and shadow.
cam_data = bpy.data.cameras.new('Camera_Presentation_Data')
cam = bpy.data.objects.new('Camera_Presentation', cam_data)
objects_col.objects.link(cam)
cam.location = (2.9, -4.7, 2.7)
look_at(cam, (0.0, 0.0, 0.38))
cam_data.type = 'PERSP'
cam_data.lens = 70.0
cam_data.sensor_width = 36.0
cam_data.dof.use_dof = False
cam_data.lens = 70.0
scene.camera = cam
cam['composition'] = 'three-quarter low view with visible contact shadow'

# Render settings: leave the saved scene ready for the benchmark's Cycles render.
scene.render.engine = 'CYCLES'
scene.cycles.samples = 64
scene.cycles.use_denoising = True
scene.render.resolution_x = 1536
scene.render.resolution_y = 1536
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False
scene.render.image_settings.color_mode = 'RGBA'
try:
    scene.render.image_settings.color_depth = '8'
except Exception:
    pass
# Use a clean contrast curve without crushing the red.
try:
    scene.view_settings.look = 'AgX - Medium High Contrast'
except Exception:
    try:
        scene.view_settings.look = 'Medium High Contrast'
    except Exception:
        pass
scene.view_settings.exposure = 0.0
scene.view_settings.gamma = 1.0

# Metadata on the scene makes the intended physical setup explicit.
scene['task'] = '1 m diameter red sphere resting exactly on a white ground plane'
scene['sphere_diameter_m'] = 1.0
scene['sphere_center_z_m'] = 0.5
scene['ground_z_m'] = 0.0
scene['contact_gap_m'] = 0.0

# Select the ball for a useful initial interactive state.
bpy.ops.object.select_all(action='DESELECT')
sphere.select_set(True)
bpy.context.view_layer.objects.active = sphere

# Verify the geometric contact numerically before saving.
min_z = min((sphere.matrix_world @ v.co).z for v in sphere.data.vertices)
max_z = max((sphere.matrix_world @ v.co).z for v in sphere.data.vertices)
result = {
    'sphere_name': sphere.name,
    'sphere_dimensions': tuple(round(v, 8) for v in sphere.dimensions),
    'sphere_min_z': round(min_z, 10),
    'sphere_max_z': round(max_z, 10),
    'floor_z': floor.location.z,
    'contact_gap': round(min_z - floor.location.z, 10),
    'camera': cam.name,
    'render_engine': scene.render.engine,
}

# Save the complete scene.
bpy.ops.wm.save_as_mainfile(filepath='/tmp/blender_bench_red_sphere.blend')


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 201, in <module>
RuntimeError: Error: Cannot open file <repo>\apps\console\tmp\blender_bench_red_sphere.blend@ for writing: No such file or directory