Skip to content

Add more trimming possibilities #1833

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fefcada
Add more trimming possibilities
adam-urbanczyk May 2, 2025
fb0ba33
Expose edge
adam-urbanczyk May 2, 2025
1518cb6
Merge branch 'shapes-trimming' of https://github.com/CadQuery/cadquer…
adam-urbanczyk May 6, 2025
7d353fd
Black fix
adam-urbanczyk May 6, 2025
a75bb0f
Mypy fix
adam-urbanczyk May 6, 2025
3563f0f
Add more trim/edge overloads
adam-urbanczyk May 7, 2025
5bb84d4
Correct handling of periodicity
adam-urbanczyk May 8, 2025
201f7ce
Docstring fix
adam-urbanczyk May 8, 2025
23f654d
Add a test for edge()
adam-urbanczyk May 8, 2025
4bbff85
Rename edge, add wireOn and extend tests
adam-urbanczyk May 9, 2025
b3fe876
Black fix
adam-urbanczyk May 9, 2025
38c5ef0
Test additional overload
adam-urbanczyk May 9, 2025
d9964e4
Fix corner case
adam-urbanczyk May 9, 2025
9988e97
Corner case test
adam-urbanczyk May 9, 2025
3416701
Add hasPCurve()
adam-urbanczyk May 9, 2025
fe1b981
Make uvBounds public
adam-urbanczyk May 10, 2025
aefcf5b
Fix Mixin1DProtocol
adam-urbanczyk May 10, 2025
a002dd0
Add faceOn
adam-urbanczyk May 10, 2025
e4efee3
Add some docs
adam-urbanczyk May 10, 2025
9c0b102
Tweak parameters
adam-urbanczyk May 10, 2025
72a33ca
Doc fix
adam-urbanczyk May 10, 2025
03a4b3d
Doc tweaks
adam-urbanczyk May 10, 2025
4d816f1
Closed edge handling fix
adam-urbanczyk May 10, 2025
1703889
faceOn test
adam-urbanczyk May 10, 2025
f60633f
Add kwargs to faceOn
adam-urbanczyk May 10, 2025
77de8a0
Apply suggestions from code review
adam-urbanczyk May 12, 2025
d6f25d7
Apply suggestions from code review - docs
adam-urbanczyk May 12, 2025
1a9cdac
Fix the fix
adam-urbanczyk May 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cadquery/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
Solid,
CompSolid,
Compound,
edgeOn,
wireOn,
wire,
face,
shell,
Expand Down Expand Up @@ -48,4 +50,5 @@
closest,
setThreads,
project,
faceOn,
)
259 changes: 235 additions & 24 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from io import BytesIO


from vtkmodules.vtkCommonDataModel import vtkPolyData
from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals

Expand Down Expand Up @@ -59,7 +60,12 @@
)

# 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
Expand Down Expand Up @@ -162,6 +168,8 @@
)
from OCP.Geom2d import Geom2d_Line

from OCP.Geom2dAPI import Geom2dAPI_Interpolate

from OCP.BRepLib import BRepLib, BRepLib_FindSurface

from OCP.BRepOffsetAPI import (
Expand Down Expand Up @@ -260,6 +268,7 @@

from OCP.ShapeAnalysis import (
ShapeAnalysis_FreeBounds,
ShapeAnalysis_Edge,
ShapeAnalysis_Wire,
ShapeAnalysis_Surface,
)
Expand Down Expand Up @@ -1802,6 +1811,9 @@ def _curve_and_param(
) -> Tuple[Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve], float]:
...

def bounds(self) -> Tuple[float, float]:
...

def paramAt(self, d: float) -> float:
...

Expand Down Expand Up @@ -1832,6 +1844,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()

Expand Down Expand Up @@ -2340,6 +2359,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,
Expand Down Expand Up @@ -3013,6 +3039,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]:
Expand Down Expand Up @@ -3464,14 +3497,9 @@ def thicken(self, thickness: float) -> "Solid":
return Solid(builder.Shape())

@classmethod
def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> "Face":

bldr = BRepBuilderAPI_MakeFace(f._geomAdaptor(), outer.wrapped)

for w in inner:
bldr.Add(TopoDS.Wire_s(w.wrapped))
def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> Self:

return cls(bldr.Face()).fix()
return f.trim(outer, *inner)

def project(self, other: "Face", d: VectorLike) -> "Face":

Expand All @@ -3489,9 +3517,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.
"""
Expand All @@ -3500,6 +3529,59 @@ 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))

@trim.register
def _(self, outer: Wire, *inner: Wire) -> Self:
"""
Trim using wires. The provided wires 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.
Expand Down Expand Up @@ -4845,22 +4927,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 wires or wires from edges.
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]]]:
Expand Down Expand Up @@ -4987,7 +5070,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))
Expand All @@ -4998,6 +5081,19 @@ 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:
"""
Convert a sequence of floats to a TColstd harray (OCCT specific).
Expand Down Expand Up @@ -5099,6 +5195,91 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS
ShapeHistory = Dict[Union[Shape, str], Shape]


@multimethod
def edgeOn(
base: Shape,
pts: Sequence[Tuple[Real, Real]],
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)


@edgeOn.register
def _(
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.
"""

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]

# handle periodicity
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())

# only closed
elif el_crv.IsClosed():
pts.append(pts[0])
params.append(t1)

# interpolate the u,v points
spline_bldr = Geom2dAPI_Interpolate(
_pts_to_harray2D(pts), _floats_to_harray(params), periodic, 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)


def wireOn(base: Shape, w: Shape, tol=1e-6, N=20) -> 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:
"""
Expand Down Expand Up @@ -5147,6 +5328,36 @@ def face(s: Sequence[Shape]) -> Shape:
return face(*s)


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.
"""

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(), tol=tol, N=N),
*(wireOn(fbase, w, tol=tol, N=N) 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],
):
Expand Down
Loading