diff --git a/pyproject.toml b/pyproject.toml index 03e3803..a25bde4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "ipython >= 8.0.0, < 10", "lib3mf >= 2.4.1", "ocpsvg >= 0.5, < 0.6", - "ocp_gordon >= 0.1.14", + "ocp_gordon >= 0.1.15", "trianglesolver", "sympy", "scipy", diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 0cd9dc8..50f5aad 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -83,11 +83,12 @@ from OCP.BRepTools import BRepTools, BRepTools_ReShape from OCP.gce import gce_MakeLin from OCP.Geom import ( Geom_BezierSurface, + Geom_BSplineCurve, Geom_RectangularTrimmedSurface, Geom_Surface, Geom_TrimmedCurve, ) -from OCP.GeomAbs import GeomAbs_C0, GeomAbs_G1, GeomAbs_G2, GeomAbs_CurveType +from OCP.GeomAbs import GeomAbs_C0, GeomAbs_CurveType, GeomAbs_G1, GeomAbs_G2 from OCP.GeomAPI import ( GeomAPI_ExtremaCurveCurve, GeomAPI_PointsToBSplineSurface, @@ -104,13 +105,17 @@ from OCP.Standard import ( Standard_NoSuchObject, ) from OCP.StdFail import StdFail_NotDone -from OCP.TColgp import TColgp_HArray2OfPnt -from OCP.TColStd import TColStd_HArray2OfReal +from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt +from OCP.TColStd import ( + TColStd_Array1OfInteger, + TColStd_Array1OfReal, + TColStd_HArray2OfReal, +) from OCP.TopExp import TopExp from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape -from typing_extensions import Self from ocp_gordon import interpolate_curve_network +from typing_extensions import Self from build123d.build_enums import ( CenterOf, @@ -922,32 +927,66 @@ class Face(Mixin2D, Shape[TopoDS_Face]): @classmethod def make_gordon_surface( cls, - profiles: Iterable[Edge], - guides: Iterable[Edge], + profiles: Iterable[VectorLike | Edge], + guides: Iterable[VectorLike | Edge], tolerance: float = 3e-4, ) -> Face: """ - Creates a Gordon surface from a network of profile and guide curves. + Constructs a Gordon surface from a network of profile and guide curves. + + Profiles and guides may consist of points or curves, but at least one + profile and one guide must be a non-point curve. Args: - profiles (Iterable[Edge]): Edges representing profile curves. - guides (Iterable[Edge]): Edges representing guide curves. - tolerance (float, optional): Tolerance for surface creation and + profiles (Iterable[VectorLike | Edge]): Profiles defined as points or edges. + guides (Iterable[VectorLike | Edge]): Guides defined as points or edges. + tolerance (float, optional): Tolerance used for surface construction and intersection calculations. Raises: - ValueError: Input edge cannot be empty + ValueError: If the input profiles or guides are empty. Returns: Face: the interpolated Gordon surface """ - def to_geom_curve(edge: Edge): - if edge.wrapped is None: - raise ValueError("input edge cannot be empty") + def create_zero_length_bspline_curve( + point: gp_Pnt, degree: int = 1 + ) -> Geom_BSplineCurve: + """ + Helper to create a simple linear B-spline curve. + """ + control_points = TColgp_Array1OfPnt(1, 2) + control_points.SetValue(1, point) + control_points.SetValue(2, point) - adaptor = BRepAdaptor_Curve(edge.wrapped) - curve = BRep_Tool.Curve_s(edge.wrapped, 0, 1) + knots = TColStd_Array1OfReal(1, 2) + knots.SetValue(1, 0.0) + knots.SetValue(2, 1.0) + + multiplicities = TColStd_Array1OfInteger(1, 2) + multiplicities.SetValue(1, degree + 1) + multiplicities.SetValue(2, degree + 1) + + curve = Geom_BSplineCurve(control_points, knots, multiplicities, degree) + return curve + + def to_geom_curve(shape: VectorLike | Edge): + if isinstance(shape, (Vector, tuple, Sequence)): + _shape = Vector(shape) + if _shape.wrapped is None: + raise ValueError("input VectorLike cannot be empty") + + single_point_curve = create_zero_length_bspline_curve( + gp_Pnt(_shape.wrapped.XYZ()) + ) + return single_point_curve + + if shape.wrapped is None: + raise ValueError("input Edge cannot be empty") + + adaptor = BRepAdaptor_Curve(shape.wrapped) + curve = BRep_Tool.Curve_s(shape.wrapped, 0, 1) if not ( (adaptor.IsPeriodic() and adaptor.IsClosed()) or adaptor.GetType() == GeomAbs_CurveType.GeomAbs_BSplineCurve @@ -958,8 +997,8 @@ class Face(Mixin2D, Shape[TopoDS_Face]): ) return curve - ocp_profiles = [to_geom_curve(edge) for edge in profiles] - ocp_guides = [to_geom_curve(edge) for edge in guides] + ocp_profiles = [to_geom_curve(shape) for shape in profiles] + ocp_guides = [to_geom_curve(shape) for shape in guides] gordon_bspline_surface = interpolate_curve_network( ocp_profiles, ocp_guides, tolerance=tolerance diff --git a/tests/test_direct_api/test_face.py b/tests/test_direct_api/test_face.py index 043c22c..518e53b 100644 --- a/tests/test_direct_api/test_face.py +++ b/tests/test_direct_api/test_face.py @@ -31,9 +31,11 @@ import os import platform import random import unittest +from unittest.mock import PropertyMock, patch -from unittest.mock import patch, PropertyMock from OCP.Geom import Geom_RectangularTrimmedSurface +from OCP.GeomAPI import GeomAPI_ExtremaCurveCurve + from build123d.build_common import Locations, PolarLocations from build123d.build_enums import Align, CenterOf, ContinuityLevel, GeomType from build123d.build_line import BuildLine @@ -57,7 +59,6 @@ from build123d.operations_generic import fillet, offset from build123d.operations_part import extrude from build123d.operations_sketch import make_face from build123d.topology import Edge, Face, Shell, Solid, Wire -from OCP.GeomAPI import GeomAPI_ExtremaCurveCurve class TestFace(unittest.TestCase): @@ -448,7 +449,7 @@ class TestFace(unittest.TestCase): profiles, guides, tolerance=tolerance ) - def test_make_gordon_surface_edge_types(self): + def test_make_gordon_surface_input_types(self): tolerance = 3e-4 def point_at_uv_against_expected(u: float, v: float, expected_point: Vector): @@ -538,6 +539,30 @@ class TestFace(unittest.TestCase): point_at_uv_against_expected(u=0.0, v=0.5, expected_point=guides[0] @ 0.5) point_at_uv_against_expected(u=1.0, v=0.5, expected_point=guides[0] @ 0.5) + profiles = [ + points[0], + ThreePointArc( + points[1], (points[1] + points[3]) / 2 + Vector(0, 0, 2), points[3] + ), + points[2], + ] + guides = [ + Spline( + points[0], + profiles[1] @ 0, + points[2], + ), + Spline( + points[0], + profiles[1] @ 1, + points[2], + ), + ] + gordon_surface = Face.make_gordon_surface(profiles, guides, tolerance=tolerance) + point_at_uv_against_expected(u=0.0, v=1.0, expected_point=guides[0] @ 1) + point_at_uv_against_expected(u=1.0, v=1.0, expected_point=guides[1] @ 1) + point_at_uv_against_expected(u=1.0, v=0.0, expected_point=points[0]) + def test_make_surface(self): corners = [Vector(x, y) for x in [-50.5, 50.5] for y in [-24.5, 24.5]] net_exterior = Wire( @@ -1232,3 +1257,4 @@ class TestAxesOfSymmetrySplitNone(unittest.TestCase): if __name__ == "__main__": unittest.main() + unittest.main()