From fefcada600502b2ef1fbee4bce8fe85c05b7d119 Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 2 May 2025 17:05:34 +0200 Subject: [PATCH 01/27] Add more trimming possibilities --- cadquery/occ_impl/shapes.py | 82 ++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 040712235..134c14148 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -59,7 +59,7 @@ ) # Array of points (used for B-spline construction): -from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt +from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt, TColgp_HArray1OfPnt2d # Array of vectors (used for B-spline interpolation): from OCP.TColgp import TColgp_Array1OfVec @@ -162,6 +162,8 @@ ) from OCP.Geom2d import Geom2d_Line +from OCP.Geom2dAPI import Geom2dAPI_Interpolate + from OCP.BRepLib import BRepLib, BRepLib_FindSurface from OCP.BRepOffsetAPI import ( @@ -3489,9 +3491,10 @@ def toArcs(self, tolerance: float = 1e-3) -> "Face": return self.__class__(BRepAlgo.ConvertFace_s(self.wrapped, tolerance)) - def trim(self, u0: Real, u1: Real, v0: Real, v1: Real, tol: Real = 1e-6) -> "Face": + @multimethod + def trim(self, u0: Real, u1: Real, v0: Real, v1: Real, tol: Real = 1e-6) -> Self: """ - Trim the face in the parametric space to (u0, u1). + Trim the face in the (u,v) space to (u0, u1)x(v1, v2). NB: this operation is done on the base geometry. """ @@ -3500,6 +3503,46 @@ def trim(self, u0: Real, u1: Real, v0: Real, v1: Real, tol: Real = 1e-6) -> "Fac return self.__class__(bldr.Shape()) + @trim.register + def _( + self, + pt1: Tuple[Real, Real], + pt2: Tuple[Real, Real], + pt3: Tuple[Real, Real], + *pts: Tuple[Real, Real], + ) -> Self: + """ + Trim the face using a polyline defined in the (u,v) space. + """ + + segs_uv = [] + geom = self._geomAdaptor() + + # build (u,v) segments + for el1, el2 in zip((pt1, pt2, pt3, *pts), (pt2, pt3, *pts, pt1)): + segs_uv.append(GCE2d_MakeSegment(gp_Pnt2d(*el1), gp_Pnt2d(*el2)).Value()) + + # convert to edges + edges = [] + + for seg in segs_uv: + edges.append(BRepBuilderAPI_MakeEdge(seg, geom).Edge()) + + # convert to a wire + builder = BRepBuilderAPI_MakeWire() + + tmp = TopTools_ListOfShape() + for edge in edges: + tmp.Append(edge) + + builder.Add(tmp) + + w = builder.Wire() + BRepLib.BuildCurves3d_s(w) + + # construct the final trimmed face + return self.constructOn(self, Wire(w)) + def isoline(self, param: Real, direction: Literal["u", "v"] = "v") -> Edge: """ Construct an isoline. @@ -4833,7 +4876,7 @@ def _get_wires(s: Shape) -> Iterable[Shape]: def _get_edges(s: Shape) -> Iterable[Shape]: """ - Get wires or wires from edges. + Get edges or edges from wires. """ t = s.ShapeType() @@ -4973,7 +5016,7 @@ def _compound_or_shape(s: Union[TopoDS_Shape, List[TopoDS_Shape]]) -> Shape: def _pts_to_harray(pts: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: """ - Convert a sequence of Vecotor to a TColgp harray (OCCT specific). + Convert a sequence of Vector to a TColgp harray (OCCT specific). """ rv = TColgp_HArray1OfPnt(1, len(pts)) @@ -4983,6 +5026,17 @@ def _pts_to_harray(pts: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: return rv +def _pts_to_harray2d(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: + """ + Convert a sequence of 2d points to a TColgp harray (OCCT specific). + """ + + rv = TColgp_HArray1OfPnt2d(1, len(pts)) + + for i, p in enumerate(pts): + rv.SetValue(i + 1, gp_Pnt2d(*p)) + + return rv def _floats_to_harray(vals: Sequence[float]) -> TColStd_HArray1OfReal: """ @@ -5083,6 +5137,24 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS #%% alternative constructors +def edge(pts: Sequence[Tuple[Real, Real]], base: Shape, periodic: bool = False, tol: float = 1e-6) -> Shape: + """ + Build an edge on a face from points in (u,v) space. + """ + + f = _get_one(base, "Face") + + # interpolate the u,v points + spline_bldr = Geom2dAPI_Interpolate(_pts_to_harray2d(pts), periodic, tol) + spline_bldr.Perform() + + # build the final edge + rv = BRepBuilderAPI_MakeEdge(spline_bldr.Curve(), f._geomAdaptor()).Edge() + BRepLib.BuildCurves3d_s(rv) + + return _compound_or_shape(rv) + + @multimethod def wire(*s: Shape) -> Shape: """ From fb0ba33c0c945fa5e22f4134d4cb5d3840f20ea5 Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 2 May 2025 17:06:41 +0200 Subject: [PATCH 02/27] Expose edge --- cadquery/func.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cadquery/func.py b/cadquery/func.py index 0ac4e26bf..e7454f89f 100644 --- a/cadquery/func.py +++ b/cadquery/func.py @@ -9,6 +9,7 @@ Solid, CompSolid, Compound, + edge, wire, face, shell, From 7d353fd7f6fd727b42277c39831ee50acc383e06 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Tue, 6 May 2025 08:55:37 +0200 Subject: [PATCH 03/27] Black fix --- cadquery/occ_impl/shapes.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 0d384f10e..36ac37499 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -59,7 +59,12 @@ ) # Array of points (used for B-spline construction): -from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt, TColgp_HArray1OfPnt2d +from OCP.TColgp import ( + TColgp_HArray1OfPnt, + TColgp_HArray2OfPnt, + TColgp_Array1OfPnt, + TColgp_HArray1OfPnt2d, +) # Array of vectors (used for B-spline interpolation): from OCP.TColgp import TColgp_Array1OfVec @@ -5040,6 +5045,7 @@ def _pts_to_harray(pts: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: return rv + def _pts_to_harray2d(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: """ Convert a sequence of 2d points to a TColgp harray (OCCT specific). @@ -5052,6 +5058,7 @@ def _pts_to_harray2d(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: return rv + def _floats_to_harray(vals: Sequence[float]) -> TColStd_HArray1OfReal: """ Convert a sequence of floats to a TColstd harray (OCCT specific). @@ -5153,21 +5160,26 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS ShapeHistory = Dict[Union[Shape, str], Shape] -def edge(pts: Sequence[Tuple[Real, Real]], base: Shape, periodic: bool = False, tol: float = 1e-6) -> Shape: +def edge( + pts: Sequence[Tuple[Real, Real]], + base: Shape, + periodic: bool = False, + tol: float = 1e-6, +) -> Shape: """ Build an edge on a face from points in (u,v) space. """ - + f = _get_one(base, "Face") # interpolate the u,v points spline_bldr = Geom2dAPI_Interpolate(_pts_to_harray2d(pts), periodic, tol) spline_bldr.Perform() - + # build the final edge rv = BRepBuilderAPI_MakeEdge(spline_bldr.Curve(), f._geomAdaptor()).Edge() BRepLib.BuildCurves3d_s(rv) - + return _compound_or_shape(rv) From a75bb0fe0fc4d83376eb5f9698ea47772732ba86 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Tue, 6 May 2025 19:12:07 +0200 Subject: [PATCH 04/27] Mypy fix --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 36ac37499..736110e93 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3471,7 +3471,7 @@ def thicken(self, thickness: float) -> "Solid": return Solid(builder.Shape()) @classmethod - def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> "Face": + def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> Self: bldr = BRepBuilderAPI_MakeFace(f._geomAdaptor(), outer.wrapped) From 3563f0f61c5900782bf202e75f2badc867abad91 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Wed, 7 May 2025 19:24:06 +0200 Subject: [PATCH 05/27] Add more trim/edge overloads --- cadquery/occ_impl/shapes.py | 83 ++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 736110e93..6e0ef1d08 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -19,6 +19,8 @@ from io import BytesIO +from itertools import chain + from vtkmodules.vtkCommonDataModel import vtkPolyData from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals @@ -3473,12 +3475,7 @@ def thicken(self, thickness: float) -> "Solid": @classmethod def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> Self: - bldr = BRepBuilderAPI_MakeFace(f._geomAdaptor(), outer.wrapped) - - for w in inner: - bldr.Add(TopoDS.Wire_s(w.wrapped)) - - return cls(bldr.Face()).fix() + return f.trim(outer, *inner) def project(self, other: "Face", d: VectorLike) -> "Face": @@ -3548,6 +3545,19 @@ def _( # construct the final trimmed face return self.constructOn(self, Wire(w)) + @trim.register + def _(self, outer: Wire, *inner: Wire) -> Self: + """ + Trim using edges. The provided edges need to have a pcurve on self. + """ + + bldr = BRepBuilderAPI_MakeFace(self._geomAdaptor(), outer.wrapped) + + for w in inner: + bldr.Add(TopoDS.Wire_s(w.wrapped)) + + return self.__class__(bldr.Face()).fix() + def isoline(self, param: Real, direction: Literal["u", "v"] = "v") -> Edge: """ Construct an isoline. @@ -4893,22 +4903,23 @@ def _get_wires(s: Shape) -> Iterable[Shape]: raise ValueError(f"Required type(s): Edge, Wire; encountered {t}") -def _get_edges(s: Shape) -> Iterable[Shape]: +def _get_edges(*shapes: Shape) -> Iterable[Shape]: """ Get edges or edges from wires. """ - t = s.ShapeType() - - if t == "Edge": - yield s - elif t == "Wire": - yield from _get_edges(s.edges()) - elif t == "Compound": - for el in s: - yield from _get_edges(el) - else: - raise ValueError(f"Required type(s): Edge, Wire; encountered {t}") + for s in shapes: + t = s.ShapeType() + + if t == "Edge": + yield s + elif t == "Wire": + yield from _get_edges(s.edges()) + elif t == "Compound": + for el in s: + yield from _get_edges(el) + else: + raise ValueError(f"Required type(s): Edge, Wire; encountered {t}") def _get_wire_lists(s: Sequence[Shape]) -> List[List[Union[Wire, Vertex]]]: @@ -5160,9 +5171,10 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS ShapeHistory = Dict[Union[Shape, str], Shape] +@multimethod def edge( - pts: Sequence[Tuple[Real, Real]], base: Shape, + pts: Sequence[Tuple[Real, Real]], periodic: bool = False, tol: float = 1e-6, ) -> Shape: @@ -5183,6 +5195,39 @@ def edge( return _compound_or_shape(rv) +@edge.register +def _(fbase: Shape, edg: Shape, *edgs: Shape, tol=1e-6, N=10): + """ + Map one or more edges onta a base face in the u,v space. + """ + + f = _get_one(fbase, "Face") + + rvs: List[TopoDS_Shape] = [] + + for el in _get_edges(edg, *edgs): + + # sample the original curve + pts3D, params = el.sample(N) + + # convert to 2D points ignoring the z coord + pts = [(el.x, el.y) for el in pts3D] + + # interpolate the u,v points + spline_bldr = Geom2dAPI_Interpolate( + _pts_to_harray2d(pts), el._geomAdaptor().IsPeriodic(), tol + ) + spline_bldr.Perform() + + # build the final edge + rv = BRepBuilderAPI_MakeEdge(spline_bldr.Curve(), f._geomAdaptor()).Edge() + BRepLib.BuildCurves3d_s(rv) + + rvs.append(rv) + + return _compound_or_shape(rvs) + + @multimethod def wire(*s: Shape) -> Shape: """ From 5bb84d40efd99dd6c8c83feedfe17891f93db830 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Thu, 8 May 2025 18:47:46 +0200 Subject: [PATCH 06/27] Correct handling of periodicity --- cadquery/occ_impl/shapes.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 6e0ef1d08..82b6588f5 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5196,7 +5196,9 @@ def edge( @edge.register -def _(fbase: Shape, edg: Shape, *edgs: Shape, tol=1e-6, N=10): +def _( + fbase: Shape, edg: Shape, *edgs: Shape, tol=1e-6, N=10, +): """ Map one or more edges onta a base face in the u,v space. """ @@ -5213,9 +5215,19 @@ def _(fbase: Shape, edg: Shape, *edgs: Shape, tol=1e-6, N=10): # convert to 2D points ignoring the z coord pts = [(el.x, el.y) for el in pts3D] + # handle periodicity + t0, _ = el._bounds() + el_crv = el._geomAdaptor() + + if el_crv.IsPeriodic() and el_crv.IsClosed(): + periodic = True + params.append(t0 + el_crv.Period()) + else: + periodic = False + # interpolate the u,v points spline_bldr = Geom2dAPI_Interpolate( - _pts_to_harray2d(pts), el._geomAdaptor().IsPeriodic(), tol + _pts_to_harray2d(pts), _floats_to_harray(params), periodic, tol ) spline_bldr.Perform() From 201f7ce866b38296150f0717ab869e44f23e7496 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Thu, 8 May 2025 18:52:16 +0200 Subject: [PATCH 07/27] Docstring fix --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 82b6588f5..7946a9ba4 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3548,7 +3548,7 @@ def _( @trim.register def _(self, outer: Wire, *inner: Wire) -> Self: """ - Trim using edges. The provided edges need to have a pcurve on self. + Trim using wires. The provided wires need to have a pcurve on self. """ bldr = BRepBuilderAPI_MakeFace(self._geomAdaptor(), outer.wrapped) From 23f654dd024dcfe25d239598dda76d76215e0475 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Thu, 8 May 2025 19:25:18 +0200 Subject: [PATCH 08/27] Add a test for edge() --- tests/test_free_functions.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 6e1c06a84..f8f4bb096 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -44,6 +44,7 @@ imprint, setThreads, project, + edge, ) from cadquery.occ_impl.shapes import ( @@ -255,6 +256,32 @@ def test_solid(): assert f in final_faces_history +def test_edge(): + + # make a base face + f = torus(10, 4).faces() + + # construct an edge with points + e1 = edge(f, [(0, 0), (0, 1), (1, 1), (1, 0)], periodic=True) + + assert e1.isValid() + + # use it to make a face + f1 = f.trim(wire(e1)) + + assert f1.isValid() + + # construct in uv space directly + e2 = edge(f, circle(0.3)) + + assert e2.isValid() + + # use it to make a face + f2 = f.trim(wire(e2)) + + assert f2.isValid() + + #%% primitives From 4bbff85b3ebfc3bcbf9199b90885d4688a8f7c7a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 07:51:23 +0200 Subject: [PATCH 09/27] Rename edge, add wireOn and extend tests --- cadquery/func.py | 3 ++- cadquery/occ_impl/shapes.py | 18 ++++++++++++++---- tests/test_free_functions.py | 9 ++++----- tests/test_shapes.py | 10 ++++++++++ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/cadquery/func.py b/cadquery/func.py index 2b0703b73..dcf25f10b 100644 --- a/cadquery/func.py +++ b/cadquery/func.py @@ -9,7 +9,8 @@ Solid, CompSolid, Compound, - edge, + edgeOn, + wireOn, wire, face, shell, diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 7946a9ba4..398290ded 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5172,7 +5172,7 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS @multimethod -def edge( +def edgeOn( base: Shape, pts: Sequence[Tuple[Real, Real]], periodic: bool = False, @@ -5195,12 +5195,12 @@ def edge( return _compound_or_shape(rv) -@edge.register +@edgeOn.register def _( - fbase: Shape, edg: Shape, *edgs: Shape, tol=1e-6, N=10, + fbase: Shape, edg: Shape, *edgs: Shape, tol: float=1e-6, N: int=10, ): """ - Map one or more edges onta a base face in the u,v space. + Map one or more edges onto a base face in the u,v space. """ f = _get_one(fbase, "Face") @@ -5240,6 +5240,16 @@ def _( return _compound_or_shape(rvs) +def wireOn(base: Shape, w: Shape, tol=1e-6, N=10) -> Shape: + """ + Map a wire onto a base face in the u,v space. + """ + + rvs = [edgeOn(base, e, tol=tol, N=N) for e in w.edges()] + + return wire(rvs) + + @multimethod def wire(*s: Shape) -> Shape: """ diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index f8f4bb096..4ac985d65 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -37,14 +37,13 @@ Compound, Edge, Shell, - Wire, check, Vector, closest, imprint, setThreads, project, - edge, + edgeOn, ) from cadquery.occ_impl.shapes import ( @@ -256,13 +255,13 @@ def test_solid(): assert f in final_faces_history -def test_edge(): +def test_edgeOn(): # make a base face f = torus(10, 4).faces() # construct an edge with points - e1 = edge(f, [(0, 0), (0, 1), (1, 1), (1, 0)], periodic=True) + e1 = edgeOn(f, [(0, 0), (0, 1), (1, 1), (1, 0)], periodic=True) assert e1.isValid() @@ -272,7 +271,7 @@ def test_edge(): assert f1.isValid() # construct in uv space directly - e2 = edge(f, circle(0.3)) + e2 = edgeOn(f, circle(0.3)) assert e2.isValid() diff --git a/tests/test_shapes.py b/tests/test_shapes.py index c5d1c9d8a..b3f81b992 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -14,6 +14,8 @@ ellipse, spline, sweep, + polygon, + wireOn, ) from pytest import approx, raises @@ -201,9 +203,17 @@ def test_trimming(): e = segment((0, 0), (0, 1)) f = plane(1, 1) + # edge trim assert e.trim(0, 0.5).Length() == approx(e.Length() / 2) + + # face trim assert f.trim(0, 0.5, -0.5, 0.5).Area() == approx(f.Area() / 2) + # face trim using points + assert f.trim( + wireOn(f, polygon((0, -0.5), (0.5, -0.5), (0.5, 0.5), (0, 0.5))) + ).Area() == approx(f.Area() / 2) + def test_bin_import_export(): From b3fe87638e3caffb3c72c50215ac8691a32892cd Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 08:06:06 +0200 Subject: [PATCH 10/27] Black fix --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 398290ded..2f0091499 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5197,7 +5197,7 @@ def edgeOn( @edgeOn.register def _( - fbase: Shape, edg: Shape, *edgs: Shape, tol: float=1e-6, N: int=10, + fbase: Shape, edg: Shape, *edgs: Shape, tol: float = 1e-6, N: int = 10, ): """ Map one or more edges onto a base face in the u,v space. From 38c5ef0b417cf2b6c5590d39a324b403b9b5d415 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 08:31:49 +0200 Subject: [PATCH 11/27] Test additional overload --- tests/test_shapes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index b3f81b992..6a55e3af3 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -209,11 +209,16 @@ def test_trimming(): # face trim assert f.trim(0, 0.5, -0.5, 0.5).Area() == approx(f.Area() / 2) - # face trim using points + # face trim using wires assert f.trim( wireOn(f, polygon((0, -0.5), (0.5, -0.5), (0.5, 0.5), (0, 0.5))) ).Area() == approx(f.Area() / 2) + # face trim using points + assert f.trim((0, -0.5), (0.5, -0.5), (0.5, 0.5), (0, 0.5)).Area() == approx( + f.Area() / 2 + ) + def test_bin_import_export(): From d9964e4e7a960682f1d3b4bf15af79205dd8d46c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 09:07:21 +0200 Subject: [PATCH 12/27] Fix corner case --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 2f0091499..c6d2bac32 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5245,7 +5245,7 @@ def wireOn(base: Shape, w: Shape, tol=1e-6, N=10) -> Shape: Map a wire onto a base face in the u,v space. """ - rvs = [edgeOn(base, e, tol=tol, N=N) for e in w.edges()] + rvs = [edgeOn(base, e, tol=tol, N=N) for e in w.Edges()] return wire(rvs) From 9988e973b956fa1052afce1bc10448cd978aaf7d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 09:08:56 +0200 Subject: [PATCH 13/27] Corner case test --- tests/test_shapes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 6a55e3af3..6e4d6b7ba 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -214,6 +214,9 @@ def test_trimming(): wireOn(f, polygon((0, -0.5), (0.5, -0.5), (0.5, 0.5), (0, 0.5))) ).Area() == approx(f.Area() / 2) + # face trim using wires - single edge case + assert f.trim(wireOn(f, circle(1))).isValid() + # face trim using points assert f.trim((0, -0.5), (0.5, -0.5), (0.5, 0.5), (0, 0.5)).Area() == approx( f.Area() / 2 From 3416701d8b31729866ace86873ebfbabb5dc5010 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Fri, 9 May 2025 16:02:23 +0200 Subject: [PATCH 14/27] Add hasPCurve() --- cadquery/occ_impl/shapes.py | 8 ++++++++ tests/test_free_functions.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index c6d2bac32..5c8b02041 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -269,6 +269,7 @@ from OCP.ShapeAnalysis import ( ShapeAnalysis_FreeBounds, + ShapeAnalysis_Edge, ShapeAnalysis_Wire, ShapeAnalysis_Surface, ) @@ -2349,6 +2350,13 @@ def trim(self, u0: Real, u1: Real) -> "Edge": return self.__class__(bldr.Shape()) + def hasPCurve(self, f: "Face") -> bool: + """ + Check if self has a pcurve defined on f. + """ + + return ShapeAnalysis_Edge().HasPCurve(self.wrapped, f.wrapped) + @classmethod def makeCircle( cls, diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 4ac985d65..76af0161f 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -264,6 +264,7 @@ def test_edgeOn(): e1 = edgeOn(f, [(0, 0), (0, 1), (1, 1), (1, 0)], periodic=True) assert e1.isValid() + assert e1.hasPCurve(f) # use it to make a face f1 = f.trim(wire(e1)) @@ -274,6 +275,7 @@ def test_edgeOn(): e2 = edgeOn(f, circle(0.3)) assert e2.isValid() + assert e2.hasPCurve(f) # use it to make a face f2 = f.trim(wire(e2)) From fe1b98156c13310f4a0fbc20ad8a5315fcf83dcf Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 12:06:22 +0200 Subject: [PATCH 15/27] Make uvBounds public --- cadquery/occ_impl/shapes.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5c8b02041..936388c17 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1842,6 +1842,13 @@ def paramsLength(self, locations: Iterable[float]) -> List[float]: class Mixin1D(object): def _bounds(self: Mixin1DProtocol) -> Tuple[float, float]: + return self.bounds() + + def bounds(self: Mixin1DProtocol) -> Tuple[float, float]: + """ + Parametric bounds of the curve. + """ + curve = self._geomAdaptor() return curve.FirstParameter(), curve.LastParameter() @@ -3030,6 +3037,13 @@ def _geomAdaptor(self) -> Geom_Surface: def _uvBounds(self) -> Tuple[float, float, float, float]: + return self.uvBounds() + + def uvBounds(self) -> Tuple[float, float, float, float]: + """ + Parametric bounds (u_min, u_max, v_min, v_max). + """ + return BRepTools.UVBounds_s(self.wrapped) def paramAt(self, pt: VectorLike) -> Tuple[float, float]: From aefcf5b8430076b0e1f622d890695c9b6c96c12f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 14:27:08 +0200 Subject: [PATCH 16/27] Fix Mixin1DProtocol --- cadquery/occ_impl/shapes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 936388c17..be2804b25 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1812,6 +1812,9 @@ def _curve_and_param( ) -> Tuple[Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve], float]: ... + def bounds(self) -> Tuple[float, float]: + ... + def paramAt(self, d: float) -> float: ... From a002dd0a3e6b2098471f5e7a566d10c6f2c8db0c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 17:58:13 +0200 Subject: [PATCH 17/27] Add faceOn --- cadquery/func.py | 1 + cadquery/occ_impl/shapes.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/cadquery/func.py b/cadquery/func.py index dcf25f10b..65f0c8266 100644 --- a/cadquery/func.py +++ b/cadquery/func.py @@ -50,4 +50,5 @@ closest, setThreads, project, + faceOn, ) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index be2804b25..65717cf02 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5323,6 +5323,36 @@ def face(s: Sequence[Shape]) -> Shape: return face(*s) +def faceOn(base: Shape, *fcs: Shape) -> Shape: + """ + Build face(s) on base by mapping planar face(s) onto the (u,v) space of base. + """ + + rv: Shape + rvs = [] + + # get a face + fbase = _get_one(base, "Face") + + # iterate over all faces + for el in fcs: + for fc in el.Faces(): + # construct pcurves and trim in one go + rvs.append( + fbase.trim( + wireOn(fbase, fc.outerWire()), + *(wireOn(fbase, w) for w in fc.innerWires()), + ) + ) + + if len(rvs) == 1: + rv = rvs[0] + else: + rv = compound(rvs) + + return rv + + def _process_sewing_history( builder: BRepBuilderAPI_Sewing, faces: List[Face], history: Optional[ShapeHistory], ): From e4efee377bd344269d15290fe53b8934f4f7bded Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 17:58:29 +0200 Subject: [PATCH 18/27] Add some docs --- doc/free-func.rst | 82 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index b669d302f..68e9dcc7e 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -344,8 +344,88 @@ and only then the final solid can be constructed. # local sewing - only two faces are taken into account sh = shell(b_top_hole, feat.faces(' Date: Sat, 10 May 2025 18:06:17 +0200 Subject: [PATCH 19/27] Tweak parameters --- doc/free-func.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index 68e9dcc7e..e57cded92 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -375,7 +375,7 @@ trims, polygons, splines or arbitrary wires. r2 = base.trim((0,0), (pi,0), (pi/2, h)) # construct a pcurve - pcurve = edgeOn(base, [(0, 0), (pi, 0), (pi, h/2), (0, h/2)], periodic=True) + pcurve = edgeOn(base, [(pi/2, h/4), (pi, h/4), (pi, h/2), (pi/2, h/2)], periodic=True) # pcurve trim r3 = base.trim(wire(pcurve)) From 72a33ca462a5c4a438a9d4da5280b6f1f39f9d0d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 18:14:50 +0200 Subject: [PATCH 20/27] Doc fix --- doc/free-func.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index e57cded92..d00fb05d2 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -398,7 +398,7 @@ to work with higher level objects like wires. du = pi Nturns = 2 - # consturct the base surface + # construct the base surface base = cylinder(d, h).faces("%CYLINDER") # construct a planar 2D patch for u,v trimming @@ -422,10 +422,10 @@ Finally, it is also possible to map whole faces. .. cadquery:: - from cadquery.func import text, faceOn, extrude + from cadquery.func import sphere, text, faceOn - t = text("CadQuery", 0.6, halign="left").moved(rz=90) + base = sphere(5).faces() - result = extrude(faceOn(base, t), (0.05, 0, 0)) + result = faceOn(base, text("CadQuery", 1)) From 03a4b3d670087ece5ae27ce688407bb77b55ff0d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 18:37:17 +0200 Subject: [PATCH 21/27] Doc tweaks --- doc/free-func.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index d00fb05d2..89c53b5ed 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -369,10 +369,10 @@ trims, polygons, splines or arbitrary wires. base = cylinder(d, h).faces("%CYLINDER") # rectungualr trim - r1 = base.trim(-pi/2, 0, 0, h) + r1 = base.trim(-pi/2, 0, 0, h/3) # polyline trim - r2 = base.trim((0,0), (pi,0), (pi/2, h)) + r2 = base.trim((0,0), (pi,0), (pi/2, h/2)) # construct a pcurve pcurve = edgeOn(base, [(pi/2, h/4), (pi, h/4), (pi, h/2), (pi/2, h/2)], periodic=True) @@ -417,7 +417,7 @@ to work with higher level objects like wires. result = base.trim(w) -Finally, it is also possible to map whole faces. +Finally, it is also possible to map complete faces. .. cadquery:: From 4d816f16f247abad2bad140bf708cfdd1375fefe Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 19:25:55 +0200 Subject: [PATCH 22/27] Closed edge handling fix --- cadquery/occ_impl/shapes.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 65717cf02..b53d6e50b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5222,7 +5222,7 @@ def edgeOn( @edgeOn.register def _( - fbase: Shape, edg: Shape, *edgs: Shape, tol: float = 1e-6, N: int = 10, + fbase: Shape, edg: Shape, *edgs: Shape, tol: float = 1e-6, N: int = 20, ): """ Map one or more edges onto a base face in the u,v space. @@ -5241,14 +5241,20 @@ def _( pts = [(el.x, el.y) for el in pts3D] # handle periodicity - t0, _ = el._bounds() + t0, t1 = el._bounds() el_crv = el._geomAdaptor() + periodic = False + + # periodic (and closed) if el_crv.IsPeriodic() and el_crv.IsClosed(): periodic = True params.append(t0 + el_crv.Period()) - else: - periodic = False + + # only closed + elif el_crv.IsClosed(): + pts.append(pts[0]) + params.append(t1) # interpolate the u,v points spline_bldr = Geom2dAPI_Interpolate( From 1703889f6ab656b847a9b2b83ff266d56c597883 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 21:17:55 +0200 Subject: [PATCH 23/27] faceOn test --- tests/test_free_functions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 76af0161f..222d7b5d3 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -36,6 +36,7 @@ Shape, Compound, Edge, + Face, Shell, check, Vector, @@ -44,6 +45,7 @@ setThreads, project, edgeOn, + faceOn, ) from cadquery.occ_impl.shapes import ( @@ -283,6 +285,25 @@ def test_edgeOn(): assert f2.isValid() +def test_faceOn(): + + # make a base face + f = sphere(4).faces() + + # single face + f1 = faceOn(f, text("d", 1)) + + assert f1.isValid() + assert isinstance(f1, Face) + assert all(w.IsClosed() for w in f1) + + # multiple faces + f2 = faceOn(f, text("CQ", 1)) + + assert f2.isValid() + assert len(f2.Faces()) == 2 + + #%% primitives From f60633f135fdf19931b2875d37573390c0fad335 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com> Date: Sat, 10 May 2025 21:45:03 +0200 Subject: [PATCH 24/27] Add kwargs to faceOn --- cadquery/occ_impl/shapes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b53d6e50b..b67c0c31a 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5271,7 +5271,7 @@ def _( return _compound_or_shape(rvs) -def wireOn(base: Shape, w: Shape, tol=1e-6, N=10) -> Shape: +def wireOn(base: Shape, w: Shape, tol=1e-6, N=20) -> Shape: """ Map a wire onto a base face in the u,v space. """ @@ -5329,7 +5329,7 @@ def face(s: Sequence[Shape]) -> Shape: return face(*s) -def faceOn(base: Shape, *fcs: Shape) -> Shape: +def faceOn(base: Shape, *fcs: Shape, tol=1e-6, N=20) -> Shape: """ Build face(s) on base by mapping planar face(s) onto the (u,v) space of base. """ @@ -5346,8 +5346,8 @@ def faceOn(base: Shape, *fcs: Shape) -> Shape: # construct pcurves and trim in one go rvs.append( fbase.trim( - wireOn(fbase, fc.outerWire()), - *(wireOn(fbase, w) for w in fc.innerWires()), + wireOn(fbase, fc.outerWire(), tol=tol, N=N), + *(wireOn(fbase, w, tol=tol, N=N) for w in fc.innerWires()), ) ) From 77de8a06666aaff1583c681ac1b22cb8e0cfc66f Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 12 May 2025 08:04:20 +0200 Subject: [PATCH 25/27] Apply suggestions from code review --- cadquery/occ_impl/shapes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b67c0c31a..935889486 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -19,7 +19,6 @@ from io import BytesIO -from itertools import chain from vtkmodules.vtkCommonDataModel import vtkPolyData from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals @@ -5082,12 +5081,12 @@ def _pts_to_harray(pts: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: return rv -def _pts_to_harray2d(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: +def _pts_to_harray2D(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: """ Convert a sequence of 2d points to a TColgp harray (OCCT specific). """ - rv = TColgp_HArray1OfPnt2d(1, len(pts)) + rv = TColgp_HArray1OfPnt2D(1, len(pts)) for i, p in enumerate(pts): rv.SetValue(i + 1, gp_Pnt2d(*p)) From d6f25d7951cf57cc53ba4f2cfe3a3faed58eb43e Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 12 May 2025 08:08:58 +0200 Subject: [PATCH 26/27] Apply suggestions from code review - docs Co-authored-by: Lorenz --- doc/free-func.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index 89c53b5ed..cc68572ec 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -351,7 +351,7 @@ and only then the final solid can be constructed. Mapping onto parametric space ----------------------------- -To complement functionalities described It is possible to trim edges and faces explicitly using simple rectangular +To complement functionalities described, it is possible to trim edges and faces explicitly using simple rectangular trims, polygons, splines or arbitrary wires. .. cadquery:: @@ -365,10 +365,10 @@ trims, polygons, splines or arbitrary wires. du = pi Nturns = 2 - # constu urct the base surface + # construct the base surface base = cylinder(d, h).faces("%CYLINDER") - # rectungualr trim + # rectangular trim r1 = base.trim(-pi/2, 0, 0, h/3) # polyline trim @@ -413,7 +413,7 @@ to work with higher level objects like wires. for e in w: assert e.hasPCurve(base), "No p-curve on base present" - # tirm the base surface + # trim the base surface result = base.trim(w) From 1a9cdac741e50bd6cc9284d2cf284095ca559e93 Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 12 May 2025 08:16:24 +0200 Subject: [PATCH 27/27] Fix the fix --- cadquery/occ_impl/shapes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 935889486..61291bdaa 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5086,7 +5086,7 @@ def _pts_to_harray2D(pts: Sequence[Tuple[Real, Real]]) -> TColgp_HArray1OfPnt2d: Convert a sequence of 2d points to a TColgp harray (OCCT specific). """ - rv = TColgp_HArray1OfPnt2D(1, len(pts)) + rv = TColgp_HArray1OfPnt2d(1, len(pts)) for i, p in enumerate(pts): rv.SetValue(i + 1, gp_Pnt2d(*p)) @@ -5209,7 +5209,7 @@ def edgeOn( f = _get_one(base, "Face") # interpolate the u,v points - spline_bldr = Geom2dAPI_Interpolate(_pts_to_harray2d(pts), periodic, tol) + spline_bldr = Geom2dAPI_Interpolate(_pts_to_harray2D(pts), periodic, tol) spline_bldr.Perform() # build the final edge @@ -5257,7 +5257,7 @@ def _( # interpolate the u,v points spline_bldr = Geom2dAPI_Interpolate( - _pts_to_harray2d(pts), _floats_to_harray(params), periodic, tol + _pts_to_harray2D(pts), _floats_to_harray(params), periodic, tol ) spline_bldr.Perform()