Files
autocad-mcp-pro/tests/test_meta_tools_geometry.py
T
Umutcan EdizsalanandCursor 9ec6c4a989 style: apply ruff format across the repository
ruff format was never run repo-wide; CI now enforces 'ruff format --check', so normalize every module once. No behavior changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:14:51 +03:00

128 lines
6.1 KiB
Python

"""Tests for deterministic geometry helpers: point_intersection and point_tangent."""
from __future__ import annotations
import math
import pytest
pytestmark = pytest.mark.asyncio
# ---------------------------------------------------------------------------
# point_intersection
# ---------------------------------------------------------------------------
class TestPointIntersection:
async def test_line_line_perpendicular(self, backend):
"""Horizontal and vertical lines cross at (50, 50)."""
h = await backend.entity_create_line(0, 50, 100, 50)
v = await backend.entity_create_line(50, 0, 50, 100)
pt = await backend.point_intersection(h.handle, v.handle)
assert abs(pt[0] - 50.0) < 1e-9
assert abs(pt[1] - 50.0) < 1e-9
async def test_line_line_diagonal(self, backend):
"""y=x and y=100-x intersect at (50, 50)."""
l1 = await backend.entity_create_line(0, 0, 100, 100)
l2 = await backend.entity_create_line(0, 100, 100, 0)
pt = await backend.point_intersection(l1.handle, l2.handle)
assert abs(pt[0] - 50.0) < 1e-9
assert abs(pt[1] - 50.0) < 1e-9
async def test_line_line_parallel_raises(self, backend):
"""Parallel lines raise RuntimeError."""
l1 = await backend.entity_create_line(0, 0, 100, 0)
l2 = await backend.entity_create_line(0, 10, 100, 10)
with pytest.raises(RuntimeError, match="parallel"):
await backend.point_intersection(l1.handle, l2.handle)
async def test_line_circle_two_intersections_ref_picks_nearest(self, backend):
"""Horizontal line through circle center gives two intersections; ref selects."""
circ = await backend.entity_create_circle(50, 50, 20)
line = await backend.entity_create_line(0, 50, 100, 50)
# Left intersection (~30, 50) — ref closer to x=0
left = await backend.point_intersection(circ.handle, line.handle, ref_x=0, ref_y=50)
assert abs(left[0] - 30.0) < 1e-6
# Right intersection (~70, 50) — ref closer to x=100
right = await backend.point_intersection(line.handle, circ.handle, ref_x=100, ref_y=50)
assert abs(right[0] - 70.0) < 1e-6
async def test_line_circle_tangent_single_solution(self, backend):
"""Line tangent to circle gives one solution (discriminant ≈ 0)."""
circ = await backend.entity_create_circle(50, 50, 20)
# Horizontal line at y=70 is tangent to circle (center y=50, r=20)
line = await backend.entity_create_line(0, 70, 100, 70)
pt = await backend.point_intersection(circ.handle, line.handle)
assert abs(pt[1] - 70.0) < 1e-6
assert abs(pt[0] - 50.0) < 1e-6
async def test_line_circle_no_intersection_raises(self, backend):
"""Line that misses the circle raises RuntimeError."""
circ = await backend.entity_create_circle(50, 50, 10)
line = await backend.entity_create_line(0, 200, 100, 200)
with pytest.raises(RuntimeError, match="not intersect"):
await backend.point_intersection(circ.handle, line.handle)
async def test_circle_circle_two_intersections_ref_picks_nearest(self, backend):
"""Two circles: unit radius at (0,0) and (1,0); intersect at ±(0.5, √3/2)."""
c1 = await backend.entity_create_circle(0, 0, 1)
c2 = await backend.entity_create_circle(1, 0, 1)
top = await backend.point_intersection(c1.handle, c2.handle, ref_x=0.5, ref_y=1)
assert abs(top[0] - 0.5) < 1e-6
assert abs(top[1] - math.sqrt(3) / 2.0) < 1e-6
async def test_circle_circle_no_intersection_raises(self, backend):
"""Non-overlapping circles raise RuntimeError."""
c1 = await backend.entity_create_circle(0, 0, 5)
c2 = await backend.entity_create_circle(100, 0, 5)
with pytest.raises(RuntimeError, match="not intersect"):
await backend.point_intersection(c1.handle, c2.handle)
async def test_unsupported_types_raise(self, backend):
"""ARC-ARC is not supported."""
a1 = await backend.entity_create_arc(0, 0, 10, 0, 90)
a2 = await backend.entity_create_arc(20, 0, 10, 90, 180)
with pytest.raises(RuntimeError, match="unsupported"):
await backend.point_intersection(a1.handle, a2.handle)
# ---------------------------------------------------------------------------
# point_tangent
# ---------------------------------------------------------------------------
class TestPointTangent:
async def test_tangent_from_outside(self, backend):
"""From (0, 0) to unit circle at (5, 0): tangent point is at (25/5, ±12/5)."""
circ = await backend.entity_create_circle(5, 0, 3)
# from_point (0,0), circle (5,0) r=3; d=5
# tangent_length = sqrt(25-9) = 4; cos_a = 3/5; sin_a = 4/5
pt = await backend.point_tangent(circ.handle, 0, 0, ref_x=0, ref_y=3)
dist = math.hypot(pt[0] - 5, pt[1] - 0)
assert abs(dist - 3.0) < 1e-6 # point is on circle
# Verify perpendicularity: (pt - center) · (pt - from) == 0
dot = (pt[0] - 5) * (pt[0] - 0) + (pt[1] - 0) * (pt[1] - 0)
assert abs(dot) < 1e-6
async def test_tangent_ref_selects_side(self, backend):
"""ref_y > 0 selects upper tangent, ref_y < 0 selects lower."""
circ = await backend.entity_create_circle(10, 0, 6)
upper = await backend.point_tangent(circ.handle, 0, 0, ref_x=5, ref_y=10)
lower = await backend.point_tangent(circ.handle, 0, 0, ref_x=5, ref_y=-10)
assert upper[1] > 0
assert lower[1] < 0
async def test_point_inside_circle_raises(self, backend):
"""From-point inside circle raises RuntimeError."""
circ = await backend.entity_create_circle(0, 0, 10)
with pytest.raises(RuntimeError, match="inside"):
await backend.point_tangent(circ.handle, 1, 1)
async def test_wrong_entity_type_raises(self, backend):
"""Passing a LINE handle raises RuntimeError."""
line = await backend.entity_create_line(0, 0, 100, 0)
with pytest.raises(RuntimeError, match="expected CIRCLE"):
await backend.point_tangent(line.handle, 0, 50)