Skip to content

Commit 9ceea9c

Browse files
authored
Merge pull request #934 from lorenzncode/tags-merge
Merge tags on boolean ops
2 parents 6b01d78 + ef8b320 commit 9ceea9c

File tree

2 files changed

+100
-21
lines changed

2 files changed

+100
-21
lines changed

cadquery/cq.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ def split(self: T, *args, **kwargs) -> T:
307307
else [v for v in arg.vals() if isinstance(v, Shape)]
308308
)
309309
rv = [solid.split(*tools)]
310+
if isinstance(arg, Workplane):
311+
self._mergeTags(arg)
310312

311313
# split using the current workplane
312314
else:
@@ -456,6 +458,7 @@ def add(self, obj):
456458
self.objects.extend(obj)
457459
elif isinstance(obj, Workplane):
458460
self.objects.extend(obj.objects)
461+
self._mergeTags(obj)
459462
else:
460463
self.objects.append(obj)
461464
return self
@@ -471,20 +474,31 @@ def val(self) -> CQObject:
471474

472475
def _getTagged(self, name: str) -> "Workplane":
473476
"""
474-
Search the parent chain for a an object with tag == name.
477+
Search the parent chain for an object with tag == name.
475478
476479
:param name: the tag to search for
477-
:type name: string
478-
:returns: the CQ object with tag == name
480+
:returns: the Workplane object with tag == name
479481
:raises: ValueError if no object tagged name
480482
"""
481483
rv = self.ctx.tags.get(name)
482484

483485
if rv is None:
484-
raise ValueError(f"No CQ object named {name} in chain")
486+
raise ValueError(f"No Workplane object named {name} in chain")
485487

486488
return rv
487489

490+
def _mergeTags(self: T, obj: "Workplane") -> T:
491+
"""
492+
Merge tags
493+
494+
This is automatically called when performing boolean ops.
495+
"""
496+
497+
if self.ctx != obj.ctx:
498+
self.ctx.tags = {**obj.ctx.tags, **self.ctx.tags}
499+
500+
return self
501+
488502
def toOCC(self) -> Any:
489503
"""
490504
Directly returns the wrapped OCCT object.
@@ -3250,22 +3264,23 @@ def union(
32503264
If there is no current solid, the items in toUnion are unioned together.
32513265
32523266
:param toUnion:
3253-
:type toUnion: a solid object, or a CQ object having a solid,
3254-
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape (default True)
3255-
:param boolean glue: use a faster gluing mode for non-overlapping shapes (default False)
3256-
:param float tol: tolerance value for fuzzy bool operation mode (default None)
3267+
:type toUnion: a solid object, or a Workplane object having a solid,
3268+
:param clean: call :py:meth:`clean` afterwards to have a clean shape (default True)
3269+
:param glue: use a faster gluing mode for non-overlapping shapes (default False)
3270+
:param tol: tolerance value for fuzzy bool operation mode (default None)
32573271
:raises: ValueError if there is no solid to add to in the chain
3258-
:return: a CQ object with the resulting object selected
3272+
:return: a Workplane object with the resulting object selected
32593273
"""
32603274

32613275
# first collect all of the items together
32623276
newS: List[Shape]
3263-
if isinstance(toUnion, CQ):
3277+
if isinstance(toUnion, Workplane):
32643278
newS = cast(List[Shape], toUnion.solids().vals())
32653279
if len(newS) < 1:
32663280
raise ValueError(
3267-
"CQ object must have at least one solid on the stack to union!"
3281+
"Workplane object must have at least one solid on the stack to union!"
32683282
)
3283+
self._mergeTags(toUnion)
32693284
elif isinstance(toUnion, (Solid, Compound)):
32703285
newS = [toUnion]
32713286
else:
@@ -3315,19 +3330,20 @@ def cut(
33153330
Cuts the provided solid from the current solid, IE, perform a solid subtraction.
33163331
33173332
:param toCut: object to cut
3318-
:type toCut: a solid object, or a CQ object having a solid,
3319-
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3333+
:type toCut: a solid object, or a Workplane object having a solid,
3334+
:param clean: call :py:meth:`clean` afterwards to have a clean shape
33203335
:raises ValueError: if there is no solid to subtract from in the chain
3321-
:return: a CQ object with the resulting object selected
3336+
:return: a Workplane object with the resulting object selected
33223337
"""
33233338

33243339
# look for parents to cut from
33253340
solidRef = self.findSolid(searchStack=True, searchParents=True)
33263341

33273342
solidToCut: Sequence[Shape]
33283343

3329-
if isinstance(toCut, CQ):
3344+
if isinstance(toCut, Workplane):
33303345
solidToCut = _selectShapes(toCut.vals())
3346+
self._mergeTags(toCut)
33313347
elif isinstance(toCut, (Solid, Compound)):
33323348
solidToCut = (toCut,)
33333349
else:
@@ -3360,19 +3376,20 @@ def intersect(
33603376
Intersects the provided solid from the current solid.
33613377
33623378
:param toIntersect: object to intersect
3363-
:type toIntersect: a solid object, or a CQ object having a solid,
3364-
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3379+
:type toIntersect: a solid object, or a Workplane object having a solid,
3380+
:param clean: call :py:meth:`clean` afterwards to have a clean shape
33653381
:raises ValueError: if there is no solid to intersect with in the chain
3366-
:return: a CQ object with the resulting object selected
3382+
:return: a Workplane object with the resulting object selected
33673383
"""
33683384

33693385
# look for parents to intersect with
33703386
solidRef = self.findSolid(searchStack=True, searchParents=True)
33713387

33723388
solidToIntersect: Sequence[Shape]
33733389

3374-
if isinstance(toIntersect, CQ):
3390+
if isinstance(toIntersect, Workplane):
33753391
solidToIntersect = _selectShapes(toIntersect.vals())
3392+
self._mergeTags(toIntersect)
33763393
elif isinstance(toIntersect, (Solid, Compound)):
33773394
solidToIntersect = (toIntersect,)
33783395
else:

tests/test_cadquery.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3155,7 +3155,7 @@ def testExtrudeUntilFace(self):
31553155
nb_faces = wp.faces().size()
31563156
wp = wp_ref.faces(">X[1]").workplane().rect(1, 1).extrude("next")
31573157

3158-
self.assertAlmostEquals(wp_ref_extrude.val().Volume(), wp.val().Volume())
3158+
self.assertAlmostEqual(wp_ref_extrude.val().Volume(), wp.val().Volume())
31593159
self.assertTrue(wp.faces().size() - nb_faces == 4)
31603160

31613161
# Test tapered option and both option
@@ -3330,7 +3330,7 @@ def testCutBlindUntilFace(self):
33303330
.cutBlind("last")
33313331
)
33323332

3333-
self.assertAlmostEquals(wp_ref_regular_cut.val().Volume(), wp.val().Volume())
3333+
self.assertAlmostEqual(wp_ref_regular_cut.val().Volume(), wp.val().Volume())
33343334

33353335
wp_last = (
33363336
wp_ref.faces(">X[4]")
@@ -5062,3 +5062,65 @@ def circumradius(n, a):
50625062
self.assertEqual(
50635063
vs[3].toTuple(), approx((a, -a * math.tan(math.radians(45)), 0))
50645064
)
5065+
5066+
def test_MergeTags(self):
5067+
5068+
a = Workplane().box(1, 1, 1)
5069+
b = (
5070+
Workplane(origin=(1, 0, 0))
5071+
.box(1, 1, 1)
5072+
.vertices(">X and >Y and >Z")
5073+
.tag("box_vertex")
5074+
.end(2)
5075+
)
5076+
a = a.add(b)
5077+
assert a.vertices(tag="box_vertex").val().Center().toTuple() == approx(
5078+
(1.5, 0.5, 0.5)
5079+
)
5080+
5081+
a = Workplane().box(4, 4, 4)
5082+
b = Workplane(origin=(0, 0, 1)).box(2, 2, 2).faces("<Z").tag("box2_face").end()
5083+
a = a.cut(b)
5084+
assert a.val().Volume() == approx(4 ** 3 - 2 ** 3)
5085+
a = a.faces(tag="box2_face").wires().toPending().extrude(4)
5086+
assert a.val().Volume() == approx(4 ** 3 + 2 ** 3)
5087+
5088+
a = Workplane().sphere(2)
5089+
b = Workplane().cylinder(4, 1).tag("cyl")
5090+
a = a.intersect(b)
5091+
assert len(a.solids(tag="cyl").val().Solids()) == 1
5092+
5093+
a = Workplane().box(4, 4, 4)
5094+
b = (
5095+
Workplane()
5096+
.box(2, 5, 5, centered=(False, True, True))
5097+
.faces(">X")
5098+
.workplane()
5099+
.tag("splitter")
5100+
.end(2)
5101+
)
5102+
a = a.split(b)
5103+
a = a.solids("<X")
5104+
assert a.val().Volume() == approx((4 ** 3) / 2.0)
5105+
a = a.workplaneFromTagged("splitter").rect(4, 4).extrude(until="next")
5106+
assert a.val().Volume() == approx((4 ** 3))
5107+
5108+
a = Workplane().box(4, 4, 4)
5109+
b = Workplane(origin=(0, 0, 3)).box(2, 2, 2).faces(">Z").tag("box2_face").end()
5110+
a = a.union(b)
5111+
a = a.faces(tag="box2_face").workplane(offset=0.5).box(1, 1, 1)
5112+
assert a.val().Volume() == approx(4 ** 3 + 2 ** 3 + 1)
5113+
5114+
# tag name conflict; keep tag from left side of boolean
5115+
a = Workplane().box(1, 1, 1).faces(">Z").workplane().tag("zface").end(2)
5116+
b = (
5117+
Workplane(origin=(1, 0, 0))
5118+
.box(1, 1, 2)
5119+
.faces(">Z")
5120+
.workplane()
5121+
.tag("zface")
5122+
.end(2)
5123+
)
5124+
a = a.union(b)
5125+
a = a.workplaneFromTagged("zface").circle(0.2)
5126+
assert a.edges("%CIRCLE").val().Center().toTuple() == approx((0, 0, 0.5))

0 commit comments

Comments
 (0)