Files
pygame-physics-opus5.5/tests/interaction/test_drag_and_tools.py
T
2026-09-24 15:45:31 +02:00

227 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests für Greifen/Ziehen/Werfen und das Erzeugen von Körpern (ohne pygame)."""
import math
import random
from dataclasses import replace
import pytest
from sandbox.config import INTERACTION, MIN_BODY_SIZE, PHYSICS, TIME_STEP, Theme
from sandbox.interaction import BodyFactory, BodyStyle, DragController, ShapeKind, ToolSettings
from sandbox.physics import Box, Circle, Vector2
from tests.helpers import add_box, add_circle, make_world
PALETTE = Theme().body_palette
def _factory(**overrides: object) -> BodyFactory:
return BodyFactory(make_world(), replace(INTERACTION, **overrides), PALETTE, random.Random(1))
def _settings(**overrides: object) -> ToolSettings:
return replace(ToolSettings.from_config(INTERACTION), **overrides)
# --------------------------------------------------------------------- Werkzeug-Einstellungen
def test_circle_half_extents_are_the_radius() -> None:
assert _settings(shape=ShapeKind.CIRCLE, size=30).half_extents() == (30, 30)
def test_box_aspect_ratio_changes_shape_but_not_area() -> None:
wide = _settings(shape=ShapeKind.BOX, size=30, aspect_ratio=2.0).half_extents()
square = _settings(shape=ShapeKind.BOX, size=30, aspect_ratio=1.0).half_extents()
assert wide[0] > wide[1]
assert square == pytest.approx((30, 30))
assert wide[0] * wide[1] == pytest.approx(square[0] * square[1])
def test_box_sides_never_drop_below_the_minimum_size() -> None:
thin = _settings(shape=ShapeKind.BOX, size=MIN_BODY_SIZE, aspect_ratio=2.0).half_extents()
assert min(thin) == pytest.approx(MIN_BODY_SIZE)
# --------------------------------------------------------------------- Körper erzeugen
def test_factory_creates_circles_and_boxes_with_the_selected_material() -> None:
factory = _factory()
circle = factory.create(_settings(size=20, density=2.0, restitution=0.9), Vector2(300, 300))
assert circle is not None
assert isinstance(circle.shape, Circle)
assert circle.mass == pytest.approx(math.pi * 20**2 * 2.0)
assert circle.material.restitution == 0.9
assert circle.material.friction == INTERACTION.friction
box = factory.create(_settings(shape=ShapeKind.BOX, size=20), Vector2(500, 300))
assert box is not None
assert isinstance(box.shape, Box)
assert factory.world.bodies == [circle, box]
def test_factory_moves_new_bodies_completely_into_the_world() -> None:
factory = _factory()
body = factory.create(_settings(size=30), Vector2(-100, 5000))
assert body is not None
assert body.position == Vector2(30, factory.world.height - 30)
def test_factory_respects_the_body_limit() -> None:
factory = _factory(max_bodies=2)
settings = _settings()
assert factory.create(settings, Vector2(100, 100)) is not None
assert factory.create(settings, Vector2(200, 100)) is not None
assert factory.is_full
assert factory.create(settings, Vector2(300, 100)) is None
assert len(factory.world.bodies) == 2
def test_preview_color_is_the_color_of_the_next_body() -> None:
factory = _factory()
settings = _settings(color_index=None)
for _ in range(10):
announced = factory.color_for(settings)
body = factory.create(settings, Vector2(300, 300))
assert body is not None
assert body.user_data == BodyStyle(announced)
def test_selected_palette_color_is_used() -> None:
factory = _factory()
body = factory.create(_settings(color_index=3), Vector2(300, 300))
assert body is not None
assert body.user_data == BodyStyle(PALETTE[3])
# --------------------------------------------------------------------- Ziehen und Werfen
def test_grab_marks_the_body_as_held_and_keeps_the_grip_offset() -> None:
world = make_world()
body = add_circle(world, 300, 300, 20)
body.put_to_sleep()
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(310, 295), time=0.0)
assert drag.body is body
assert body.held
assert not body.sleeping
drag.move(Vector2(410, 295), time=0.1)
assert drag.target == Vector2(400, 300) # Griffversatz (−10, +5) bleibt erhalten
def test_target_is_clamped_so_the_body_fits_into_the_world() -> None:
world = make_world()
body = add_circle(world, 300, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(300, 300), time=0.0)
drag.move(Vector2(-500, -500), time=0.1)
assert drag.target == Vector2(20, 20)
def test_held_body_follows_the_pointer_through_the_physics() -> None:
world = make_world()
body = add_circle(world, 300, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(300, 300), time=0.0)
drag.move(Vector2(450, 250), time=0.0)
for _ in range(60):
drag.apply(TIME_STEP)
world.step(TIME_STEP)
assert body.position.is_close(Vector2(450, 250), tolerance=0.5)
def test_held_body_speed_is_limited() -> None:
world = make_world()
body = add_circle(world, 30, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(30, 300), time=0.0)
drag.move(Vector2(980, 300), time=0.0)
drag.apply(TIME_STEP)
assert body.velocity.length() == pytest.approx(PHYSICS.max_speed)
def test_held_body_still_collides_and_pushes_other_bodies() -> None:
world = make_world()
obstacle = add_box(world, 500, 680, 20, 20)
held = add_box(world, 300, 680, 20, 20)
drag = DragController(world, INTERACTION)
drag.grab(held, Vector2(300, 680), time=0.0)
drag.move(Vector2(700, 680), time=0.0)
for _ in range(120):
drag.apply(TIME_STEP)
world.step(TIME_STEP)
assert obstacle.position.x > 520 # wurde weggeschoben
assert obstacle.position.x - held.position.x >= 40 - PHYSICS.penetration_slop - 1e-3
def test_held_body_pressed_against_an_obstacle_does_not_jitter() -> None:
"""Ein gegen ein blockiertes Hindernis gedrückter, gehaltener Körper steht ruhig.
Würde er abprallen, zöge ihn die Maus im nächsten Schritt wieder heran – er zitterte.
"""
world = make_world()
add_box(world, 1000 - 20, 680, 20, 20, restitution=1.0) # liegt an der rechten Wand
held = add_box(world, 700, 680, 20, 20, restitution=1.0)
drag = DragController(world, INTERACTION)
drag.grab(held, Vector2(700, 680), time=0.0)
drag.move(Vector2(990, 680), time=0.0) # Ziel liegt hinter dem Hindernis
positions = []
for step in range(180):
drag.apply(TIME_STEP)
world.step(TIME_STEP)
if step >= 120:
positions.append(held.position.x)
assert max(positions) - min(positions) < 0.1
# Wand → Hindernis → gehaltener Körper: Jeder der beiden Kontakte behält höchstens den
# Slop als Resttiefe, der Körper steht also bei x ≤ 1000 − 40 − 20 + 2·Slop.
assert max(positions) <= 1000 - 40 - 20 + 2 * PHYSICS.penetration_slop + 1e-3
def test_release_throws_with_the_smoothed_pointer_velocity() -> None:
world = make_world(gravity=(0.0, 0.0))
body = add_circle(world, 200, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(200, 300), time=0.0)
for frame in range(1, 31):
t = frame / 60
drag.move(Vector2(200 + 600 * t, 300 - 150 * t), time=t)
assert drag.release(paused=False) is body
assert body.velocity.is_close(Vector2(600, -150), tolerance=5.0)
assert not body.held
assert drag.body is None
def test_throw_speed_is_limited() -> None:
world = make_world()
body = add_circle(world, 200, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(200, 300), time=0.0)
for frame in range(1, 11):
drag.move(Vector2(200 + 100 * frame, 300), time=frame / 120) # 12 000 px/s
drag.release(paused=False)
assert body.velocity.length() == pytest.approx(INTERACTION.max_throw_speed)
def test_release_while_paused_does_not_throw() -> None:
world = make_world()
body = add_circle(world, 200, 300, 20)
drag = DragController(world, INTERACTION)
drag.grab(body, Vector2(200, 300), time=0.0)
drag.move(Vector2(260, 300), time=0.05)
drag.place()
assert body.position == Vector2(260, 300)
drag.release(paused=True)
assert body.velocity == Vector2(0, 0)
def test_cancel_and_release_without_body_are_harmless() -> None:
world = make_world()
drag = DragController(world, INTERACTION)
drag.cancel()
drag.apply(TIME_STEP)
drag.place()
drag.move(Vector2(1, 1), time=0.0)
assert drag.release(paused=False) is None
assert drag.throw_velocity == Vector2(0, 0)