mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-05 18:20:46 -08:00
267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
"""
|
|
build123d tests
|
|
|
|
name: test_constrained_lines.py
|
|
by: Gumyr
|
|
date: October 8, 2025
|
|
|
|
desc:
|
|
This python module contains tests for the build123d project.
|
|
|
|
license:
|
|
|
|
Copyright 2025 Gumyr
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
"""
|
|
|
|
import math
|
|
import pytest
|
|
from OCP.gp import gp_Pnt2d, gp_Dir2d, gp_Lin2d
|
|
from build123d import Edge, Axis, Vector, Tangency, Plane
|
|
from build123d.topology.constrained_lines import (
|
|
_make_2tan_lines,
|
|
_make_tan_oriented_lines,
|
|
_edge_from_line,
|
|
)
|
|
from build123d.geometry import TOLERANCE
|
|
|
|
|
|
@pytest.fixture
|
|
def unit_circle() -> Edge:
|
|
"""A simple unit circle centered at the origin on XY."""
|
|
return Edge.make_circle(1.0, Plane.XY)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# utility tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_edge_from_line():
|
|
line = _edge_from_line(gp_Pnt2d(0, 0), gp_Pnt2d(1, 0))
|
|
assert Edge(line).length == 1
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
_edge_from_line(gp_Pnt2d(0, 0), gp_Pnt2d(0, 0))
|
|
assert "Failed to build edge from line contacts" in str(excinfo.value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _make_2tan_lines tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_two_circles_tangents(unit_circle):
|
|
"""Tangent lines between two separated circles should yield four results."""
|
|
c1 = unit_circle
|
|
c2 = unit_circle.translate((3, 0, 0)) # displaced along X
|
|
lines = _make_2tan_lines(c1, c2, edge_factory=Edge)
|
|
# There should be 4 external/internal tangents
|
|
assert len(lines) in (4, 2)
|
|
for ln in lines:
|
|
assert isinstance(ln, Edge)
|
|
# Tangent lines should not intersect the circle interior
|
|
dmin = c1.distance_to(ln)
|
|
assert dmin >= -1e-6
|
|
|
|
|
|
def test_two_constrained_circles_tangents1(unit_circle):
|
|
"""Tangent lines between two separated circles should yield four results."""
|
|
c1 = unit_circle
|
|
c2 = unit_circle.translate((3, 0, 0)) # displaced along X
|
|
lines = _make_2tan_lines((c1, Tangency.ENCLOSING), c2, edge_factory=Edge)
|
|
# There should be 2 external/internal tangents
|
|
assert len(lines) == 2
|
|
for ln in lines:
|
|
assert isinstance(ln, Edge)
|
|
# Tangent lines should not intersect the circle interior
|
|
dmin = c1.distance_to(ln)
|
|
assert dmin >= -1e-6
|
|
|
|
|
|
def test_two_constrained_circles_tangents2(unit_circle):
|
|
"""Tangent lines between two separated circles should yield four results."""
|
|
c1 = unit_circle
|
|
c2 = unit_circle.translate((3, 0, 0)) # displaced along X
|
|
lines = _make_2tan_lines(
|
|
(c1, Tangency.ENCLOSING), (c2, Tangency.ENCLOSING), edge_factory=Edge
|
|
)
|
|
# There should be 1 external/external tangents
|
|
assert len(lines) == 1
|
|
for ln in lines:
|
|
assert isinstance(ln, Edge)
|
|
# Tangent lines should not intersect the circle interior
|
|
dmin = c1.distance_to(ln)
|
|
assert dmin >= -1e-6
|
|
|
|
|
|
def test_curve_and_point_tangent(unit_circle):
|
|
"""A line tangent to a circle and passing through a point should exist."""
|
|
pt = Vector(2.0, 0.0)
|
|
lines = _make_2tan_lines(unit_circle, pt, edge_factory=Edge)
|
|
assert len(lines) == 2
|
|
for ln in lines:
|
|
# The line must pass through the given point (approximately)
|
|
dist_to_point = ln.distance_to(pt)
|
|
assert math.isclose(dist_to_point, 0.0, abs_tol=1e-6)
|
|
# It should also touch the circle at exactly one point
|
|
dist_to_circle = unit_circle.distance_to(ln)
|
|
assert math.isclose(dist_to_circle, 0.0, abs_tol=TOLERANCE)
|
|
|
|
|
|
def test_invalid_tangent_raises(unit_circle):
|
|
"""Non-intersecting degenerate input result in no output."""
|
|
lines = _make_2tan_lines(unit_circle, unit_circle, edge_factory=Edge)
|
|
assert len(lines) == 0
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
_make_2tan_lines(unit_circle, Vector(0, 0), edge_factory=Edge)
|
|
assert "Unable to find common tangent line(s)" in str(excinfo.value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _make_tan_oriented_lines tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("angle_deg", [math.radians(30), -math.radians(30)])
|
|
def test_oriented_tangents_with_x_axis(unit_circle, angle_deg):
|
|
"""Lines tangent to a circle at ±30° from the X-axis."""
|
|
lines = _make_tan_oriented_lines(unit_circle, Axis.X, angle_deg, edge_factory=Edge)
|
|
assert all(isinstance(e, Edge) for e in lines)
|
|
# The tangent lines should all intersect the X axis (red line)
|
|
for ln in lines:
|
|
p = ln.position_at(0.5)
|
|
assert abs(p.Z) < 1e-9
|
|
|
|
lines = _make_tan_oriented_lines(unit_circle, Axis.X, 0, edge_factory=Edge)
|
|
assert len(lines) == 0
|
|
|
|
lines = _make_tan_oriented_lines(
|
|
unit_circle, Axis((0, -2), (1, 0)), 0, edge_factory=Edge
|
|
)
|
|
assert len(lines) == 0
|
|
|
|
|
|
def test_oriented_tangents_with_y_axis(unit_circle):
|
|
"""Lines tangent to a circle and 30° from Y-axis should exist."""
|
|
angle = math.radians(30)
|
|
lines = _make_tan_oriented_lines(unit_circle, Axis.Y, angle, edge_factory=Edge)
|
|
assert len(lines) >= 1
|
|
# They should roughly touch the circle (tangent distance ≈ 0)
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_oriented_constrained_tangents_with_y_axis(unit_circle):
|
|
angle = math.radians(30)
|
|
lines = _make_tan_oriented_lines(
|
|
(unit_circle, Tangency.ENCLOSING), Axis.Y, angle, edge_factory=Edge
|
|
)
|
|
assert len(lines) == 1
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_invalid_oriented_tangent_raises(unit_circle):
|
|
"""Non-intersecting degenerate input result in no output."""
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
_make_tan_oriented_lines(unit_circle, Axis.Z, 1, edge_factory=Edge)
|
|
assert "reference Axis can't be perpendicular to Plane.XY" in str(excinfo.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
_make_tan_oriented_lines(
|
|
unit_circle, Axis((1, 2, 3), (0, 0, -1)), 1, edge_factory=Edge
|
|
)
|
|
assert "reference Axis can't be perpendicular to Plane.XY" in str(excinfo.value)
|
|
|
|
|
|
def test_invalid_oriented_tangent(unit_circle):
|
|
lines = _make_tan_oriented_lines(
|
|
unit_circle, Axis((1, 0), (0, 1)), 0, edge_factory=Edge
|
|
)
|
|
assert len(lines) == 0
|
|
|
|
lines = _make_tan_oriented_lines(
|
|
unit_circle.translate((0, 1 + 1e-7)), Axis.X, 0, edge_factory=Edge
|
|
)
|
|
assert len(lines) == 0
|
|
|
|
|
|
def test_make_constrained_lines0(unit_circle):
|
|
lines = Edge.make_constrained_lines(unit_circle, unit_circle.translate((3, 0, 0)))
|
|
assert len(lines) == 4
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines1(unit_circle):
|
|
lines = Edge.make_constrained_lines(unit_circle, (3, 0))
|
|
assert len(lines) == 2
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines3(unit_circle):
|
|
lines = Edge.make_constrained_lines(unit_circle, Axis.X, angle=30)
|
|
assert len(lines) == 2
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
assert abs((ln @ 1).Y) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines4(unit_circle):
|
|
lines = Edge.make_constrained_lines(unit_circle, Axis.Y, angle=30)
|
|
assert len(lines) == 2
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
assert abs((ln @ 1).X) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines5(unit_circle):
|
|
lines = Edge.make_constrained_lines(
|
|
(unit_circle, Tangency.ENCLOSING), Axis.Y, angle=30
|
|
)
|
|
assert len(lines) == 1
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines6(unit_circle):
|
|
lines = Edge.make_constrained_lines(
|
|
(unit_circle, Tangency.ENCLOSING), Axis.Y, direction=(1, 1)
|
|
)
|
|
assert len(lines) == 1
|
|
for ln in lines:
|
|
assert unit_circle.distance_to(ln) < 1e-6
|
|
|
|
|
|
def test_make_constrained_lines_raises(unit_circle):
|
|
with pytest.raises(TypeError) as excinfo:
|
|
Edge.make_constrained_lines(unit_circle, Axis.Z, ref_angle=1)
|
|
assert "Unexpected argument(s): ref_angle" in str(excinfo.value)
|
|
|
|
with pytest.raises(TypeError) as excinfo:
|
|
Edge.make_constrained_lines(unit_circle)
|
|
assert "Provide exactly 2 tangency targets." in str(excinfo.value)
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
Edge.make_constrained_lines(Axis.X, Axis.Y)
|
|
assert "Unable to find common tangent line(s)" in str(excinfo.value)
|
|
|
|
with pytest.raises(TypeError) as excinfo:
|
|
Edge.make_constrained_lines(unit_circle, ("three", 0))
|
|
assert "Invalid tangency:" in str(excinfo.value)
|