build123d/tests/test_direct_api/test_edge.py
gumyr a52f112375
Some checks failed
benchmarks / benchmarks (macos-13, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-13, 3.10) (push) Has been cancelled
tests / tests (macos-13, 3.13) (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Improving test coverage
2025-08-31 20:11:58 -04:00

429 lines
16 KiB
Python

"""
build123d imports
name: test_edge.py
by: Gumyr
date: January 22, 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 numpy as np
import unittest
from unittest.mock import patch, PropertyMock
from build123d.build_enums import AngularDirection, GeomType, PositionMode, Transition
from build123d.geometry import Axis, Plane, Vector
from build123d.objects_curve import CenterArc, EllipticalCenterArc
from build123d.objects_sketch import Circle, Rectangle, RegularPolygon
from build123d.operations_generic import sweep
from build123d.topology import Edge, Face, Wire
from OCP.GeomProjLib import GeomProjLib
class TestEdge(unittest.TestCase):
def test_close(self):
self.assertAlmostEqual(
Edge.make_circle(1, end_angle=180).close().length, math.pi + 2, 5
)
self.assertAlmostEqual(Edge.make_circle(1).close().length, 2 * math.pi, 5)
def test_make_half_circle(self):
half_circle = Edge.make_circle(radius=1, start_angle=0, end_angle=180)
self.assertAlmostEqual(half_circle.start_point(), (1, 0, 0), 3)
self.assertAlmostEqual(half_circle.end_point(), (-1, 0, 0), 3)
def test_make_half_circle2(self):
half_circle = Edge.make_circle(radius=1, start_angle=270, end_angle=90)
self.assertAlmostEqual(half_circle.start_point(), (0, -1, 0), 3)
self.assertAlmostEqual(half_circle.end_point(), (0, 1, 0), 3)
def test_make_clockwise_half_circle(self):
half_circle = Edge.make_circle(
radius=1,
start_angle=180,
end_angle=0,
angular_direction=AngularDirection.CLOCKWISE,
)
self.assertAlmostEqual(half_circle.end_point(), (1, 0, 0), 3)
self.assertAlmostEqual(half_circle.start_point(), (-1, 0, 0), 3)
def test_make_clockwise_half_circle2(self):
half_circle = Edge.make_circle(
radius=1,
start_angle=90,
end_angle=-90,
angular_direction=AngularDirection.CLOCKWISE,
)
self.assertAlmostEqual(half_circle.start_point(), (0, 1, 0), 3)
self.assertAlmostEqual(half_circle.end_point(), (0, -1, 0), 3)
def test_arc_center(self):
self.assertAlmostEqual(Edge.make_ellipse(2, 1).arc_center, (0, 0, 0), 5)
with self.assertRaises(ValueError):
Edge.make_line((0, 0, 0), (0, 0, 1)).arc_center
def test_spline_with_parameters(self):
spline = Edge.make_spline(
points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 0.4, 1.0]
)
self.assertAlmostEqual(spline.end_point(), (2, 0, 0), 5)
with self.assertRaises(ValueError):
Edge.make_spline(
points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 1.0]
)
with self.assertRaises(ValueError):
Edge.make_spline(
points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], tangents=[(1, 1, 0)]
)
def test_spline_approx(self):
spline = Edge.make_spline_approx([(0, 0), (1, 1), (2, 1), (3, 0)])
self.assertAlmostEqual(spline.end_point(), (3, 0, 0), 5)
spline = Edge.make_spline_approx(
[(0, 0), (1, 1), (2, 1), (3, 0)], smoothing=(1.0, 5.0, 10.0)
)
self.assertAlmostEqual(spline.end_point(), (3, 0, 0), 5)
def test_distribute_locations(self):
line = Edge.make_line((0, 0, 0), (10, 0, 0))
locs = line.distribute_locations(3)
for i, x in enumerate([0, 5, 10]):
self.assertAlmostEqual(locs[i].position, (x, 0, 0), 5)
self.assertAlmostEqual(locs[0].orientation, (0, 90, 180), 5)
locs = line.distribute_locations(3, positions_only=True)
for i, x in enumerate([0, 5, 10]):
self.assertAlmostEqual(locs[i].position, (x, 0, 0), 5)
self.assertAlmostEqual(locs[0].orientation, (0, 0, 0), 5)
def test_to_wire(self):
edge = Edge.make_line((0, 0, 0), (1, 1, 1))
for end in [0, 1]:
self.assertAlmostEqual(
edge.position_at(end),
Wire(edge).position_at(end),
5,
)
def test_arc_center2(self):
edges = [
Edge.make_circle(1, plane=Plane((1, 2, 3)), end_angle=30),
Edge.make_ellipse(1, 0.5, plane=Plane((1, 2, 3)), end_angle=30),
]
for edge in edges:
self.assertAlmostEqual(edge.arc_center, (1, 2, 3), 5)
with self.assertRaises(ValueError):
Edge.make_line((0, 0), (1, 1)).arc_center
def test_find_intersection_points(self):
circle = Edge.make_circle(1)
line = Edge.make_line((0, -2), (0, 2))
crosses = circle.find_intersection_points(line)
for target, actual in zip([(0, 1, 0), (0, -1, 0)], crosses):
self.assertAlmostEqual(actual, target, 5)
with self.assertRaises(ValueError):
circle.find_intersection_points(Edge.make_line((0, 0, -1), (0, 0, 1)))
with self.assertRaises(ValueError):
circle.find_intersection_points(Edge.make_line((0, 0, -1), (0, 0, 1)))
self_intersect = Edge.make_spline([(-3, 2), (3, -2), (4, 0), (3, 2), (-3, -2)])
self.assertAlmostEqual(
self_intersect.find_intersection_points()[0],
(-2.6861636507066047, 0, 0),
5,
)
line = Edge.make_line((1, -2), (1, 2))
crosses = line.find_intersection_points(Axis.X)
self.assertAlmostEqual(crosses[0], (1, 0, 0), 5)
with self.assertRaises(ValueError):
line.find_intersection_points(Plane.YZ)
circle.wrapped = None
with self.assertRaises(ValueError):
circle.find_intersection_points(line)
# def test_intersections_tolerance(self):
# Multiple operands not currently supported
# r1 = ShapeList() + (PolarLocations(1, 4) * Edge.make_line((0, -1), (0, 1)))
# l1 = Edge.make_line((1, 0), (2, 0))
# i1 = l1.intersect(*r1)
# r2 = Rectangle(2, 2).edges()
# l2 = Pos(1) * Edge.make_line((0, 0), (1, 0))
# i2 = l2.intersect(*r2)
# self.assertEqual(len(i1.vertices()), len(i2.vertices()))
def test_trim(self):
line = Edge.make_line((-2, 0), (2, 0))
self.assertAlmostEqual(line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5)
self.assertAlmostEqual(line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5)
with self.assertRaises(ValueError):
line.trim(0.75, 0.25)
line.wrapped = None
with self.assertRaises(ValueError):
line.trim(0.1, 0.9)
def test_trim_to_length(self):
e1 = Edge.make_line((0, 0), (10, 10))
e1_trim = e1.trim_to_length(0.0, 10)
self.assertAlmostEqual(e1_trim.length, 10, 5)
e2 = Edge.make_circle(10, start_angle=0, end_angle=90)
e2_trim = e2.trim_to_length(0.5, 1)
self.assertAlmostEqual(e2_trim.length, 1, 5)
self.assertAlmostEqual(
e2_trim.position_at(0), Vector(10, 0, 0).rotate(Axis.Z, 45), 5
)
e3 = Edge.make_spline(
[(0, 10, 0), (-4, 5, 2), (0, 0, 0)], tangents=[(-1, 0), (1, 0)]
)
e3_trim = e3.trim_to_length(0, 7)
self.assertAlmostEqual(e3_trim.length, 7, 5)
a4 = Axis((0, 0, 0), (1, 1, 1))
e4_trim = Edge(a4).trim_to_length(0.5, 2)
self.assertAlmostEqual(e4_trim.length, 2, 5)
e1.wrapped = None
with self.assertRaises(ValueError):
e1.trim_to_length(0.1, 2)
def test_bezier(self):
with self.assertRaises(ValueError):
Edge.make_bezier((1, 1))
cntl_pnts = [(1, 2, 3)] * 30
with self.assertRaises(ValueError):
Edge.make_bezier(*cntl_pnts)
with self.assertRaises(ValueError):
Edge.make_bezier((0, 0, 0), (1, 1, 1), weights=[1.0])
bezier = Edge.make_bezier((0, 0), (0, 1), (1, 1), (1, 0))
bbox = bezier.bounding_box()
self.assertAlmostEqual(bbox.min, (0, 0, 0), 5)
self.assertAlmostEqual(bbox.max, (1, 0.75, 0), 5)
def test_mid_way(self):
mid = Edge.make_mid_way(
Edge.make_line((0, 0), (0, 1)), Edge.make_line((1, 0), (1, 1)), 0.25
)
self.assertAlmostEqual(mid.position_at(0), (0.25, 0, 0), 5)
self.assertAlmostEqual(mid.position_at(1), (0.25, 1, 0), 5)
def test_distribute_locations2(self):
with self.assertRaises(ValueError):
Edge.make_circle(1).distribute_locations(1)
locs = Edge.make_circle(1).distribute_locations(5, positions_only=True)
for i, loc in enumerate(locs):
self.assertAlmostEqual(
loc.position,
Vector(1, 0, 0).rotate(Axis.Z, i * 90),
5,
)
self.assertAlmostEqual(loc.orientation, (0, 0, 0), 5)
def test_find_tangent(self):
circle = Edge.make_circle(1)
parm = circle.find_tangent(135)[0]
self.assertAlmostEqual(
circle @ parm, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5
)
line = Edge.make_line((0, 0), (1, 1))
parm = line.find_tangent(45)[0]
self.assertAlmostEqual(parm, 0, 5)
parm = line.find_tangent(0)
self.assertEqual(len(parm), 0)
def test_param_at_point(self):
u = Edge.make_circle(1).param_at_point((0, 1))
self.assertAlmostEqual(u, 0.25, 5)
u = 0.3
edge = Edge.make_line((0, 0), (34, 56))
pnt = edge.position_at(u)
self.assertAlmostEqual(edge.param_at_point(pnt), u, 5)
ca = CenterArc((0, 0), 1, -200, 220).edge()
for u in [0.3, 1.0]:
pnt = ca.position_at(u)
self.assertAlmostEqual(ca.param_at_point(pnt), u, 5)
ea = EllipticalCenterArc((15, 0), 10, 5, start_angle=90, end_angle=270).edge()
for u in [0.3, 0.9]:
pnt = ea.position_at(u)
self.assertAlmostEqual(ea.param_at_point(pnt), u, 5)
with self.assertRaises(ValueError):
edge.param_at_point((-1, 1))
ea.wrapped = None
with self.assertRaises(ValueError):
ea.param_at_point((15, 5))
def test_param_at_point_bspline(self):
# Define a complex spline with inflections and non-monotonic behavior
curve = Edge.make_spline(
[
(-2, 0, 0),
(-10, 1, 0),
(0, 0, 0),
(1, -2, 0),
(2, 0, 0),
(1, 1, 0),
]
)
# Sample N points along the curve using position_at and check that
# param_at_point returns approximately the same param (inverted)
N = 20
for u in np.linspace(0.0, 1.0, N):
p = curve.position_at(u)
u_back = curve.param_at_point(p)
self.assertAlmostEqual(u, u_back, delta=1e-6, msg=f"u={u}, u_back={u_back}")
def test_conical_helix(self):
helix = Edge.make_helix(1, 4, 1, normal=(-1, 0, 0), angle=10, lefthand=True)
self.assertAlmostEqual(helix.bounding_box().min.X, -4, 5)
def test_reverse(self):
e1 = Edge.make_line((0, 0), (1, 1))
self.assertAlmostEqual(e1 @ 0.1, (0.1, 0.1, 0), 5)
self.assertAlmostEqual(e1.reversed() @ 0.1, (0.9, 0.9, 0), 5)
e2 = Edge.make_circle(1, start_angle=0, end_angle=180)
e2r = e2.reversed()
self.assertAlmostEqual((e2 @ 0.1).X, -(e2r @ 0.1).X, 5)
e2r = e2.reversed(reconstruct=True)
self.assertAlmostEqual((e2 @ 0.1).X, -(e2r @ 0.1).X, 5)
e2.wrapped = None
with self.assertRaises(ValueError):
e2.reversed()
def test_init(self):
with self.assertRaises(TypeError):
Edge(direction=(1, 0, 0))
def test_is_interior(self):
path = RegularPolygon(5, 5).face().outer_wire()
profile = path.location_at(0) * (Circle(0.6) & Rectangle(2, 1))
target = sweep(profile, path, transition=Transition.RIGHT)
inside_edges = target.edges().filter_by(lambda e: e.is_interior)
self.assertEqual(len(inside_edges), 5)
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in inside_edges))
def test_position_at(self):
line = Edge.make_line((1, 1), (2, 2))
self.assertEqual(line @ 0, Vector(1, 1, 0))
self.assertEqual(line @ 1, Vector(2, 2, 0))
self.assertEqual(line.reversed() @ 0, Vector(2, 2, 0))
self.assertEqual(line.reversed() @ 1, Vector(1, 1, 0))
self.assertEqual(
line.position_at(1, position_mode=PositionMode.LENGTH),
Vector(1, 1) + Vector(math.sqrt(2) / 2, math.sqrt(2) / 2),
)
self.assertEqual(
line.reversed().position_at(1, position_mode=PositionMode.LENGTH),
Vector(2, 2) - Vector(math.sqrt(2) / 2, math.sqrt(2) / 2),
)
def test_tangent_at(self):
arc = Edge.make_circle(1, start_angle=0, end_angle=180)
self.assertEqual(arc % 0, Vector(0, 1, 0))
self.assertEqual(arc % 1, Vector(0, -1, 0))
self.assertEqual(arc.reversed() % 0, Vector(0, 1, 0))
self.assertEqual(arc.reversed() % 1, Vector(0, -1, 0))
self.assertEqual(arc.reversed() @ 0, Vector(-1, 0, 0))
self.assertEqual(arc.reversed() @ 1, Vector(1, 0, 0))
self.assertEqual(
arc.tangent_at(math.pi, position_mode=PositionMode.LENGTH), Vector(0, -1, 0)
)
self.assertEqual(
arc.reversed().tangent_at(math.pi / 2, position_mode=PositionMode.LENGTH),
Vector(1, 0, 0),
)
def test_location_at(self):
arc = Edge.make_circle(1, start_angle=0, end_angle=180)
self.assertEqual(arc.location_at(0).position, Vector(1, 0, 0))
self.assertEqual(arc.location_at(1).position, Vector(-1, 0, 0))
self.assertEqual(arc.location_at(0).z_axis.direction, Vector(0, 1, 0))
self.assertEqual(arc.location_at(1).z_axis.direction, Vector(0, -1, 0))
self.assertEqual(arc.reversed().location_at(0).position, Vector(-1, 0, 0))
self.assertEqual(arc.reversed().location_at(1).position, Vector(1, 0, 0))
self.assertEqual(
arc.reversed().location_at(0).z_axis.direction, Vector(0, 1, 0)
)
self.assertEqual(
arc.reversed().location_at(1).z_axis.direction, Vector(0, -1, 0)
)
self.assertEqual(
arc.location_at(math.pi, position_mode=PositionMode.LENGTH).position,
Vector(-1, 0, 0),
)
self.assertEqual(
arc.reversed()
.location_at(math.pi, position_mode=PositionMode.LENGTH)
.position,
Vector(1, 0, 0),
)
def test_extend_spline(self):
geom_surface = Face.make_rect(4, 4).geom_adaptor()
with self.assertRaises(TypeError):
Edge.make_line((0, 0), (1, 0))._extend_spline(True, geom_surface)
spline = Edge.make_spline([(0, 0), (1,), (2, 0)])
spline.wrapped = None
with self.assertRaises(ValueError):
spline._extend_spline(True, geom_surface)
@patch.object(GeomProjLib, "Project_s", return_value=None)
def test_extend_spline_failed_snap(self, mock_is_valid):
geom_surface = Face.make_rect(4, 4).geom_adaptor()
spline = Edge.make_spline([(0, 0), (1, 0), (2, 0)])
with self.assertRaises(RuntimeError):
spline._extend_spline(True, geom_surface)
def test_geom_adaptor(self):
line = Edge.make_line((0, 0), (1, 0))
line.wrapped = None
with self.assertRaises(ValueError):
line.geom_adaptor()
if __name__ == "__main__":
unittest.main()