Skip to content

Commit 0325474

Browse files
authored
findSolid errors (#655)
* findSolid returns None when no solids found Removed a line in the findSolid docstring about raising an error if no solids found. Added exceptions to several methods that were previously failing with AttributeErrors when they tried to treat the None object as a solid. * findSolid needed type hint Optional on it's return value * Type hint and test Workplane.findFace * Rewrite to raise ValueError in findSolid * I forgot about findFace again * add exceptions to docstrings
1 parent 0f32de9 commit 0325474

File tree

2 files changed

+141
-53
lines changed

2 files changed

+141
-53
lines changed

cadquery/cq.py

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ def split(self, keepTop: bool = False, keepBottom: bool = False) -> "Workplane":
236236
237237
:param boolean keepTop: True to keep the top, False or None to discard it
238238
:param boolean keepBottom: True to keep the bottom, False or None to discard it
239-
:raises: ValueError if keepTop and keepBottom are both false.
240-
:raises: ValueError if there is not a solid in the current stack or the parent chain
239+
:raises ValueError: if keepTop and keepBottom are both false.
240+
:raises ValueError: if there is no solid in the current stack or parent chain
241241
:returns: CQ object with the desired objects on the stack.
242242
243243
The most common operation splits a solid and keeps one half. This sample creates
@@ -250,11 +250,11 @@ def split(self, keepTop: bool = False, keepBottom: bool = False) -> "Workplane":
250250
c = c.faces(">Y").workplane(-0.5).split(keepTop=True)
251251
"""
252252

253-
solid = self.findSolid()
254-
255253
if (not keepTop) and (not keepBottom):
256254
raise ValueError("You have to keep at least one half")
257255

256+
solid = self.findSolid()
257+
258258
maxDim = solid.BoundingBox().DiagonalLength * 10.0
259259
topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim)
260260
bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim)
@@ -291,18 +291,20 @@ def combineSolids(
291291
combined and returned on the stack of the new object.
292292
"""
293293
# loop through current stack objects, and combine them
294-
toCombine = self.solids().vals()
294+
toCombine = cast(List[Solid], self.solids().vals())
295295

296296
if otherCQToCombine:
297-
for obj in otherCQToCombine.solids().vals():
297+
otherSolids = cast(List[Solid], otherCQToCombine.solids().vals())
298+
for obj in otherSolids:
298299
toCombine.append(obj)
299300

300301
if len(toCombine) < 1:
301302
raise ValueError("Cannot Combine: at least one solid required!")
302303

303304
# get context solid and we don't want to find our own objects
304-
ctxSolid = self.findSolid(searchStack=False, searchParents=True)
305-
305+
ctxSolid = self._findType(
306+
(Solid, Compound), searchStack=False, searchParents=True
307+
)
306308
if ctxSolid is None:
307309
ctxSolid = toCombine.pop(0)
308310

@@ -676,10 +678,9 @@ def findSolid(
676678
Finds the first solid object in the chain, searching from the current node
677679
backwards through parents until one is found.
678680
679-
:param searchStack: should objects on the stack be searched first.
681+
:param searchStack: should objects on the stack be searched first?
680682
:param searchParents: should parents be searched?
681-
:raises: ValueError if no solid is found in the current object or its parents,
682-
and errorOnEmpty is True
683+
:raises ValueError: if no solid is found
683684
684685
This function is very important for chains that are modifying a single parent object,
685686
most often a solid.
@@ -692,7 +693,15 @@ def findSolid(
692693
results with an object already on the stack.
693694
"""
694695

695-
return self._findType((Solid, Compound), searchStack, searchParents)
696+
found = self._findType((Solid, Compound), searchStack, searchParents)
697+
698+
if found is None:
699+
message = "on the stack or " if searchStack else ""
700+
raise ValueError(
701+
"Cannot find a solid {}in the parent chain".format(message)
702+
)
703+
704+
return found
696705

697706
def findFace(self, searchStack: bool = True, searchParents: bool = True) -> Face:
698707
"""
@@ -701,11 +710,16 @@ def findFace(self, searchStack: bool = True, searchParents: bool = True) -> Face
701710
702711
:param searchStack: should objects on the stack be searched first.
703712
:param searchParents: should parents be searched?
704-
:raises: ValueError if no face is found in the current object or its parents,
705-
and errorOnEmpty is True
713+
:returns: A face or None if no face is found.
706714
"""
707715

708-
return self._findType(Face, searchStack, searchParents)
716+
found = self._findType(Face, searchStack, searchParents)
717+
718+
if found is None:
719+
message = "on the stack or " if searchStack else ""
720+
raise ValueError("Cannot find a face {}in the parent chain".format(message))
721+
722+
return found
709723

710724
def _selectObjects(
711725
self,
@@ -1104,7 +1118,7 @@ def shell(
11041118
:param thickness: a positive float, representing the thickness of the desired shell.
11051119
Negative values shell inwards, positive values shell outwards.
11061120
:param kind: kind of joints, intersetion or arc (default: arc).
1107-
:raises: ValueError if the current stack contains objects that are not faces of a solid
1121+
:raises ValueError: if the current stack contains objects that are not faces of a solid
11081122
further up in the chain.
11091123
:returns: a CQ object with the resulting shelled solid selected.
11101124
@@ -1148,15 +1162,14 @@ def fillet(self, radius: float) -> "Workplane":
11481162
11491163
:param radius: the radius of the fillet, must be > zero
11501164
:type radius: positive float
1151-
:raises: ValueError if at least one edge is not selected
1152-
:raises: ValueError if the solid containing the edge is not in the chain
1165+
:raises ValueError: if at least one edge is not selected
1166+
:raises ValueError: if the solid containing the edge is not in the chain
11531167
:returns: cq object with the resulting solid selected.
11541168
11551169
This example will create a unit cube, with the top edges filleted::
11561170
11571171
s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1)
11581172
"""
1159-
# TODO: we will need much better edge selectors for this to work
11601173
# TODO: ensure that edges selected actually belong to the solid in the chain, otherwise,
11611174
# TODO: we segfault
11621175

@@ -1185,8 +1198,8 @@ def chamfer(self, length: float, length2: Optional[float] = None) -> "Workplane"
11851198
:param length2: optional parameter for asymmetrical chamfer
11861199
:type length: positive float
11871200
:type length2: positive float
1188-
:raises: ValueError if at least one edge is not selected
1189-
:raises: ValueError if the solid containing the edge is not in the chain
1201+
:raises ValueError: if at least one edge is not selected
1202+
:raises ValueError: if the solid containing the edge is not in the chain
11901203
:returns: cq object with the resulting solid selected.
11911204
11921205
This example will create a unit cube, with the top edges chamfered::
@@ -2501,17 +2514,16 @@ def close(self) -> "Workplane":
25012514
def largestDimension(self) -> float:
25022515
"""
25032516
Finds the largest dimension in the stack.
2517+
25042518
Used internally to create thru features, this is how you can compute
25052519
how long or wide a feature must be to make sure to cut through all of the material
2520+
2521+
:raises ValueError: if no solids or compounds are found
25062522
:return: A value representing the largest dimension of the first solid on the stack
25072523
"""
25082524
# Get all the solids contained within this CQ object
25092525
compound = self.findSolid()
25102526

2511-
# Protect against this being called on something like a blank workplane
2512-
if not compound:
2513-
return -1
2514-
25152527
return compound.BoundingBox().DiagonalLength
25162528

25172529
def cutEach(
@@ -2526,12 +2538,10 @@ def cutEach(
25262538
:param fcn: a function suitable for use in the eachpoint method: ie, that accepts a vector
25272539
:param useLocalCoords: same as for :py:meth:`eachpoint`
25282540
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
2541+
:raises ValueError: if no solids or compounds are found in the stack or parent chain
25292542
:return: a CQ object that contains the resulting solid
2530-
:raises: an error if there is not a context solid to cut from
25312543
"""
25322544
ctxSolid = self.findSolid()
2533-
if ctxSolid is None:
2534-
raise ValueError("Must have a solid in the chain to cut from!")
25352545

25362546
# will contain all of the counterbores as a single compound
25372547
results = cast(List[Shape], self.eachpoint(fcn, useLocalCoords).vals())
@@ -2920,7 +2930,9 @@ def _combineWithBase(self, obj: Shape) -> "Workplane":
29202930
:return: a new object that represents the result of combining the base object with obj,
29212931
or obj if one could not be found
29222932
"""
2923-
baseSolid = self.findSolid(searchParents=True)
2933+
baseSolid = self._findType(
2934+
(Solid, Compound), searchStack=True, searchParents=True
2935+
)
29242936
r = obj
29252937
if baseSolid is not None:
29262938
r = baseSolid.fuse(obj)
@@ -2936,7 +2948,9 @@ def _cutFromBase(self, obj: Shape) -> "Workplane":
29362948
:return: a new object that represents the result of combining the base object with obj,
29372949
or obj if one could not be found
29382950
"""
2939-
baseSolid = self.findSolid(searchParents=True)
2951+
baseSolid = self._findType(
2952+
(Solid, Compound), searchStack=True, searchParents=True
2953+
)
29402954
r = obj
29412955
if baseSolid is not None:
29422956
r = baseSolid.cut(obj)
@@ -2989,21 +3003,23 @@ def union(
29893003
"""
29903004

29913005
# first collect all of the items together
2992-
newS: Sequence[Shape]
3006+
newS: List[Shape]
29933007
if isinstance(toUnion, CQ):
29943008
newS = cast(List[Shape], toUnion.solids().vals())
29953009
if len(newS) < 1:
29963010
raise ValueError(
29973011
"CQ object must have at least one solid on the stack to union!"
29983012
)
29993013
elif isinstance(toUnion, (Solid, Compound)):
3000-
newS = (toUnion,)
3014+
newS = [toUnion]
30013015
else:
30023016
raise ValueError("Cannot union type '{}'".format(type(toUnion)))
30033017

30043018
# now combine with existing solid, if there is one
30053019
# look for parents to cut from
3006-
solidRef = self.findSolid(searchStack=True, searchParents=True)
3020+
solidRef = self._findType(
3021+
(Solid, Compound), searchStack=True, searchParents=True
3022+
)
30073023
if solidRef is not None:
30083024
r = solidRef.fuse(*newS, glue=glue, tol=tol)
30093025
elif len(newS) > 1:
@@ -3044,16 +3060,13 @@ def cut(
30443060
:param toCut: object to cut
30453061
:type toCut: a solid object, or a CQ object having a solid,
30463062
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3047-
:raises: ValueError if there is no solid to subtract from in the chain
3063+
:raises ValueError: if there is no solid to subtract from in the chain
30483064
:return: a CQ object with the resulting object selected
30493065
"""
30503066

30513067
# look for parents to cut from
30523068
solidRef = self.findSolid(searchStack=True, searchParents=True)
30533069

3054-
if solidRef is None:
3055-
raise ValueError("Cannot find solid to cut from")
3056-
30573070
solidToCut: Sequence[Shape]
30583071

30593072
if isinstance(toCut, CQ):
@@ -3092,16 +3105,13 @@ def intersect(
30923105
:param toIntersect: object to intersect
30933106
:type toIntersect: a solid object, or a CQ object having a solid,
30943107
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3095-
:raises: ValueError if there is no solid to intersect with in the chain
3108+
:raises ValueError: if there is no solid to intersect with in the chain
30963109
:return: a CQ object with the resulting object selected
30973110
"""
30983111

30993112
# look for parents to intersect with
31003113
solidRef = self.findSolid(searchStack=True, searchParents=True)
31013114

3102-
if solidRef is None:
3103-
raise ValueError("Cannot find solid to intersect with")
3104-
31053115
solidToIntersect: Sequence[Shape]
31063116

31073117
if isinstance(toIntersect, CQ):
@@ -3145,7 +3155,7 @@ def cutBlind(
31453155
<0 means in the negative direction
31463156
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
31473157
:param float taper: angle for optional tapered extrusion
3148-
:raises: ValueError if there is no solid to subtract from in the chain
3158+
:raises ValueError: if there is no solid to subtract from in the chain
31493159
:return: a CQ object with the resulting object selected
31503160
31513161
see :py:meth:`cutThruAll` to cut material from the entire part
@@ -3157,7 +3167,6 @@ def cutBlind(
31573167
toCut = self._extrude(distanceToCut, taper=taper)
31583168

31593169
# now find a solid in the chain
3160-
31613170
solidRef = self.findSolid()
31623171

31633172
s = solidRef.cut(toCut)
@@ -3176,7 +3185,7 @@ def cutThruAll(self, clean: bool = True, taper: float = 0) -> "Workplane":
31763185
from. cutThruAll always removes material from a part.
31773186
31783187
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3179-
:raises: ValueError if there is no solid to subtract from in the chain
3188+
:raises ValueError: if there is no solid to subtract from in the chain
31803189
:return: a CQ object with the resulting object selected
31813190
31823191
see :py:meth:`cutBlind` to cut material to a limited depth
@@ -3185,6 +3194,7 @@ def cutThruAll(self, clean: bool = True, taper: float = 0) -> "Workplane":
31853194
self.ctx.pendingWires = []
31863195

31873196
solidRef = self.findSolid()
3197+
31883198
rv = []
31893199
for solid in solidRef.Solids():
31903200
s = solid.dprism(None, wires, thruAll=True, additive=False, taper=-taper)
@@ -3209,7 +3219,9 @@ def loft(
32093219
r: Shape = Solid.makeLoft(wiresToLoft, ruled)
32103220

32113221
if combine:
3212-
parentSolid = self.findSolid(searchStack=False, searchParents=True)
3222+
parentSolid = self._findType(
3223+
(Solid, Compound), searchStack=False, searchParents=True
3224+
)
32133225
if parentSolid is not None:
32143226
r = parentSolid.fuse(r)
32153227

@@ -3751,14 +3763,12 @@ def section(self, height: float = 0.0) -> "Workplane":
37513763
Slices current solid at the given height.
37523764
37533765
:param float height: height to slice at (default: 0)
3766+
:raises ValueError: if no solids or compounds are found
37543767
:return: a CQ object with the resulting face(s).
37553768
"""
37563769

37573770
solidRef = self.findSolid(searchStack=True, searchParents=True)
37583771

3759-
if solidRef is None:
3760-
raise ValueError("Cannot find solid to slice")
3761-
37623772
plane = Face.makePlane(
37633773
basePnt=self.plane.origin + self.plane.zDir * height, dir=self.plane.zDir
37643774
)

0 commit comments

Comments
 (0)