Skip to content

Commit

Permalink
Type annotations for shapes (#391)
Browse files Browse the repository at this point in the history
* Started annotating shapes

* shapes pass mypy check

* Mypy completely green on CQ

* Safer downcast

* Added additonal test
  • Loading branch information
adam-urbanczyk authored Jul 6, 2020
1 parent dd75dcb commit 9808cce
Show file tree
Hide file tree
Showing 4 changed files with 581 additions and 317 deletions.
102 changes: 58 additions & 44 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Iterable,
Callable,
List,
cast,
)
from typing_extensions import Literal

Expand All @@ -55,6 +56,11 @@
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]


def _selectShapes(objects: Iterable[Any]) -> List[Shape]:

return [el for el in objects if isinstance(el, Shape)]


class CQContext(object):
"""
A shared context for modeling.
Expand Down Expand Up @@ -328,9 +334,9 @@ def combineSolids(
ctxSolid = toCombine.pop(0)

# now combine them all. make sure to save a reference to the ctxSolid pointer!
s = ctxSolid
s: Shape = ctxSolid
if toCombine:
s = s.fuse(*toCombine)
s = s.fuse(*_selectShapes(toCombine))

return self.newObject([s])

Expand Down Expand Up @@ -541,9 +547,9 @@ def _computeXdir(normal):
raise ValueError("Selected faces must be co-planar.")

if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = Shape.CombinedCenter(self.objects)
center = Shape.CombinedCenter(_selectShapes(self.objects))
elif centerOption == "CenterOfBoundBox":
center = Shape.CombinedCenterOfBoundBox(self.objects)
center = Shape.CombinedCenterOfBoundBox(_selectShapes(self.objects))

normal = objs[0].normalAt()
xDir = _computeXdir(normal)
Expand Down Expand Up @@ -1039,7 +1045,7 @@ def rotate(
"""
return self.newObject(
[
o.rotate(axisStartPoint, axisEndPoint, angleDegrees)
o.rotate(Vector(axisStartPoint), Vector(axisEndPoint), angleDegrees)
if isinstance(o, Shape)
else o
for o in self.objects
Expand Down Expand Up @@ -1067,7 +1073,10 @@ def translate(self, vec: VectorLike) -> "Workplane":
:returns: a CQ object
"""
return self.newObject(
[o.translate(vec) if isinstance(o, Shape) else o for o in self.objects]
[
o.translate(Vector(vec)) if isinstance(o, Shape) else o
for o in self.objects
]
)

def shell(self, thickness: float) -> "Workplane":
Expand Down Expand Up @@ -1108,11 +1117,11 @@ def shell(self, thickness: float) -> "Workplane":
"""
solidRef = self.findSolid()

for f in self.objects:
if type(f) != Face:
raise ValueError("Shelling requires that faces be selected")
faces = [f for f in self.objects if isinstance(f, Face)]
if len(faces) < len(self.objects):
raise ValueError("Shelling requires that faces are selected")

s = solidRef.shell(self.objects, thickness)
s = solidRef.shell(faces, thickness)
return self.newObject([s])

def fillet(self, radius: float) -> "Workplane":
Expand All @@ -1138,7 +1147,7 @@ def fillet(self, radius: float) -> "Workplane":

solid = self.findSolid()

edgeList = self.edges().vals()
edgeList = cast(List[Edge], self.edges().vals())
if len(edgeList) < 1:
raise ValueError("Fillets requires that edges be selected")

Expand Down Expand Up @@ -1175,7 +1184,7 @@ def chamfer(self, length: float, length2: Optional[float] = None) -> "Workplane"
"""
solid = self.findSolid()

edgeList = self.edges().vals()
edgeList = cast(List[Edge], self.edges().vals())
if len(edgeList) < 1:
raise ValueError("Chamfer requires that edges be selected")

Expand Down Expand Up @@ -1676,24 +1685,25 @@ def spline(
allPoints = vecs

if tangents:
t1, t2 = tangents
tangents = (
t1, t2 = Vector(tangents[0]), Vector(tangents[1])
tangents_g: Optional[Tuple[Vector, Vector]] = (
self.plane.toWorldCoords(t1) - self.plane.origin,
self.plane.toWorldCoords(t2) - self.plane.origin,
)
else:
tangents_g = None

e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic)
e = Edge.makeSpline(allPoints, tangents=tangents_g, periodic=periodic)

if makeWire:
rv = Wire.assembleEdges([e])
rv_w = Wire.assembleEdges([e])
if not forConstruction:
self._addPendingWire(rv)
self._addPendingWire(rv_w)
else:
rv = e
if not forConstruction:
self._addPendingEdge(e)

return self.newObject([rv])
return self.newObject([rv_w if makeWire else e])

def parametricCurve(
self,
Expand Down Expand Up @@ -1754,7 +1764,7 @@ def ellipseArc(
self.plane.xDir,
angle1,
angle2,
sense == 1,
sense,
)

# Rotate if necessary
Expand All @@ -1767,15 +1777,14 @@ def ellipseArc(
e = e.translate(center.sub(startPoint))

if makeWire:
rv = Wire.assembleEdges([e])
rv_w = Wire.assembleEdges([e])
if not forConstruction:
self._addPendingWire(rv)
self._addPendingWire(rv_w)
else:
rv = e
if not forConstruction:
self._addPendingEdge(e)

return self.newObject([rv])
return self.newObject([rv_w if makeWire else e])

def threePointArc(
self, point1: VectorLike, point2: VectorLike, forConstruction: bool = False
Expand Down Expand Up @@ -2048,7 +2057,7 @@ def consolidateWires(self) -> "Workplane":
Additionally, it has a bug where a profile composed of two wires ( rather than one )
also does not work properly. Together these are a real problem.
"""
wires = self.wires().vals()
wires = cast(List[Wire], self.wires().vals())
if len(wires) < 2:
return self

Expand Down Expand Up @@ -2471,7 +2480,7 @@ def cutEach(
raise ValueError("Must have a solid in the chain to cut from!")

# will contain all of the counterbores as a single compound
results = self.eachpoint(fcn, useLocalCoords).vals()
results = cast(List[Shape], self.eachpoint(fcn, useLocalCoords).vals())

s = ctxSolid.cut(*results)

Expand Down Expand Up @@ -2850,7 +2859,7 @@ def sweep(
newS = newS.clean()
return newS

def _combineWithBase(self, obj: CQObject) -> "Workplane":
def _combineWithBase(self, obj: Shape) -> "Workplane":
"""
Combines the provided object with the base solid, if one can be found.
:param obj:
Expand All @@ -2866,7 +2875,7 @@ def _combineWithBase(self, obj: CQObject) -> "Workplane":

return self.newObject([r])

def _cutFromBase(self, obj: CQObject) -> "Workplane":
def _cutFromBase(self, obj: Shape) -> "Workplane":
"""
Cuts the provided object from the base solid, if one can be found.
:param obj:
Expand Down Expand Up @@ -2921,9 +2930,9 @@ def union(
"""

# first collect all of the items together
newS: Sequence[CQObject]
newS: Sequence[Shape]
if isinstance(toUnion, CQ):
newS = toUnion.solids().vals()
newS = cast(List[Shape], toUnion.solids().vals())
if len(newS) < 1:
raise ValueError(
"CQ object must have at least one solid on the stack to union!"
Expand Down Expand Up @@ -2967,10 +2976,10 @@ def cut(
if solidRef is None:
raise ValueError("Cannot find solid to cut from")

solidToCut: Sequence[CQObject]
solidToCut: Sequence[Shape]

if isinstance(toCut, CQ):
solidToCut = toCut.vals()
solidToCut = _selectShapes(toCut.vals())
elif isinstance(toCut, (Solid, Compound)):
solidToCut = (toCut,)
else:
Expand Down Expand Up @@ -3002,10 +3011,10 @@ def intersect(
if solidRef is None:
raise ValueError("Cannot find solid to intersect with")

solidToIntersect: Sequence[CQObject]
solidToIntersect: Sequence[Shape]

if isinstance(toIntersect, CQ):
solidToIntersect = toIntersect.vals()
solidToIntersect = _selectShapes(toIntersect.vals())
elif isinstance(toIntersect, (Solid, Compound)):
solidToIntersect = (toIntersect,)
else:
Expand Down Expand Up @@ -3104,7 +3113,7 @@ def loft(
wiresToLoft = self.ctx.pendingWires
self.ctx.pendingWires = []

r = Solid.makeLoft(wiresToLoft, ruled)
r: Shape = Solid.makeLoft(wiresToLoft, ruled)

if combine:
parentSolid = self.findSolid(searchStack=False, searchParents=True)
Expand Down Expand Up @@ -3133,7 +3142,7 @@ def _extrude(
# group wires together into faces based on which ones are inside the others
# result is a list of lists

wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), [])
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires))
# now all of the wires have been used to create an extrusion
self.ctx.pendingWires = []

Expand Down Expand Up @@ -3190,7 +3199,9 @@ def _revolve(
# Revolve the wires, make a compound out of them and then fuse them
toFuse = []
for ws in wireSets:
thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
thisObj = Solid.revolve(
ws[0], ws[1:], angleDegrees, Vector(axisStart), Vector(axisEnd)
)
toFuse.append(thisObj)

return Compound.makeCompound(toFuse)
Expand All @@ -3214,16 +3225,19 @@ def _sweep(
"""

toFuse = []

p = path.val()
if not isinstance(p, (Wire, Edge)):
raise ValueError("Wire or Edge instance required")

if not multisection:
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires))
for ws in wireSets:
thisObj = Solid.sweep(
ws[0], ws[1:], path.val(), makeSolid, isFrenet, transition
)
thisObj = Solid.sweep(ws[0], ws[1:], p, makeSolid, isFrenet, transition)
toFuse.append(thisObj)
else:
sections = self.ctx.pendingWires
thisObj = Solid.sweep_multi(sections, path.val(), makeSolid, isFrenet)
thisObj = Solid.sweep_multi(sections, p, makeSolid, isFrenet)
toFuse.append(thisObj)

self.ctx.pendingWires = []
Expand Down Expand Up @@ -3563,8 +3577,8 @@ def text(
clean: bool = True,
font: str = "Arial",
kind: Literal["regular", "bold", "italic"] = "regular",
halign: Literal["center", "top", "bottom"] = "center",
valign: Literal["center", "left", "right"] = "center",
halign: Literal["center", "left", "right"] = "center",
valign: Literal["center", "top", "bottom"] = "center",
) -> "Workplane":
"""
Create a 3D text
Expand Down Expand Up @@ -3654,7 +3668,7 @@ def _repr_html_(self) -> Any:
if type(self.val()) is Vector:
return "&lt {} &gt".format(self.__repr__()[1:-1])
else:
return Compound.makeCompound(self.objects)._repr_html_()
return Compound.makeCompound(_selectShapes(self.objects))._repr_html_()


# alias for backward compatibility
Expand Down
2 changes: 1 addition & 1 deletion cadquery/occ_impl/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ def __ne__(self, other):
return not self.__eq__(other)

@property
def origin(self):
def origin(self) -> Vector:
return self._origin

@origin.setter
Expand Down
Loading

0 comments on commit 9808cce

Please sign in to comment.