"""
Test implementation for features/09_baking.feature
Tests normal map baking from high-poly to low-poly meshes.
"""
import pytest
from pytest_bdd import scenarios, given, when, then, parsers
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
scenarios('../../features/09_baking.feature')
@given("the MCP server is running")
def mcp_server(bdd_context):
bdd_context["server_running"] = True
@given("the Blender addon is connected")
def blender_connected(bdd_context, mock_blender_connection):
mock_blender_connection.connect()
bdd_context["connection"] = mock_blender_connection
@given("high-poly and low-poly meshes exist")
def meshes_exist(bdd_context, mesh_factory):
bdd_context["high_poly"] = mesh_factory.create_high_poly(face_count=10000)
bdd_context["low_poly"] = mesh_factory.create_cube(subdivisions=2)
@given("the low-poly mesh has UV coordinates")
def low_poly_has_uvs(bdd_context):
if "low_poly" in bdd_context:
bdd_context["low_poly"]["has_uvs"] = True
@given(parsers.parse('a high-poly mesh "{high}" and low-poly mesh "{low}"'))
def named_meshes_exist(bdd_context, mesh_factory, high, low):
bdd_context["high_poly_name"] = high
bdd_context["low_poly_name"] = low
bdd_context["high_poly"] = mesh_factory.create_high_poly(face_count=10000)
bdd_context["low_poly"] = mesh_factory.create_cube(subdivisions=2)
@given(parsers.parse('"{low}" has proper UV unwrapping'))
def mesh_has_uv_unwrapping(bdd_context, low):
if "low_poly" in bdd_context:
bdd_context["low_poly"]["has_uvs"] = True
bdd_context["low_poly"]["uv_unwrapped"] = True
@given("a high-poly and low-poly with significant distance")
def meshes_with_distance(bdd_context, mesh_factory):
bdd_context["high_poly"] = mesh_factory.create_high_poly(face_count=10000)
bdd_context["low_poly"] = mesh_factory.create_cube(subdivisions=2)
bdd_context["needs_cage"] = True
@given("a low-poly mesh without UV coordinates")
def mesh_without_uvs(bdd_context, mesh_factory):
mesh = mesh_factory.create_cube(subdivisions=2)
mesh["has_uvs"] = False
bdd_context["low_poly"] = mesh
@given("the //textures/ directory does not exist")
def textures_dir_not_exists(bdd_context):
bdd_context["textures_dir_exists"] = False
@given(parsers.parse('"{low}" has no material'))
def mesh_has_no_material(bdd_context, low):
if "low_poly" in bdd_context:
bdd_context["low_poly"]["has_material"] = False
@when(parsers.parse('I call bake_normals with high="{high}", low="{low}", map_size={size:d}, space="{space}"'))
def call_bake_normals_full(bdd_context, mock_blender_connection, high, low, size, space):
low_poly = bdd_context.get("low_poly", {})
if not low_poly.get("has_uvs", True):
result = {
"success": False,
"error": "PRECONDITION_FAILED",
"message": "Mesh has no UV coordinates",
"suggestions": ["Unwrap UVs before baking", "Use Smart UV Project"]
}
else:
result = {
"success": True,
"result": {
"high_poly": high,
"low_poly": low,
"map_size": size,
"normal_space": space,
"image_path": f"//textures/{low}_normals.png",
"resolution": f"{size}x{size}",
"material_assigned": True
}
}
params = {"high": high, "low": low, "map_size": size, "space": space}
mock_blender_connection.set_response("bake_normals", params, result)
bdd_context["bake_result"] = mock_blender_connection.send_command("bake_normals", params)
@when(parsers.parse('I call bake_normals with cage="{cage}"'))
def call_bake_normals_with_cage(bdd_context, mock_blender_connection, cage):
result = {
"success": True,
"result": {
"cage_object": cage,
"cage_used_for_raycasting": True,
"artifacts_minimized": True,
"details_captured": True,
"image_path": "//textures/normals.png"
}
}
params = {"cage": cage}
mock_blender_connection.set_response("bake_normals", params, result)
bdd_context["bake_result"] = mock_blender_connection.send_command("bake_normals", params)
@when("I call bake_normals")
def call_bake_normals_default(bdd_context, mock_blender_connection, error_simulator):
low_poly = bdd_context.get("low_poly", {})
if not low_poly.get("has_uvs", True):
result = error_simulator.no_uvs()
else:
textures_exists = bdd_context.get("textures_dir_exists", True)
result = {
"success": True,
"result": {
"image_path": "//textures/normals.png",
"directory_created": not textures_exists,
"material_created": not low_poly.get("has_material", True),
"normal_map_connected": True
}
}
mock_blender_connection.set_response("bake_normals", {}, result)
bdd_context["bake_result"] = mock_blender_connection.send_command("bake_normals", {})
@then("a normal map image is created at //textures/")
def normal_map_created(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert "image_path" in data, "Should create image at path"
assert "//textures/" in data["image_path"], "Should save to textures directory"
@then(parsers.parse("the image resolution is {size:d}x{size:d}"))
def image_resolution_correct(bdd_context, size):
result = bdd_context["bake_result"]
data = result["result"]
expected_res = f"{size}x{size}"
assert data.get("map_size") == size or data.get("resolution") == expected_res, \
f"Should have resolution {expected_res}"
@then(parsers.parse("the normal map uses {space} space"))
def normal_map_space_correct(bdd_context, space):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("normal_space") == space, f"Should use {space} space"
@then(parsers.parse('the image is assigned to "{low}" material\'s normal map slot'))
def image_assigned_to_material(bdd_context, low):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("material_assigned", False), f"Should assign to {low}'s material"
@then("the file path is returned")
def file_path_returned(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert "image_path" in data, "Should return file path"
@then("the cage is used for ray-casting")
def cage_used(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("cage_used_for_raycasting", False), "Should use cage for raycasting"
@then("surface details are captured accurately")
def details_captured(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("details_captured", False), "Should capture details accurately"
@then("baking artifacts are minimized")
def artifacts_minimized(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("artifacts_minimized", False), "Should minimize artifacts"
@then("the call fails with error")
def call_fails(bdd_context):
result = bdd_context["bake_result"]
assert not result["success"], "Should fail with error"
@then("the message suggests unwrapping UVs first")
def suggests_unwrap_uvs(bdd_context):
result = bdd_context["bake_result"]
message = result.get("message", "")
suggestions = result.get("suggestions", [])
full_text = (message + " " + " ".join(suggestions)).lower()
assert "uv" in full_text or "unwrap" in full_text, \
"Should suggest unwrapping UVs"
@then("the directory is created automatically")
def directory_created(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("directory_created", False), "Should create directory automatically"
@then("the normal map is saved there")
def normal_map_saved(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert "image_path" in data, "Should save normal map"
@then("a new material is created")
def material_created(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("material_created", False), "Should create new material"
@then("the normal map node is connected to the shader")
def normal_map_connected(bdd_context):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("normal_map_connected", False), "Should connect normal map to shader"
@then(parsers.parse('the material is assigned to "{low}"'))
def material_assigned(bdd_context, low):
result = bdd_context["bake_result"]
data = result["result"]
assert data.get("material_assigned", False) or data.get("material_created", False), \
f"Should assign material to {low}"