Skip to content

Commit 8c68faa

Browse files
Add extend, speed up distance and improve imprint (#1797)
* 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 * Apply suggestion. * Fix docstring --------- Co-authored-by: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com>
1 parent db62a1d commit 8c68faa

File tree

3 files changed

+207
-9
lines changed

3 files changed

+207
-9
lines changed

cadquery/occ_impl/shapes.py

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
Protocol,
1616
)
1717

18+
from typing_extensions import Self
19+
1820
from io import BytesIO
1921

2022
from vtkmodules.vtkCommonDataModel import vtkPolyData
@@ -192,7 +194,11 @@
192194

193195
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
194196

195-
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
197+
from OCP.BRepTools import (
198+
BRepTools,
199+
BRepTools_WireExplorer,
200+
BRepTools_ReShape,
201+
)
196202

197203
from OCP.LocOpe import LocOpe_DPrism
198204

@@ -1397,14 +1403,19 @@ def distance(self, other: "Shape") -> float:
13971403
Minimal distance between two shapes
13981404
"""
13991405

1400-
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
1406+
dist_calc = BRepExtrema_DistShapeShape(self.wrapped, other.wrapped)
1407+
dist_calc.SetMultiThread(True)
1408+
1409+
return dist_calc.Value()
14011410

14021411
def distances(self, *others: "Shape") -> Iterator[float]:
14031412
"""
14041413
Minimal distances to between self and other shapes
14051414
"""
14061415

14071416
dist_calc = BRepExtrema_DistShapeShape()
1417+
dist_calc.SetMultiThread(True)
1418+
14081419
dist_calc.LoadS1(self.wrapped)
14091420

14101421
for s in others:
@@ -1689,6 +1700,40 @@ def __setstate__(self, data: Tuple[BytesIO, bool]):
16891700
self.wrapped = wrapped
16901701
self.forConstruction = data[1]
16911702

1703+
def replace(self, old: "Shape", *new: "Shape") -> Self:
1704+
"""
1705+
Replace old subshape with new subshapes.
1706+
"""
1707+
1708+
tools: List[Shape] = []
1709+
1710+
for el in new:
1711+
if isinstance(el, Compound):
1712+
tools.extend(el)
1713+
else:
1714+
tools.append(el)
1715+
1716+
bldr = BRepTools_ReShape()
1717+
bldr.Replace(old.wrapped, compound(tools).wrapped)
1718+
1719+
rv = bldr.Apply(self.wrapped)
1720+
1721+
return self.__class__(rv)
1722+
1723+
def remove(self, *subshape: "Shape") -> Self:
1724+
"""
1725+
Remove subshapes.
1726+
"""
1727+
1728+
bldr = BRepTools_ReShape()
1729+
1730+
for el in subshape:
1731+
bldr.Remove(el.wrapped)
1732+
1733+
rv = bldr.Apply(self.wrapped)
1734+
1735+
return self.__class__(rv)
1736+
16921737

16931738
class ShapeProtocol(Protocol):
16941739
@property
@@ -3484,6 +3529,32 @@ def isolines(
34843529

34853530
return [self.isoline(p, direction) for p in params]
34863531

3532+
def extend(
3533+
self,
3534+
d: float,
3535+
umin: bool = True,
3536+
umax: bool = True,
3537+
vmin: bool = True,
3538+
vmax: bool = True,
3539+
) -> "Face":
3540+
"""
3541+
Extend a face. Does not work well in periodic directions.
3542+
3543+
:param d: length of the extension.
3544+
:param umin: extend along the umin isoline.
3545+
:param umax: extend along the umax isoline.
3546+
:param vmin: extend along the vmin isoline.
3547+
:param vmax: extend along the vmax isoline.
3548+
"""
3549+
3550+
# convert to NURBS if needed
3551+
tmp = self.toNURBS() if self.geomType() != "BSPLINE" else self
3552+
3553+
rv = TopoDS_Face()
3554+
BRepLib.ExtendFace_s(tmp.wrapped, d, umin, umax, vmin, vmax, rv)
3555+
3556+
return self.__class__(rv)
3557+
34873558

34883559
class Shell(Shape):
34893560
"""
@@ -4357,6 +4428,25 @@ def innerShells(self) -> List[Shell]:
43574428

43584429
return [s for s in self.Shells() if not s.isSame(outer)]
43594430

4431+
def addCavity(self, *shells: Union[Shell, "Solid"]) -> Self:
4432+
"""
4433+
Add one or more cavities.
4434+
"""
4435+
4436+
builder = BRepBuilderAPI_MakeSolid(self.wrapped)
4437+
4438+
# if a solid is provided only outer shell is added
4439+
for sh in shells:
4440+
builder.Add(
4441+
sh.wrapped if isinstance(sh, Shell) else sh.outerShell().wrapped
4442+
)
4443+
4444+
# fix orientations
4445+
sf = ShapeFix_Solid(builder.Solid())
4446+
sf.Perform()
4447+
4448+
return self.__class__(sf.Solid())
4449+
43604450

43614451
class CompSolid(Shape, Mixin3D):
43624452
"""
@@ -4385,13 +4475,15 @@ def _makeCompound(listOfShapes: Iterable[TopoDS_Shape]) -> TopoDS_Compound:
43854475

43864476
return comp
43874477

4388-
def remove(self, shape: Shape):
4478+
def remove(self, *shape: Shape):
43894479
"""
4390-
Remove the specified shape.
4480+
Remove the specified shapes.
43914481
"""
43924482

43934483
comp_builder = TopoDS_Builder()
4394-
comp_builder.Remove(self.wrapped, shape.wrapped)
4484+
4485+
for s in shape:
4486+
comp_builder.Remove(self.wrapped, s.wrapped)
43954487

43964488
@classmethod
43974489
def makeCompound(cls, listOfShapes: Iterable[Shape]) -> "Compound":
@@ -5672,11 +5764,17 @@ def imprint(
56725764
# collect shapes present in the history dict
56735765
for k, v in history.items():
56745766
if isinstance(k, str):
5675-
history[k] = _compound_or_shape(list(images.Find(v.wrapped)))
5767+
try:
5768+
history[k] = _compound_or_shape(list(images.Find(v.wrapped)))
5769+
except Standard_NoSuchObject:
5770+
pass
56765771

56775772
# store all top-level shape relations
56785773
for s in shapes:
5679-
history[s] = _compound_or_shape(list(images.Find(s.wrapped)))
5774+
try:
5775+
history[s] = _compound_or_shape(list(images.Find(s.wrapped)))
5776+
except Standard_NoSuchObject:
5777+
pass
56805778

56815779
return _compound_or_shape(builder.Shape())
56825780

@@ -5928,6 +6026,7 @@ def sweep(
59286026
def _make_builder():
59296027

59306028
rv = BRepOffsetAPI_MakePipeShell(spine.wrapped)
6029+
59316030
if aux:
59326031
rv.SetMode(_get_one_wire(aux).wrapped, True)
59336032
else:
@@ -6142,6 +6241,15 @@ def closest(s1: Shape, s2: Shape) -> Tuple[Vector, Vector]:
61426241
"""
61436242
Closest points between two shapes.
61446243
"""
6145-
ext = BRepExtrema_DistShapeShape(s1.wrapped, s2.wrapped)
6244+
# configure
6245+
ext = BRepExtrema_DistShapeShape()
6246+
ext.SetMultiThread(True)
6247+
6248+
# load shapes
6249+
ext.LoadS1(s1.wrapped)
6250+
ext.LoadS2(s2.wrapped)
6251+
6252+
# perform
6253+
assert ext.Perform()
61466254

61476255
return Vector(ext.PointOnShape1(1)), Vector(ext.PointOnShape2(1))

tests/test_free_functions.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
from OCP.BOPAlgo import BOPAlgo_CheckStatus
5353

54-
from pytest import approx, raises
54+
from pytest import approx, raises, fixture
5555
from math import pi
5656

5757
#%% test utils
@@ -430,6 +430,7 @@ def test_imprint():
430430

431431
assert len(res_glue_full.Faces()) == len(compound(b1, b2).Faces()) - 1
432432

433+
# imprint with history
433434
history = dict(b1=b1, b3=b3)
434435
res_glue_partial = imprint(b1, b3, glue="partial", history=history)
435436

@@ -439,6 +440,45 @@ def test_imprint():
439440
assert len(b1_imp.Faces()) == len(b1.Faces()) + 1
440441
assert len(res_glue_partial.Faces()) == len(b1_imp.Faces() + b3_imp.Faces()) - 1
441442

443+
# imprint with faulty history
444+
history = dict(b2=b2)
445+
# this does not raise!
446+
res_glue_partial = imprint(b1, b3, glue="partial", history=history)
447+
448+
assert b2 not in history
449+
450+
451+
@fixture
452+
def patch_find(monkeypatch):
453+
"""
454+
Fixture for throwing exception during imprinting.
455+
"""
456+
457+
from OCP.TopTools import TopTools_DataMapOfShapeListOfShape
458+
from OCP.Standard import Standard_NoSuchObject
459+
from OCP.BOPAlgo import BOPAlgo_Builder
460+
461+
def dummy(x):
462+
463+
raise ValueError("A")
464+
raise Standard_NoSuchObject
465+
466+
class DummyMap(TopTools_DataMapOfShapeListOfShape):
467+
def Find(self, x):
468+
raise Standard_NoSuchObject
469+
470+
monkeypatch.setattr(BOPAlgo_Builder, "Images", lambda x: DummyMap())
471+
472+
473+
def test_imprint_error(patch_find):
474+
475+
b1 = box(1, 1, 1)
476+
b2 = b1.moved(x=1)
477+
478+
history = {}
479+
480+
_ = imprint(b1, b2, history=history)
481+
442482

443483
def test_setThreads():
444484

tests/test_shapes.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
cylinder,
1414
ellipse,
1515
spline,
16+
sweep,
1617
)
1718

1819
from pytest import approx, raises
@@ -255,3 +256,52 @@ def test_isolines():
255256

256257
assert isos_u[0].Length() == approx(2)
257258
assert isos_v[0].Length() == approx(pi)
259+
260+
261+
def test_extend():
262+
263+
f = sweep(spline((0, 0), (0, 1), (2, 0)), spline((0, 0, 0), (0, 1, 1), (0, 1, 5)))
264+
f_ext = f.extend(1)
265+
266+
assert f_ext.Area() > f.Area()
267+
268+
269+
def test_remove():
270+
271+
b = box(2, 2, 2) - box(1, 1, 1).moved(z=0.5)
272+
273+
assert len(b.Faces()) == 12
274+
275+
br = b.remove(*b.innerShells())
276+
277+
assert len(br.Faces()) == 6
278+
assert br.isValid()
279+
280+
281+
def test_addCavity():
282+
283+
b1 = box(2, 2, 2)
284+
b2 = box(1, 1, 1).moved(z=0.5)
285+
286+
br = b1.addCavity(b2)
287+
288+
assert len(br.Faces()) == 12
289+
assert len(br.Shells()) == 2
290+
assert br.isValid()
291+
292+
293+
def test_replace():
294+
295+
b = box(1, 1, 1)
296+
f_top = b.faces(">Z")
297+
f_top_split = f_top / plane(0.5, 0.5).moved(f_top.Center())
298+
299+
br1 = b.replace(f_top, f_top_split)
300+
301+
assert len(br1.Faces()) == len(b.Faces()) + 1
302+
assert br1.isValid()
303+
304+
br2 = b.replace(f_top, *f_top_split) # invoke with individual faces
305+
306+
assert len(br2.Faces()) == len(b.Faces()) + 1
307+
assert br2.isValid()

0 commit comments

Comments
 (0)