"""
Test implementation for features/08_viewport_controls.feature
Tests viewport projection control and view alignment tools.
"""
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/08_viewport_controls.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("a 3D viewport is visible")
def viewport_visible(bdd_context):
bdd_context["viewport_state"] = {
"visible": True,
"projection": "PERSP",
"orientation": "USER"
}
@given("the viewport is in perspective mode")
def viewport_in_perspective(bdd_context):
bdd_context["viewport_state"] = {"projection": "PERSP"}
@given("the viewport is in orthographic mode")
def viewport_in_ortho(bdd_context):
bdd_context["viewport_state"] = {"projection": "ORTHO"}
@given("any object is active")
@given("an object is selected")
def object_active(bdd_context, mesh_factory):
bdd_context["active_object"] = mesh_factory.create_cube()
@when(parsers.parse('I call set_view_projection with projection="{projection}"'))
def call_set_view_projection(bdd_context, mock_blender_connection, projection):
result = {
"success": True,
"result": {
"projection": projection,
"previous_projection": bdd_context.get("viewport_state", {}).get("projection", "PERSP")
}
}
params = {"projection": projection}
mock_blender_connection.set_response("set_view_projection", params, result)
bdd_context["viewport_result"] = mock_blender_connection.send_command("set_view_projection", params)
bdd_context["viewport_state"]["projection"] = projection
@when(parsers.parse('I call align_view_to_axis with axis="{axis}" and side="{side}"'))
@when(parsers.parse('I call align_view_to_axis with axis="{axis}", side="{side}"'))
def call_align_view_to_axis(bdd_context, mock_blender_connection, axis, side):
# Map axis/side to view name
view_map = {
("Y", "POS"): "FRONT",
("Y", "NEG"): "BACK",
("X", "POS"): "RIGHT",
("X", "NEG"): "LEFT",
("Z", "POS"): "TOP",
("Z", "NEG"): "BOTTOM"
}
view_name = view_map.get((axis, side), "FRONT")
result = {
"success": True,
"result": {
"axis": axis,
"side": side,
"view_aligned": view_name,
"perfectly_aligned": True
}
}
params = {"axis": axis, "side": side}
mock_blender_connection.set_response("align_view_to_axis", params, result)
bdd_context["align_result"] = mock_blender_connection.send_command("align_view_to_axis", params)
@when("I call frame_selected")
def call_frame_selected(bdd_context, mock_blender_connection):
result = {
"success": True,
"result": {
"object_framed": True,
"object_centered": True,
"zoom_adjusted": True
}
}
mock_blender_connection.set_response("frame_selected", {}, result)
bdd_context["frame_result"] = mock_blender_connection.send_command("frame_selected", {})
@then("the viewport uses orthographic projection")
def viewport_uses_ortho(bdd_context):
result = bdd_context["viewport_result"]
data = result["result"]
assert data.get("projection") == "ORTHO", "Should use orthographic projection"
@then("there is no perspective foreshortening")
@then("parallel lines remain parallel")
def no_perspective(bdd_context):
state = bdd_context.get("viewport_state", {})
assert state.get("projection") == "ORTHO", "Orthographic view has no perspective"
@then("the viewport uses perspective projection")
def viewport_uses_perspective(bdd_context):
result = bdd_context["viewport_result"]
data = result["result"]
assert data.get("projection") == "PERSP", "Should use perspective projection"
@then("depth perspective is visible")
def depth_perspective_visible(bdd_context):
state = bdd_context.get("viewport_state", {})
assert state.get("projection") == "PERSP", "Perspective view has depth"
@then(parsers.parse("the view snaps to {expected_view} direction"))
def view_snaps_to_direction(bdd_context, expected_view):
result = bdd_context["align_result"]
data = result["result"]
assert data.get("view_aligned") == expected_view, \
f"Should align to {expected_view}, got {data.get('view_aligned')}"
@then("the viewport is aligned perfectly to the axis")
def viewport_aligned_to_axis(bdd_context):
result = bdd_context["align_result"]
data = result["result"]
assert data.get("perfectly_aligned", False), "View should be perfectly aligned"
@then("I have a perfect top-down orthographic view")
def perfect_top_down_ortho(bdd_context):
align_result = bdd_context.get("align_result", {})
viewport_state = bdd_context.get("viewport_state", {})
if align_result:
assert align_result["result"].get("view_aligned") == "TOP", "Should be top view"
assert viewport_state.get("projection") == "ORTHO", "Should be orthographic"
@then("the view is suitable for precision modeling")
def suitable_for_precision_modeling(bdd_context):
# Top-down ortho is ideal for precision work
state = bdd_context.get("viewport_state", {})
assert state.get("projection") == "ORTHO", "Ortho view needed for precision"
@then("the viewport centers on the selected object")
def viewport_centers_on_object(bdd_context):
result = bdd_context["frame_result"]
data = result["result"]
assert data.get("object_centered", False), "Object should be centered"
@then("the object fills the visible area appropriately")
def object_fills_area(bdd_context):
result = bdd_context["frame_result"]
data = result["result"]
assert data.get("zoom_adjusted", False), "Zoom should be adjusted to frame object"