mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 10:41:20 -08:00
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
429 lines
16 KiB
Python
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()
|