Skip to content

Commit dde33e3

Browse files
Face sweep for free function API (#1622)
* Face sweep * Add multisection support * black fix * Support lofting faces * Add some tests * Improve coverage * Fix test * Small doc tweaks * Docstring tweak * Docstring updates
1 parent edabe5e commit dde33e3

File tree

3 files changed

+168
-41
lines changed

3 files changed

+168
-41
lines changed

cadquery/occ_impl/shapes.py

Lines changed: 134 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4222,7 +4222,7 @@ def _get_edges(s: Shape) -> Iterable[Shape]:
42224222

42234223
def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]:
42244224
"""
4225-
Get lists or wires for sweeping or lofting.
4225+
Get lists of wires for sweeping or lofting.
42264226
"""
42274227

42284228
wire_lists: List[List[Wire]] = []
@@ -4237,6 +4237,23 @@ def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]:
42374237
return wire_lists
42384238

42394239

4240+
def _get_face_lists(s: Sequence[Shape]) -> List[List[Face]]:
4241+
"""
4242+
Get lists of faces for sweeping or lofting.
4243+
"""
4244+
4245+
face_lists: List[List[Face]] = []
4246+
4247+
for el in s:
4248+
if not face_lists:
4249+
face_lists = [[f] for f in el.Faces()]
4250+
else:
4251+
for face_list, f in zip(face_lists, el.Faces()):
4252+
face_list.append(f)
4253+
4254+
return face_lists
4255+
4256+
42404257
def _normalize(s: Shape) -> Shape:
42414258
"""
42424259
Apply some normalizations:
@@ -4482,7 +4499,7 @@ def rect(w: float, h: float) -> Shape:
44824499
@multimethod
44834500
def spline(*pts: VectorLike, tol: float = 1e-6, periodic: bool = False) -> Shape:
44844501
"""
4485-
Construct a polygon (closed polyline) from points.
4502+
Construct a spline from points.
44864503
"""
44874504

44884505
data = _pts_to_harray(pts)
@@ -4910,74 +4927,165 @@ def offset(s: Shape, t: float, cap=True, tol: float = 1e-6) -> Shape:
49104927
@multimethod
49114928
def sweep(s: Shape, path: Shape, cap: bool = False) -> Shape:
49124929
"""
4913-
Sweep edge or wire along a path.
4930+
Sweep edge, wire or face along a path. For faces cap has no effect.
4931+
Do not mix faces with other types.
49144932
"""
49154933

49164934
spine = _get_one_wire(path)
49174935

49184936
results = []
49194937

4920-
for w in _get_wires(s):
4921-
builder = BRepOffsetAPI_MakePipeShell(spine.wrapped)
4922-
builder.Add(w.wrapped, False, False)
4923-
builder.Build()
4938+
# try to get faces
4939+
faces = s.Faces()
49244940

4925-
if cap:
4926-
builder.MakeSolid()
4941+
# if faces were supplied
4942+
if faces:
4943+
for f in faces:
4944+
tmp = sweep(f.outerWire(), path, True)
49274945

4928-
results.append(builder.Shape())
4946+
# if needed subtract two sweeps
4947+
inner_wires = f.innerWires()
4948+
if inner_wires:
4949+
tmp -= sweep(compound(inner_wires), path, True)
4950+
4951+
results.append(tmp.wrapped)
4952+
4953+
# otherwise sweep wires
4954+
else:
4955+
for w in _get_wires(s):
4956+
builder = BRepOffsetAPI_MakePipeShell(spine.wrapped)
4957+
builder.Add(w.wrapped, False, False)
4958+
builder.Build()
4959+
4960+
if cap:
4961+
builder.MakeSolid()
4962+
4963+
results.append(builder.Shape())
49294964

49304965
return _compound_or_shape(results)
49314966

49324967

49334968
@sweep.register
49344969
def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape:
49354970
"""
4936-
Sweep edges or wires along a path, chaining sections are supported.
4971+
Sweep edges, wires or faces along a path, multiple sections are supported.
4972+
For faces cap has no effect. Do not mix faces with other types.
49374973
"""
49384974

49394975
spine = _get_one_wire(path)
49404976

49414977
results = []
49424978

4943-
# construct sweeps
4944-
for el in _get_wire_lists(s):
4979+
# try to construct sweeps using faces
4980+
for el in _get_face_lists(s):
4981+
# build outer part
49454982
builder = BRepOffsetAPI_MakePipeShell(spine.wrapped)
49464983

4947-
for w in el:
4948-
builder.Add(w.wrapped, False, False)
4984+
for f in el:
4985+
builder.Add(f.outerWire().wrapped, False, False)
49494986

49504987
builder.Build()
4988+
builder.MakeSolid()
49514989

4952-
if cap:
4953-
builder.MakeSolid()
4990+
# build inner parts
4991+
builders_inner = []
49544992

4955-
results.append(builder.Shape())
4993+
# initialize builders
4994+
for w in el[0].innerWires():
4995+
builder_inner = BRepOffsetAPI_MakePipeShell(spine.wrapped)
4996+
builder_inner.Add(w.wrapped, False, False)
4997+
builders_inner.append(builder_inner)
4998+
4999+
# add remaining sections
5000+
for f in el[1:]:
5001+
for builder_inner, w in zip(builders_inner, f.innerWires()):
5002+
builder_inner.Add(w.wrapped, False, False)
5003+
5004+
# actually build
5005+
inner_parts = []
5006+
5007+
for builder_inner in builders_inner:
5008+
builder_inner.Build()
5009+
builder_inner.MakeSolid()
5010+
inner_parts.append(Shape(builder_inner.Shape()))
5011+
5012+
results.append((Shape(builder.Shape()) - compound(inner_parts)).wrapped)
5013+
5014+
# if no faces were provided try with wires
5015+
if not results:
5016+
# construct sweeps
5017+
for el in _get_wire_lists(s):
5018+
builder = BRepOffsetAPI_MakePipeShell(spine.wrapped)
5019+
5020+
for w in el:
5021+
builder.Add(w.wrapped, False, False)
5022+
5023+
builder.Build()
5024+
5025+
if cap:
5026+
builder.MakeSolid()
5027+
5028+
results.append(builder.Shape())
49565029

49575030
return _compound_or_shape(results)
49585031

49595032

49605033
@multimethod
49615034
def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape:
49625035
"""
4963-
Loft edges or wires.
5036+
Loft edges, wires or faces. For faces cap has no effect. Do not mix faces with other types.
49645037
"""
49655038

49665039
results = []
4967-
4968-
# construct lofts
49695040
builder = BRepOffsetAPI_ThruSections()
49705041

4971-
for el in _get_wire_lists(s):
4972-
builder.Init(cap, ruled)
5042+
# try to construct lofts using faces
5043+
for el in _get_face_lists(s):
5044+
# build outer part
5045+
builder.Init(True, ruled)
49735046

4974-
for w in el:
4975-
builder.AddWire(w.wrapped)
5047+
for f in el:
5048+
builder.AddWire(f.outerWire().wrapped)
49765049

49775050
builder.Build()
49785051
builder.Check()
49795052

4980-
results.append(builder.Shape())
5053+
builders_inner = []
5054+
5055+
# initialize builders
5056+
for w in el[0].innerWires():
5057+
builder_inner = BRepOffsetAPI_ThruSections()
5058+
builder_inner.Init(True, ruled)
5059+
builder_inner.AddWire(w.wrapped)
5060+
builders_inner.append(builder_inner)
5061+
5062+
# add remaining sections
5063+
for f in el[1:]:
5064+
for builder_inner, w in zip(builders_inner, f.innerWires()):
5065+
builder_inner.AddWire(w.wrapped)
5066+
5067+
# actually build
5068+
inner_parts = []
5069+
5070+
for builder_inner in builders_inner:
5071+
builder_inner.Build()
5072+
builder_inner.Check()
5073+
inner_parts.append(Shape(builder_inner.Shape()))
5074+
5075+
results.append((Shape(builder.Shape()) - compound(inner_parts)).wrapped)
5076+
5077+
# otherwise construct using wires
5078+
if not results:
5079+
for el in _get_wire_lists(s):
5080+
builder.Init(cap, ruled)
5081+
5082+
for w in el:
5083+
builder.AddWire(w.wrapped)
5084+
5085+
builder.Build()
5086+
builder.Check()
5087+
5088+
results.append(builder.Shape())
49815089

49825090
return _compound_or_shape(results)
49835091

doc/free-func.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,26 +197,27 @@ Free function API currently supports :meth:`~cadquery.occ_impl.shapes.extrude`,
197197
.. cadquery::
198198

199199
from cadquery.occ_impl.shapes import *
200-
200+
201201
r = rect(1,0.5)
202+
f = face(r, circle(0.2).moved(0.2), rect(0.2, 0.4).moved(-0.2))
202203
c = circle(0.2)
203-
p = spline([(0,0,0), (0,1,2)], [(0,0,1), (0,1,1)])
204-
204+
p = spline([(0,0,0), (0,-1,2)], [(0,0,1), (0,-1,1)])
205+
205206
# extrude
206207
s1 = extrude(r, (0,0,2))
207208
s2 = extrude(fill(r), (0,0,1))
208-
209+
209210
# sweep
210211
s3 = sweep(r, p)
211-
s4 = sweep(r, p, cap=True)
212-
212+
s4 = sweep(f, p)
213+
213214
# loft
214215
s5 = loft(r, c.moved(z=2))
215216
s6 = loft(r, c.moved(z=1), cap=True)\
216-
217+
217218
# revolve
218219
s7 = revolve(fill(r), (0.5, 0, 0), (0, 1, 0), 90)
219-
220+
220221
results = (s1, s2, s3, s4, s5, s6, s7)
221222
result = compound([el.moved(2*i) for i,el in enumerate(results)])
222223

tests/test_free_functions.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -488,23 +488,34 @@ def test_sweep():
488488
w1 = rect(1, 1)
489489
w2 = w1.moved(Location(0, 0, 1))
490490

491+
f1 = face(rect(1, 1), circle(0.25))
492+
f2 = face(rect(2, 1), ellipse(0.9, 0.45)).moved(x=2, z=2, ry=90)
493+
f3 = face(rect(1, 1))
494+
491495
p1 = segment((0, 0, 0), (0, 0, 1))
492496
p2 = spline((w1.Center(), w2.Center()), ((-0.5, 0, 1), (0.5, 0, 1)))
497+
p3 = spline((f1.Center(), f2.Center()), ((0, 0, 1), (1, 0, 0)))
493498

494-
r1 = sweep(w1, p1)
495-
r2 = sweep((w1, w2), p1)
496-
r3 = sweep(w1, p1, cap=True)
497-
r4 = sweep((w1, w2), p1, cap=True)
498-
r5 = sweep((w1, w2), p2, cap=True)
499+
r1 = sweep(w1, p1) # simple sweep
500+
r2 = sweep((w1, w2), p1) # multi-section sweep
501+
r3 = sweep(w1, p1, cap=True) # simple with cap
502+
r4 = sweep((w1, w2), p1, cap=True) # multi-section with cap
503+
r5 = sweep((w1, w2), p2, cap=True) # see above
504+
r6 = sweep(f1, p3) # simple face sweep
505+
r7 = sweep((f1, f2), p3) # multi-section face sweep
506+
r8 = sweep(f3, p3) # simplest face sweep (no inner wires)
499507

500-
assert_all_valid(r1, r2, r3, r4, r5)
508+
assert_all_valid(r1, r2, r3, r4, r5, r6, r7, r8)
501509

502510
assert r1.Area() == approx(4)
503511
assert r2.Area() == approx(4)
504512
assert r3.Volume() == approx(1)
505513
assert r4.Volume() == approx(1)
506514
assert r5.Volume() > 0
507515
assert len(r5.Faces()) == 6
516+
assert len(r6.Faces()) == 7
517+
assert len(r7.Faces()) == 7
518+
assert len(r8.Faces()) == 6
508519

509520

510521
def test_loft():
@@ -516,16 +527,23 @@ def test_loft():
516527
w4 = segment((0, 0), (1, 0))
517528
w5 = w4.moved(0, 0, 1)
518529

530+
f1 = face(rect(2, 1), rect(0.5, 0.2).moved(x=0.5), rect(0.5, 0.2).moved(x=-0.5))
531+
f2 = face(rect(3, 2), circle(0.5).moved(x=0.7), circle(0.5).moved(x=-0.7)).moved(
532+
z=1
533+
)
534+
519535
r1 = loft(w1, w2, w3) # loft
520536
r2 = loft(w1, w2, w3, ruled=True) # ruled loft
521537
r3 = loft([w1, w2, w3]) # overload
522538
r4 = loft(w1, w2, w3, cap=True) # capped loft
523539
r5 = loft(w4, w5) # loft with open edges
540+
r6 = loft(f1, f2) # loft with faces
524541

525-
assert_all_valid(r1, r2, r3, r4, r5)
542+
assert_all_valid(r1, r2, r3, r4, r5, r6)
526543

527544
assert len(r1.Faces()) == 1
528545
assert len(r2.Faces()) == 2
529546
assert len((r1 - r3).Faces()) == 0
530547
assert r4.Volume() > 0
531548
assert r5.Area() == approx(1)
549+
assert len(r6.Faces()) == 16

0 commit comments

Comments
 (0)