From a52f112375766719452c6e2b9cbfea3777e1eba3 Mon Sep 17 00:00:00 2001 From: gumyr Date: Sun, 31 Aug 2025 20:11:58 -0400 Subject: [PATCH] Improving test coverage --- src/build123d/topology/one_d.py | 33 +++------ tests/test_direct_api/test_edge.py | 81 ++++---------------- tests/test_direct_api/test_wire.py | 114 ++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 90 deletions(-) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index c10b037..286c311 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -152,6 +152,7 @@ from OCP.TopoDS import ( TopoDS_Face, TopoDS_Shape, TopoDS_Shell, + TopoDS_Vertex, TopoDS_Wire, ) from OCP.gp import ( @@ -803,19 +804,6 @@ class Mixin1D(Shape): if side != Side.BOTH: # Find and remove the end arcs - # offset_edges = offset_wire.edges() - # edges_to_keep: list[list[Edge]] = [[], [], []] - # i = 0 - # for edge in offset_edges: - # if edge.geom_type == GeomType.CIRCLE and ( - # edge.arc_center == line.position_at(0) - # or edge.arc_center == line.position_at(1) - # ): - # i += 1 - # else: - # edges_to_keep[i].append(edge) - # edges_to_keep[0] += edges_to_keep[2] - # wires = [Wire(edges) for edges in edges_to_keep[0:2]] endpoints = (line.position_at(0), line.position_at(1)) offset_edges = offset_wire.edges().filter_by( lambda e: ( @@ -826,8 +814,6 @@ class Mixin1D(Shape): ) wires = edges_to_wires(offset_edges) centers = [w.position_at(0.5) for w in wires] - tangent = line.tangent_at(0) - start = line.position_at(0) angles = [ line.tangent_at(0).get_signed_angle(c - line.position_at(0)) for c in centers @@ -2619,7 +2605,6 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): edge, label, color, parent = args[:4] + (None,) * (4 - l_a) elif isinstance(args[0], Wire): wire, label, color, parent = args[:4] + (None,) * (4 - l_a) - # elif isinstance(args[0], Curve): elif ( hasattr(args[0], "wrapped") and isinstance(args[0].wrapped, TopoDS_Compound) @@ -3202,14 +3187,14 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): raise ValueError("Can't find point on empty wire") point_on_curve = Vector(point) + vertex_on_curve = Vertex(point_on_curve) + assert vertex_on_curve.wrapped is not None separation = self.distance_to(point) if not isclose_b(separation, 0, abs_tol=TOLERANCE): raise ValueError(f"point ({point}) is {separation} from wire") - extrema = BRepExtrema_DistShapeShape( - Vertex(point_on_curve).wrapped, self.wrapped - ) + extrema = BRepExtrema_DistShapeShape(vertex_on_curve.wrapped, self.wrapped) extrema.Perform() if not extrema.IsDone() or extrema.NbSolution() == 0: raise ValueError("point is not on Wire") @@ -3217,15 +3202,19 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): supp_type = extrema.SupportTypeShape2(1) if supp_type == BRepExtrema_SupportType.BRepExtrema_IsOnEdge: - closest_topods_edge = downcast(extrema.SupportOnShape2(1)) + closest_topods_edge = tcast( + TopoDS_Edge, downcast(extrema.SupportOnShape2(1)) + ) closest_topods_edge_param = extrema.ParOnEdgeS2(1)[0] elif supp_type == BRepExtrema_SupportType.BRepExtrema_IsVertex: - v_hit = downcast(extrema.SupportOnShape2(1)) + v_hit = tcast(TopoDS_Vertex, downcast(extrema.SupportOnShape2(1))) vertex_edge_map = TopTools_IndexedDataMapOfShapeListOfShape() TopExp.MapShapesAndAncestors_s( self.wrapped, ta.TopAbs_VERTEX, ta.TopAbs_EDGE, vertex_edge_map ) - closest_topods_edge = downcast(vertex_edge_map.FindFromKey(v_hit).First()) + closest_topods_edge = tcast( + TopoDS_Edge, downcast(vertex_edge_map.FindFromKey(v_hit).First()) + ) closest_topods_edge_param = BRep_Tool.Parameter_s( v_hit, closest_topods_edge ) diff --git a/tests/test_direct_api/test_edge.py b/tests/test_direct_api/test_edge.py index 9ebe011..fb60a7d 100644 --- a/tests/test_direct_api/test_edge.py +++ b/tests/test_direct_api/test_edge.py @@ -32,7 +32,6 @@ import unittest from unittest.mock import patch, PropertyMock -from build123d.topology.shape_core import TOLERANCE from build123d.build_enums import AngularDirection, GeomType, PositionMode, Transition from build123d.geometry import Axis, Plane, Vector from build123d.objects_curve import CenterArc, EllipticalCenterArc @@ -187,6 +186,10 @@ class TestEdge(unittest.TestCase): 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)) @@ -210,6 +213,10 @@ class TestEdge(unittest.TestCase): 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)) @@ -278,6 +285,10 @@ class TestEdge(unittest.TestCase): 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( @@ -315,6 +326,10 @@ class TestEdge(unittest.TestCase): 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)) @@ -410,69 +425,5 @@ class TestEdge(unittest.TestCase): line.geom_adaptor() -class TestWireToBSpline(unittest.TestCase): - def setUp(self): - # A simple rectilinear, multi-segment wire: - # p0 ── p1 - # │ - # p2 ── p3 - self.p0 = Vector(0, 0, 0) - self.p1 = Vector(20, 0, 0) - self.p2 = Vector(20, 10, 0) - self.p3 = Vector(35, 10, 0) - - e01 = Edge.make_line(self.p0, self.p1) - e12 = Edge.make_line(self.p1, self.p2) - e23 = Edge.make_line(self.p2, self.p3) - - self.wire = Wire([e01, e12, e23]) - - def test_to_bspline_basic_properties(self): - bs = self.wire._to_bspline() - - # 1) Type/geom check - self.assertIsInstance(bs, Edge) - self.assertEqual(bs.geom_type, GeomType.BSPLINE) - - # 2) Endpoint preservation - self.assertLess((Vector(bs.vertices()[0]) - self.p0).length, TOLERANCE) - self.assertLess((Vector(bs.vertices()[-1]) - self.p3).length, TOLERANCE) - - # 3) Length preservation (within numerical tolerance) - self.assertAlmostEqual(bs.length, self.wire.length, delta=1e-6) - - # 4) Topology collapse: single edge has only 2 vertices (start/end) - self.assertEqual(len(bs.vertices()), 2) - - # 5) The composite BSpline should pass through former junctions - for junction in (self.p1, self.p2): - self.assertLess(bs.distance_to(junction), 1e-6) - - # 6) Normalized parameter increases along former junctions - u_p1 = bs.param_at_point(self.p1) - u_p2 = bs.param_at_point(self.p2) - self.assertGreater(u_p1, 0.0) - self.assertLess(u_p2, 1.0) - self.assertLess(u_p1, u_p2) - - # 7) Re-evaluating at those parameters should be close to the junctions - self.assertLess((bs.position_at(u_p1) - self.p1).length, 1e-6) - self.assertLess((bs.position_at(u_p2) - self.p2).length, 1e-6) - - def test_to_bspline_orientation(self): - # Ensure the BSpline follows the wire's topological order - bs = self.wire._to_bspline() - - # Start ~ p0, end ~ p3 - self.assertLess((bs.position_at(0.0) - self.p0).length, 1e-6) - self.assertLess((bs.position_at(1.0) - self.p3).length, 1e-6) - - # Parameters at interior points should sit between 0 and 1 - u0 = bs.param_at_point(self.p1) - u1 = bs.param_at_point(self.p2) - self.assertTrue(0.0 < u0 < 1.0) - self.assertTrue(0.0 < u1 < 1.0) - - if __name__ == "__main__": unittest.main() diff --git a/tests/test_direct_api/test_wire.py b/tests/test_direct_api/test_wire.py index f4cf622..cbb9449 100644 --- a/tests/test_direct_api/test_wire.py +++ b/tests/test_direct_api/test_wire.py @@ -31,13 +31,16 @@ import random import unittest import numpy as np -from build123d.build_enums import Side +from build123d.topology.shape_core import TOLERANCE + +from build123d.build_enums import GeomType, Side from build123d.build_line import BuildLine from build123d.geometry import Axis, Color, Location, Plane, Vector -from build123d.objects_curve import Line, PolarLine, Polyline, Spline +from build123d.objects_curve import Curve, Line, PolarLine, Polyline, Spline from build123d.objects_sketch import Circle, Rectangle, RegularPolygon from build123d.operations_generic import fillet from build123d.topology import Edge, Face, Wire +from OCP.BRepAdaptor import BRepAdaptor_CompCurve class TestWire(unittest.TestCase): @@ -64,6 +67,9 @@ class TestWire(unittest.TestCase): self.assertAlmostEqual( squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5 ) + square.wrapped = None + with self.assertRaises(ValueError): + square.fillet_2d(0.1, square.vertices()) def test_chamfer_2d(self): square = Wire.make_rect(1, 1) @@ -71,6 +77,18 @@ class TestWire(unittest.TestCase): self.assertAlmostEqual( squaroid.length, 4 * (1 - 2 * 0.1 + 0.1 * math.sqrt(2)), 5 ) + verts = square.vertices() + verts[0].wrapped = None + three_corners = square.chamfer_2d(0.1, 0.1, verts) + self.assertEqual(len(three_corners.edges()), 7) + + square.wrapped = None + with self.assertRaises(ValueError): + square.chamfer_2d(0.1, 0.1, square.vertices()) + + def test_close(self): + t = Polyline((0, 0), (1, 0), (0, 1), close=True) + self.assertIs(t, t.close()) def test_chamfer_2d_edge(self): square = Wire.make_rect(1, 1) @@ -98,7 +116,15 @@ class TestWire(unittest.TestCase): hull_wire = Wire.make_convex_hull(adjoining_edges) self.assertAlmostEqual(Face(hull_wire).area, 319.9612, 4) - # def test_fix_degenerate_edges(self): + def test_fix_degenerate_edges(self): + e0 = Edge.make_line((0, 0), (1, 0)) + e1 = Edge.make_line((2, 0), (1, 0)) + + w = Wire([e0, e1]) + w.wrapped = None + with self.assertRaises(ValueError): + w.fix_degenerate_edges(0.1) + # # Can't find a way to create one # edge0 = Edge.make_line((0, 0, 0), (1, 0, 0)) # edge1 = Edge.make_line(edge0 @ 0, edge0 @ 0 + Vector(0, 1, 0)) @@ -175,6 +201,10 @@ class TestWire(unittest.TestCase): with self.assertRaises(ValueError): w1.param_at_point((20, 20, 20)) + w1.wrapped = None + with self.assertRaises(ValueError): + w1.param_at_point((0, 0)) + def test_param_at_point_reversed_edges(self): with BuildLine(Plane.YZ) as wing_line: l1 = Line((0, 65), (80 / 2 + 1.526 * 4, 65)) @@ -216,6 +246,13 @@ class TestWire(unittest.TestCase): self.assertAlmostEqual(ordered_edges[1] @ 0, (1, 0, 0), 5) self.assertAlmostEqual(ordered_edges[2] @ 0, (1, 1, 0), 5) + def test_geom_adaptor(self): + w = Polyline((0, 0), (1, 0), (1, 1)) + self.assertTrue(isinstance(w.geom_adaptor(), BRepAdaptor_CompCurve)) + w.wrapped = None + with self.assertRaises(ValueError): + w.geom_adaptor() + def test_constructor(self): e0 = Edge.make_line((0, 0), (1, 0)) e1 = Edge.make_line((1, 0), (1, 1)) @@ -241,9 +278,80 @@ class TestWire(unittest.TestCase): c0 = Polyline((0, 0), (1, 0), (1, 1)) w8 = Wire(c0) self.assertTrue(w8.is_valid) + w9 = Wire(Curve([e0, e1])) + self.assertTrue(w9.is_valid) with self.assertRaises(ValueError): Wire(bob="fred") +class TestWireToBSpline(unittest.TestCase): + def setUp(self): + # A simple rectilinear, multi-segment wire: + # p0 ── p1 + # │ + # p2 ── p3 + self.p0 = Vector(0, 0, 0) + self.p1 = Vector(20, 0, 0) + self.p2 = Vector(20, 10, 0) + self.p3 = Vector(35, 10, 0) + + e01 = Edge.make_line(self.p0, self.p1) + e12 = Edge.make_line(self.p1, self.p2) + e23 = Edge.make_line(self.p2, self.p3) + + self.wire = Wire([e01, e12, e23]) + + def test_to_bspline_basic_properties(self): + bs = self.wire._to_bspline() + + # 1) Type/geom check + self.assertIsInstance(bs, Edge) + self.assertEqual(bs.geom_type, GeomType.BSPLINE) + + # 2) Endpoint preservation + self.assertLess((Vector(bs.vertices()[0]) - self.p0).length, TOLERANCE) + self.assertLess((Vector(bs.vertices()[-1]) - self.p3).length, TOLERANCE) + + # 3) Length preservation (within numerical tolerance) + self.assertAlmostEqual(bs.length, self.wire.length, delta=1e-6) + + # 4) Topology collapse: single edge has only 2 vertices (start/end) + self.assertEqual(len(bs.vertices()), 2) + + # 5) The composite BSpline should pass through former junctions + for junction in (self.p1, self.p2): + self.assertLess(bs.distance_to(junction), 1e-6) + + # 6) Normalized parameter increases along former junctions + u_p1 = bs.param_at_point(self.p1) + u_p2 = bs.param_at_point(self.p2) + self.assertGreater(u_p1, 0.0) + self.assertLess(u_p2, 1.0) + self.assertLess(u_p1, u_p2) + + # 7) Re-evaluating at those parameters should be close to the junctions + self.assertLess((bs.position_at(u_p1) - self.p1).length, 1e-6) + self.assertLess((bs.position_at(u_p2) - self.p2).length, 1e-6) + + w = self.wire + w.wrapped = None + with self.assertRaises(ValueError): + w._to_bspline() + + def test_to_bspline_orientation(self): + # Ensure the BSpline follows the wire's topological order + bs = self.wire._to_bspline() + + # Start ~ p0, end ~ p3 + self.assertLess((bs.position_at(0.0) - self.p0).length, 1e-6) + self.assertLess((bs.position_at(1.0) - self.p3).length, 1e-6) + + # Parameters at interior points should sit between 0 and 1 + u0 = bs.param_at_point(self.p1) + u1 = bs.param_at_point(self.p2) + self.assertTrue(0.0 < u0 < 1.0) + self.assertTrue(0.0 < u1 < 1.0) + + if __name__ == "__main__": unittest.main()