Skip to content

Commit 2ad954c

Browse files
Local and non-manifold sewing (#1808)
* Add extend and speed up distance calc * Add conversion to NURBS * Add checks to extend * Remove checks * Add test and defaults * Better coverage * Ignore coverege of a failed imprint * Better coverage * Remove pragma * Fixture cleanup * Add replace * Fix replace and add remove to Shape * Add addCavity * Add more tests * Coverage fix * Actually faster distance * Add local and non-manifold sewing * Add history to sewing * Rework local sewing and add a test * Simplify history handling * Add history to solid and reorganize tests * Mypy fix * add addHole * Add test for addHole * black fix * Add project * Test project * Add some docs * Add one more example * Doc fix * Fix typo Co-authored-by: Jeremy Wright <[email protected]> * Typo fix --------- Co-authored-by: Jeremy Wright <[email protected]>
1 parent 8c68faa commit 2ad954c

File tree

5 files changed

+291
-34
lines changed

5 files changed

+291
-34
lines changed

cadquery/func.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@
4747
check,
4848
closest,
4949
setThreads,
50+
project,
5051
)

cadquery/occ_impl/shapes.py

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@
294294

295295
from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters
296296

297-
from OCP.BRepAlgo import BRepAlgo
297+
from OCP.BRepAlgo import BRepAlgo, BRepAlgo_NormalProjection
298298

299299
from OCP.ChFi2d import ChFi2d_FilletAPI # For Wire.Fillet()
300300

@@ -2216,15 +2216,15 @@ def project(
22162216
self: T1D, face: "Face", d: VectorLike, closest: bool = True
22172217
) -> Union[T1D, List[T1D]]:
22182218
"""
2219-
Project onto a face along the specified direction
2219+
Project onto a face along the specified direction.
22202220
"""
22212221

2222-
bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(d).toDir())
2223-
shapes = Compound(bldr.Shape())
2224-
22252222
# select the closest projection if requested
22262223
rv: Union[T1D, List[T1D]]
22272224

2225+
bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(d).toDir())
2226+
shapes = Compound(bldr.Shape())
2227+
22282228
if closest:
22292229

22302230
dist_calc = BRepExtrema_DistShapeShape()
@@ -3555,6 +3555,20 @@ def extend(
35553555

35563556
return self.__class__(rv)
35573557

3558+
def addHole(self, *inner: Wire | Edge) -> Self:
3559+
"""
3560+
Add one or more holes.
3561+
"""
3562+
3563+
bldr = BRepBuilderAPI_MakeFace(self.wrapped)
3564+
3565+
for w in inner:
3566+
bldr.Add(
3567+
TopoDS.Wire_s(w.wrapped if isinstance(w, Wire) else wire(w).wrapped)
3568+
)
3569+
3570+
return self.__class__(bldr.Face()).fix()
3571+
35583572

35593573
class Shell(Shape):
35603574
"""
@@ -5082,6 +5096,8 @@ def _adaptor_curve_to_edge(crv: Adaptor3d_Curve, p1: float, p2: float) -> TopoDS
50825096

50835097
#%% alternative constructors
50845098

5099+
ShapeHistory = Dict[Union[Shape, str], Shape]
5100+
50855101

50865102
@multimethod
50875103
def wire(*s: Shape) -> Shape:
@@ -5131,21 +5147,54 @@ def face(s: Sequence[Shape]) -> Shape:
51315147
return face(*s)
51325148

51335149

5150+
def _process_sewing_history(
5151+
builder: BRepBuilderAPI_Sewing, faces: List[Face], history: Optional[ShapeHistory],
5152+
):
5153+
"""
5154+
Reusable helper for processing sewing history.
5155+
"""
5156+
5157+
# fill history if provided
5158+
if history is not None:
5159+
# collect shapes present in the history dict
5160+
for k, v in history.items():
5161+
if isinstance(k, str):
5162+
history[k] = Face(builder.Modified(v.wrapped))
5163+
5164+
# store all top-level shape relations
5165+
for f in faces:
5166+
history[f] = Face(builder.Modified(f.wrapped))
5167+
5168+
51345169
@multimethod
5135-
def shell(*s: Shape, tol: float = 1e-6) -> Shape:
5170+
def shell(
5171+
*s: Shape,
5172+
tol: float = 1e-6,
5173+
manifold: bool = True,
5174+
ctx: Optional[Sequence[Shape] | Shape] = None,
5175+
history: Optional[ShapeHistory] = None,
5176+
) -> Shape:
51365177
"""
5137-
Build shell from faces.
5178+
Build shell from faces. If ctx is specified, local sewing is performed.
51385179
"""
51395180

5140-
builder = BRepBuilderAPI_Sewing(tol)
5181+
builder = BRepBuilderAPI_Sewing(tol, option4=not manifold)
5182+
if ctx:
5183+
if isinstance(ctx, Shape):
5184+
builder.Load(ctx.wrapped)
5185+
else:
5186+
builder.Load(compound(ctx).wrapped)
5187+
5188+
faces: list[Face] = []
51415189

51425190
for el in s:
51435191
for f in _get(el, "Face"):
51445192
builder.Add(f.wrapped)
5193+
faces.append(f)
51455194

51465195
builder.Perform()
5147-
51485196
sewed = builder.SewedShape()
5197+
_process_sewing_history(builder, faces, history)
51495198

51505199
# for one face sewing will not produce a shell
51515200
if sewed.ShapeType() == TopAbs_ShapeEnum.TopAbs_FACE:
@@ -5162,16 +5211,24 @@ def shell(*s: Shape, tol: float = 1e-6) -> Shape:
51625211

51635212

51645213
@shell.register
5165-
def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape:
5214+
def shell(
5215+
s: Sequence[Shape],
5216+
tol: float = 1e-6,
5217+
manifold: bool = True,
5218+
ctx: Optional[Sequence[Shape] | Shape] = None,
5219+
history: Optional[ShapeHistory] = None,
5220+
) -> Shape:
51665221
"""
5167-
Build shell from a sequence of faces.
5222+
Build shell from a sequence of faces. If ctx is specified, local sewing is performed.
51685223
"""
51695224

5170-
return shell(*s, tol=tol)
5225+
return shell(*s, tol=tol, manifold=manifold, ctx=ctx, history=history)
51715226

51725227

51735228
@multimethod
5174-
def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
5229+
def solid(
5230+
s1: Shape, *sn: Shape, tol: float = 1e-6, history: Optional[ShapeHistory] = None,
5231+
) -> Shape:
51755232
"""
51765233
Build solid from faces or shells.
51775234
"""
@@ -5186,7 +5243,7 @@ def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
51865243
shells = [el.wrapped for el in shells_faces if el.ShapeType() == "Shell"]
51875244
if not shells:
51885245
faces = [el for el in shells_faces]
5189-
shells = [shell(*faces, tol=tol).wrapped]
5246+
shells = [shell(*faces, tol=tol, history=history).wrapped]
51905247

51915248
rvs = [builder.SolidFromShell(sh) for sh in shells]
51925249

@@ -5195,17 +5252,20 @@ def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
51955252

51965253
@solid.register
51975254
def solid(
5198-
s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None, tol: float = 1e-6
5255+
s: Sequence[Shape],
5256+
inner: Optional[Sequence[Shape]] = None,
5257+
tol: float = 1e-6,
5258+
history: Optional[ShapeHistory] = None,
51995259
) -> Shape:
52005260
"""
52015261
Build solid from a sequence of faces.
52025262
"""
52035263

52045264
builder = BRepBuilderAPI_MakeSolid()
5205-
builder.Add(shell(*s, tol=tol).wrapped)
5265+
builder.Add(shell(*s, tol=tol, history=history).wrapped)
52065266

52075267
if inner:
5208-
for sh in _get(shell(*inner, tol=tol), "Shell"):
5268+
for sh in _get(shell(*inner, tol=tol, history=history), "Shell"):
52095269
builder.Add(sh.wrapped)
52105270

52115271
# fix orientations
@@ -5741,7 +5801,7 @@ def imprint(
57415801
*shapes: Shape,
57425802
tol: float = 0.0,
57435803
glue: GlueLiteral = "full",
5744-
history: Optional[Dict[Union[Shape, str], Shape]] = None,
5804+
history: Optional[ShapeHistory] = None,
57455805
) -> Shape:
57465806
"""
57475807
Imprint arbitrary number of shapes.
@@ -6199,6 +6259,29 @@ def loft(
61996259
return loft(s, cap, ruled, continuity, parametrization, degree, compat)
62006260

62016261

6262+
def project(
6263+
s: Shape,
6264+
base: Shape,
6265+
continuity: Literal["C1", "C2", "C3"] = "C2",
6266+
degree: int = 3,
6267+
maxseg: int = 30,
6268+
tol: float = 1e-4,
6269+
):
6270+
"""
6271+
Project s onto base using normal projection.
6272+
"""
6273+
6274+
bldr = BRepAlgo_NormalProjection(base.wrapped)
6275+
bldr.SetParams(tol, tol ** (2 / 3), _to_geomabshape(continuity), degree, maxseg)
6276+
6277+
for el in _get_edges(s):
6278+
bldr.Add(s.wrapped)
6279+
6280+
bldr.Build()
6281+
6282+
return _compound_or_shape(bldr.Projection())
6283+
6284+
62026285
#%% diagnotics
62036286

62046287

doc/free-func.rst

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. _freefuncapi:
1+
22

33
*****************
44
Free function API
@@ -241,7 +241,8 @@ Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move`
241241
Text
242242
----
243243

244-
The free function API has extensive text creation capabilities including text on planar curves and text on surfaces.
244+
The free function API has extensive text creation capabilities including text on
245+
planar curves and text on surfaces.
245246

246247

247248
.. cadquery::
@@ -275,3 +276,76 @@ The free function API has extensive text creation capabilities including text on
275276
r4 = offset(r3, TH).moved(z=S)
276277

277278
result = compound(r1, r2, r3, r4)
279+
280+
281+
Adding features manually
282+
------------------------
283+
284+
In certain cases it is desirable to add features such as holes or protrusions manually.
285+
E.g., for complicated shapes it might be beneficial performance-wise because it
286+
avoids boolean operations. One can add or remove faces, add holes to existing faces
287+
and last but not least reconstruct existing solids.
288+
289+
.. cadquery::
290+
291+
from cadquery.func import *
292+
293+
w = 1
294+
r = 0.9*w/2
295+
296+
# box
297+
b = box(w, w, w)
298+
# bottom face
299+
b_bot = b.faces('<Z')
300+
# top faces
301+
b_top = b.faces('>Z')
302+
303+
# inner face
304+
inner = extrude(circle(r), (0,0,w))
305+
306+
# add holes to the bottom and top face
307+
b_bot_hole = b_bot.addHole(inner.edges('<Z'))
308+
b_top_hole = b_top.addHole(inner.edges('>Z'))
309+
310+
# construct the final solid
311+
result = solid(
312+
b.remove(b_top, b_bot).faces(), #side faces
313+
b_bot_hole, # bottom with a hole
314+
inner, # inner cylinder face
315+
b_top_hole, # top with a hole
316+
)
317+
318+
If the base shape is more complicated, it is possible to use local sewing that
319+
takes into account on indicated elements of the context shape. This, however,
320+
necessitates a two step approach - first a shell needs to be explicitly sewn
321+
and only then the final solid can be constructed.
322+
323+
.. cadquery::
324+
325+
from cadquery.func import *
326+
327+
w = 1
328+
h = 0.1
329+
r = 0.9*w/2
330+
331+
# box
332+
b = box(w, w, w)
333+
# top face
334+
b_top = b.faces('>Z')
335+
336+
# protrusion
337+
feat_side = extrude(circle(r).moved(b_top.Center()), (0,0,h))
338+
feat_top = face(feat_side.edges('>Z'))
339+
feat = shell(feat_side, feat_top) # sew into a shell
340+
341+
# add hole to the box
342+
b_top_hole = b_top.addHole(feat.edges('<Z'))
343+
b = b.replace(b_top, b_top_hole)
344+
345+
# local sewing - only two faces are taken into account
346+
sh = shell(b_top_hole, feat.faces('<Z'), ctx=(b, feat))
347+
348+
# construct the final solid
349+
result = solid(sh)
350+
351+

0 commit comments

Comments
 (0)