Add Black formatting check to CI (#255)

* Add Black formatting check to CI
* Add some documentation for code contributors
* Use uncompromised code formatting
This commit is contained in:
Miguel Sánchez de León Peque
2020-01-20 20:52:12 +01:00
committed by Adam Urbańczyk
parent 74573fc3bb
commit 102c16c14e
43 changed files with 2968 additions and 1892 deletions

View File

@ -24,27 +24,31 @@ env:
matrix:
include:
- env: PYTHON_VERSION=3.6
- name: "Python 3.6 - osx"
env: PYTHON_VERSION=3.6
os: osx
- env: PYTHON_VERSION=3.6
- name: "Python 3.6 - linux"
env: PYTHON_VERSION=3.6
os: linux
- env: PYTHON_VERSION=3.7
- name: "Python 3.7 - osx"
env: PYTHON_VERSION=3.7
os: osx
- env: PYTHON_VERSION=3.7
- name: "Python 3.7 - linux"
env: PYTHON_VERSION=3.7
os: linux
- name: "Lint"
env: PYTHON_VERSION=3.7
os: linux
script:
- black . --diff --check
before_install:
- if [[ "$PYTHON_VERSION" == "2.7" ]]; then
PY_MAJOR=2 ;
else
PY_MAJOR=3 ;
fi ;
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
OS=Linux ;
else
OS=MacOSX ;
fi ;
wget https://repo.continuum.io/miniconda/Miniconda$PY_MAJOR-latest-$OS-x86_64.sh -O miniconda.sh
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$OS-x86_64.sh -O miniconda.sh
- bash miniconda.sh -b -p $HOME/miniconda;
- export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH";
- conda config --set always_yes yes --set changeps1 no;

View File

@ -123,6 +123,38 @@ You do not need to be a software developer to have a big impact on this project.
It is asked that all contributions to this project be made in a respectful and considerate way. Please use the [Python Community Code of Conduct's](https://www.python.org/psf/codeofconduct/) guidelines as a reference.
### Contributing code
If you are going to contribute code, make sure to follow this steps:
- Consider opening an issue first to discuss what you have in mind
- Try to keep it as short and simple as possible (if you want to change several
things, start with just one!)
- Fork the CadQuery repository, clone your fork and create a new branch to
start working on your changes
- Start with the tests! How should CadQuery behave after your changes? Make
sure to add some tests to the test suite to ensure proper behavior
- Make sure your tests have assertions checking all the expected results
- Add a nice docstring to the test indicating what the test is doing; if there
is too much to explain, consider splitting the test in two!
- Go ahead and implement the changes
- Add a nice docstring to the functions/methods/classes you implement
describing what they do, what the expected parameters are and what it returns
(if anything)
- Update the documentation if there is any change to the public API
- Consider adding an example to the documentation showing your cool new
feature!
- Make sure nothing is broken (run the complete test suite with `pytest`)
- Run `black` to autoformat your code and make sure your code style complies
with CadQuery's
- Push the changes to your fork and open a pull-request upstream
- Keep an eye on the automated feedback you will receive from the CI pipelines;
if there is a test failing or some code is not properly formatted, you will
be notified without human intervention
- Be prepared for constructive feedback and criticism!
- Be patient and respectful, remember that those reviewing your code are also
working hard (sometimes reviewing changes is harder than implementing them!)
### How to Report a Bug
When filing a bug report [issue](https://github.com/CadQuery/cadquery/issues), please be sure to answer these questions:

View File

@ -1,25 +1,65 @@
# these items point to the OCC implementation
from .occ_impl.geom import Plane, BoundBox, Vector, Matrix
from .occ_impl.shapes import (Shape, Vertex, Edge, Face, Wire, Solid, Shell,
Compound, sortWiresByBuildOrder)
from .occ_impl.shapes import (
Shape,
Vertex,
Edge,
Face,
Wire,
Solid,
Shell,
Compound,
sortWiresByBuildOrder,
)
from .occ_impl import exporters
from .occ_impl import importers
# these items are the common implementation
# the order of these matter
from .selectors import (NearestToPointSelector, ParallelDirSelector,
DirectionSelector, PerpendicularDirSelector, TypeSelector,
DirectionMinMaxSelector, StringSyntaxSelector, Selector)
from .selectors import (
NearestToPointSelector,
ParallelDirSelector,
DirectionSelector,
PerpendicularDirSelector,
TypeSelector,
DirectionMinMaxSelector,
StringSyntaxSelector,
Selector,
)
from .cq import CQ, Workplane, selectors
from . import plugins
__all__ = [
'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder',
'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers',
'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector',
'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', 'plugins'
"CQ",
"Workplane",
"plugins",
"selectors",
"Plane",
"BoundBox",
"Matrix",
"Vector",
"sortWiresByBuildOrder",
"Shape",
"Vertex",
"Edge",
"Wire",
"Face",
"Solid",
"Shell",
"Compound",
"exporters",
"importers",
"NearestToPointSelector",
"ParallelDirSelector",
"DirectionSelector",
"PerpendicularDirSelector",
"TypeSelector",
"DirectionMinMaxSelector",
"StringSyntaxSelector",
"Selector",
"plugins",
]
__version__ = "2.0.0dev"

View File

@ -18,8 +18,19 @@
"""
import math
from . import Vector, Plane, Shape, Edge, Wire, Face, Solid, Compound, \
sortWiresByBuildOrder, selectors, exporters
from . import (
Vector,
Plane,
Shape,
Edge,
Wire,
Face,
Solid,
Compound,
sortWiresByBuildOrder,
selectors,
exporters,
)
class CQContext(object):
@ -31,7 +42,9 @@ class CQContext(object):
"""
def __init__(self):
self.pendingWires = [] # a list of wires that have been created and need to be extruded
self.pendingWires = (
[]
) # a list of wires that have been created and need to be extruded
# a list of created pending edges that need to be joined into wires
self.pendingEdges = []
# a reference to the first point for a set of edges.
@ -99,8 +112,12 @@ class CQ(object):
# tricky-- if an object is a compound of solids,
# do not return all of the solids underneath-- typically
# then we'll keep joining to ourself
if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound':
for i in getattr(o, 'Compounds')():
if (
propName == "Solids"
and isinstance(o, Solid)
and o.ShapeType() == "Compound"
):
for i in getattr(o, "Compounds")():
all[i.hashCode()] = i
else:
if hasattr(o, propName):
@ -259,8 +276,9 @@ class CQ(object):
return self.objects[0].wrapped
def workplane(self, offset=0.0, invert=False, centerOption='CenterOfMass',
origin=None):
def workplane(
self, offset=0.0, invert=False, centerOption="CenterOfMass", origin=None
):
"""
Creates a new 2-D workplane, located relative to the first face on the stack.
@ -310,6 +328,7 @@ class CQ(object):
For now you can work around by creating a workplane and then offsetting the center
afterwards.
"""
def _isCoPlanar(f0, f1):
"""Test if two faces are on the same plane."""
p0 = f0.Center()
@ -318,9 +337,11 @@ class CQ(object):
n1 = f1.normalAt()
# test normals (direction of planes)
if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or
(abs(n0.y - n1.y) < self.ctx.tolerance) or
(abs(n0.z - n1.z) < self.ctx.tolerance)):
if not (
(abs(n0.x - n1.x) < self.ctx.tolerance)
or (abs(n0.y - n1.y) < self.ctx.tolerance)
or (abs(n0.z - n1.z) < self.ctx.tolerance)
):
return False
# test if p1 is on the plane of f0 (offset of planes)
@ -339,22 +360,23 @@ class CQ(object):
xd = Vector(1, 0, 0)
return xd
if centerOption not in {'CenterOfMass', 'ProjectedOrigin', 'CenterOfBoundBox'}:
raise ValueError('Undefined centerOption value provided.')
if centerOption not in {"CenterOfMass", "ProjectedOrigin", "CenterOfBoundBox"}:
raise ValueError("Undefined centerOption value provided.")
if len(self.objects) > 1:
# are all objects 'PLANE'?
if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects):
if not all(o.geomType() in ("PLANE", "CIRCLE") for o in self.objects):
raise ValueError(
"If multiple objects selected, they all must be planar faces.")
"If multiple objects selected, they all must be planar faces."
)
# are all faces co-planar with each other?
if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
raise ValueError("Selected faces must be co-planar.")
if centerOption in {'CenterOfMass', 'ProjectedOrigin'}:
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = Shape.CombinedCenter(self.objects)
elif centerOption == 'CenterOfBoundBox':
elif centerOption == "CenterOfBoundBox":
center = Shape.CombinedCenterOfBoundBox(self.objects)
normal = self.objects[0].normalAt()
@ -364,26 +386,27 @@ class CQ(object):
obj = self.objects[0]
if isinstance(obj, Face):
if centerOption in {'CenterOfMass', 'ProjectedOrigin'}:
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = obj.Center()
elif centerOption == 'CenterOfBoundBox':
elif centerOption == "CenterOfBoundBox":
center = obj.CenterOfBoundBox()
normal = obj.normalAt(center)
xDir = _computeXdir(normal)
else:
if hasattr(obj, 'Center'):
if centerOption in {'CenterOfMass', 'ProjectedOrigin'}:
if hasattr(obj, "Center"):
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = obj.Center()
elif centerOption == 'CenterOfBoundBox':
elif centerOption == "CenterOfBoundBox":
center = obj.CenterOfBoundBox()
normal = self.plane.zDir
xDir = self.plane.xDir
else:
raise ValueError(
"Needs a face or a vertex or point on a work plane")
"Needs a face or a vertex or point on a work plane"
)
# update center to projected origin if desired
if centerOption == 'ProjectedOrigin':
if centerOption == "ProjectedOrigin":
if origin is None:
origin = self.plane.origin
elif isinstance(origin, tuple):
@ -459,9 +482,7 @@ class CQ(object):
return rv[0]
if searchParents and self.parent is not None:
return self.parent._findType(types,
searchStack=True,
searchParents=True)
return self.parent._findType(types, searchStack=True, searchParents=True)
return None
@ -554,7 +575,7 @@ class CQ(object):
:py:class:`StringSyntaxSelector`
"""
return self._selectObjects('Vertices', selector)
return self._selectObjects("Vertices", selector)
def faces(self, selector=None):
"""
@ -586,7 +607,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Faces', selector)
return self._selectObjects("Faces", selector)
def edges(self, selector=None):
"""
@ -617,7 +638,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Edges', selector)
return self._selectObjects("Edges", selector)
def wires(self, selector=None):
"""
@ -640,7 +661,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Wires', selector)
return self._selectObjects("Wires", selector)
def solids(self, selector=None):
"""
@ -666,7 +687,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Solids', selector)
return self._selectObjects("Solids", selector)
def shells(self, selector=None):
"""
@ -686,7 +707,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Shells', selector)
return self._selectObjects("Shells", selector)
def compounds(self, selector=None):
"""
@ -704,7 +725,7 @@ class CQ(object):
See more about selectors HERE
"""
return self._selectObjects('Compounds', selector)
return self._selectObjects("Compounds", selector)
def toSvg(self, opts=None):
"""
@ -777,8 +798,9 @@ class CQ(object):
:type angleDegrees: float
:returns: a CQ object
"""
return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees)
for o in self.objects])
return self.newObject(
[o.rotate(axisStartPoint, axisEndPoint, angleDegrees) for o in self.objects]
)
def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
"""
@ -789,8 +811,7 @@ class CQ(object):
:param basePointVector: the base point to mirror about
:type basePointVector: tuple
"""
newS = self.newObject(
[self.objects[0].mirror(mirrorPlane, basePointVector)])
newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)])
return newS.first()
def translate(self, vec):
@ -943,7 +964,7 @@ class Workplane(CQ):
:py:meth:`CQ.workplane`
"""
FOR_CONSTRUCTION = 'ForConstruction'
FOR_CONSTRUCTION = "ForConstruction"
def __init__(self, inPlane, origin=(0, 0, 0), obj=None):
"""
@ -967,7 +988,7 @@ class Workplane(CQ):
the *current point* is on the origin.
"""
if inPlane.__class__.__name__ == 'Plane':
if inPlane.__class__.__name__ == "Plane":
tmpPlane = inPlane
elif isinstance(inPlane, str) or isinstance(inPlane, str):
tmpPlane = Plane.named(inPlane, origin)
@ -976,7 +997,8 @@ class Workplane(CQ):
if tmpPlane is None:
raise ValueError(
'Provided value {} is not a valid work plane'.format(inPlane))
"Provided value {} is not a valid work plane".format(inPlane)
)
self.obj = obj
self.plane = tmpPlane
@ -999,10 +1021,10 @@ class Workplane(CQ):
"""
# old api accepted a vector, so we'll check for that.
if rotate.__class__.__name__ == 'Vector':
if rotate.__class__.__name__ == "Vector":
rotate = rotate.toTuple()
if offset.__class__.__name__ == 'Vector':
if offset.__class__.__name__ == "Vector":
offset = offset.toTuple()
p = self.plane.rotated(rotate)
@ -1060,8 +1082,7 @@ class Workplane(CQ):
elif isinstance(obj, Vector):
p = obj
else:
raise RuntimeError(
"Cannot convert object type '%s' to vector " % type(obj))
raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
if useLocalCoords:
return self.plane.toLocalCoords(p)
@ -1387,8 +1408,15 @@ class Workplane(CQ):
return self.eachpoint(_makeslot, True)
def spline(self, listOfXYTuple, tangents=None, periodic=False,
forConstruction=False, includeCurrent=False, makeWire=False):
def spline(
self,
listOfXYTuple,
tangents=None,
periodic=False,
forConstruction=False,
includeCurrent=False,
makeWire=False,
):
"""
Create a spline interpolated through the provided points.
@ -1435,8 +1463,7 @@ class Workplane(CQ):
if tangents:
t1, t2 = tangents
tangents = (self.plane.toWorldCoords(t1),
self.plane.toWorldCoords(t2))
tangents = (self.plane.toWorldCoords(t1), self.plane.toWorldCoords(t2))
e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic)
@ -1516,10 +1543,16 @@ class Workplane(CQ):
midPoint = endPoint.add(startPoint).multiply(0.5)
sagVector = endPoint.sub(startPoint).normalized().multiply(abs(sag))
if(sag > 0):
sagVector.x, sagVector.y = -sagVector.y, sagVector.x # Rotate sagVector +90 deg
if sag > 0:
sagVector.x, sagVector.y = (
-sagVector.y,
sagVector.x,
) # Rotate sagVector +90 deg
else:
sagVector.x, sagVector.y = sagVector.y, -sagVector.x # Rotate sagVector -90 deg
sagVector.x, sagVector.y = (
sagVector.y,
-sagVector.x,
) # Rotate sagVector -90 deg
sagPoint = midPoint.add(sagVector)
@ -1580,8 +1613,7 @@ class Workplane(CQ):
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
rotatedWires = self.plane.rotateShapes(
consolidated.wires().vals(), matrix)
rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix)
for w in rotatedWires:
consolidated.objects.append(w)
@ -1616,8 +1648,7 @@ class Workplane(CQ):
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
'Y')
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "Y")
for w in mirroredWires:
consolidated.objects.append(w)
@ -1646,8 +1677,7 @@ class Workplane(CQ):
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
'X')
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "X")
for w in mirroredWires:
consolidated.objects.append(w)
@ -1859,6 +1889,7 @@ class Workplane(CQ):
better way to handle forConstruction
project points not in the workplane plane onto the workplane plane
"""
def makeRectangleWire(pnt):
# Here pnt is in local coordinates due to useLocalCoords=True
# (xc,yc,zc) = pnt.toTuple()
@ -1909,6 +1940,7 @@ class Workplane(CQ):
project points not in the workplane plane onto the workplane plane
"""
def makeCircleWire(obj):
cir = Wire.makeCircle(radius, obj, Vector(0, 0, 1))
cir.forConstruction = forConstruction
@ -1927,19 +1959,25 @@ class Workplane(CQ):
:param diameter: the size of the circle the polygon is inscribed into
:return: a polygon wire
"""
def _makePolygon(center):
# pnt is a vector in local coordinates
angle = 2.0 * math.pi / nSides
pnts = []
for i in range(nSides + 1):
pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)),
(diameter / 2.0 * math.sin(angle * i)), 0))
pnts.append(
center
+ Vector(
(diameter / 2.0 * math.cos(angle * i)),
(diameter / 2.0 * math.sin(angle * i)),
0,
)
)
return Wire.makePolygon(pnts, forConstruction)
return self.eachpoint(_makePolygon, True)
def polyline(self, listOfXYTuple, forConstruction=False,
includeCurrent=False):
def polyline(self, listOfXYTuple, forConstruction=False, includeCurrent=False):
"""
Create a polyline from a list of points
@ -2089,11 +2127,11 @@ class Workplane(CQ):
boreDir = Vector(0, 0, -1)
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordianates!
diameter / 2.0, depth, center, boreDir
) # local coordianates!
# add the counter bore
cbore = Solid.makeCylinder(
cboreDiameter / 2.0, cboreDepth, center, boreDir)
cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir)
r = hole.fuse(cbore)
return r
@ -2142,7 +2180,8 @@ class Workplane(CQ):
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coords!
diameter / 2.0, depth, center, boreDir
) # local coords!
r = cskDiameter / 2.0
h = r / math.tan(math.radians(cskAngle / 2.0))
csk = Solid.makeCone(r, 0.0, h, center, boreDir)
@ -2191,7 +2230,8 @@ class Workplane(CQ):
boreDir = Vector(0, 0, -1)
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordinates!
diameter / 2.0, depth, center, boreDir
) # local coordinates!
return hole
return self.cutEach(_makeHole, True, clean)
@ -2235,8 +2275,9 @@ class Workplane(CQ):
# are multiple sets
r = None
for ws in wireSets:
thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin,
eDir, angleDegrees)
thisObj = Solid.extrudeLinearWithRotation(
ws[0], ws[1:], self.plane.origin, eDir, angleDegrees
)
if r is None:
r = thisObj
else:
@ -2277,7 +2318,8 @@ class Workplane(CQ):
selected may not be planar
"""
r = self._extrude(
distance, both=both, taper=taper) # returns a Solid (or a compound if there were multiple)
distance, both=both, taper=taper
) # returns a Solid (or a compound if there were multiple)
if combine:
newS = self._combineWithBase(r)
@ -2287,7 +2329,9 @@ class Workplane(CQ):
newS = newS.clean()
return newS
def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True):
def revolve(
self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True
):
"""
Use all un-revolved wires in the parent chain to create a solid.
@ -2344,8 +2388,17 @@ class Workplane(CQ):
newS = newS.clean()
return newS
def sweep(self, path, multisection=False, sweepAlongWires=None, makeSolid=True, isFrenet=False,
combine=True, clean=True, transition='right'):
def sweep(
self,
path,
multisection=False,
sweepAlongWires=None,
makeSolid=True,
isFrenet=False,
combine=True,
clean=True,
transition="right",
):
"""
Use all un-extruded wires in the parent chain to create a swept solid.
@ -2365,17 +2418,22 @@ class Workplane(CQ):
multisection = sweepAlongWires
from warnings import warn
warn('sweepAlongWires keyword argument is is depracated and will '\
'be removed in the next version; use multisection instead',
DeprecationWarning)
r = self._sweep(path.wire(), multisection, makeSolid, isFrenet,
transition) # returns a Solid (or a compound if there were multiple)
warn(
"sweepAlongWires keyword argument is is depracated and will "
"be removed in the next version; use multisection instead",
DeprecationWarning,
)
r = self._sweep(
path.wire(), multisection, makeSolid, isFrenet, transition
) # returns a Solid (or a compound if there were multiple)
if combine:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return newS
def _combineWithBase(self, obj):
@ -2442,7 +2500,8 @@ class Workplane(CQ):
solids = toUnion.solids().vals()
if len(solids) < 1:
raise ValueError(
"CQ object must have at least one solid on the stack to union!")
"CQ object must have at least one solid on the stack to union!"
)
newS = solids.pop(0)
for s in solids:
newS = newS.fuse(s)
@ -2522,7 +2581,8 @@ class Workplane(CQ):
newS = solidRef.intersect(solidToIntersect)
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return self.newObject([newS])
@ -2591,8 +2651,7 @@ class Workplane(CQ):
rv = []
for solid in solidRef.Solids():
s = solid.dprism(faceRef, wires, thruAll=True, additive=False,
taper=-taper)
s = solid.dprism(faceRef, wires, thruAll=True, additive=False, taper=-taper)
if clean:
s = s.clean()
@ -2636,8 +2695,7 @@ class Workplane(CQ):
# 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 = []
@ -2664,8 +2722,7 @@ class Workplane(CQ):
toFuse.append(thisObj)
if both:
thisObj = Solid.extrudeLinear(
ws[0], ws[1:], eDir.multiply(-1.))
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.0))
toFuse.append(thisObj)
return Compound.makeCompound(toFuse)
@ -2685,8 +2742,7 @@ class Workplane(CQ):
This method is a utility method, primarily for plugin and internal use.
"""
# We have to gather the wires to be revolved
wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires))
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires))
# Mark that all of the wires have been used to create a revolution
self.ctx.pendingWires = []
@ -2694,14 +2750,19 @@ class Workplane(CQ):
# 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, axisStart, axisEnd)
toFuse.append(thisObj)
return Compound.makeCompound(toFuse)
def _sweep(self, path, multisection=False, makeSolid=True, isFrenet=False,
transition='right'):
def _sweep(
self,
path,
multisection=False,
makeSolid=True,
isFrenet=False,
transition="right",
):
"""
Makes a swept solid from an existing set of pending wires.
@ -2716,8 +2777,9 @@ class Workplane(CQ):
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:], path.val(), makeSolid, isFrenet, transition
)
toFuse.append(thisObj)
else:
sections = self.ctx.pendingWires
@ -2728,7 +2790,15 @@ class Workplane(CQ):
return Compound.makeCompound(toFuse)
def box(self, length, width, height, centered=(True, True, True), combine=True, clean=True):
def box(
self,
length,
width,
height,
centered=(True, True, True),
combine=True,
clean=True,
):
"""
Return a 3d box with specified dimensions for each object on the stack.
@ -2776,11 +2846,11 @@ class Workplane(CQ):
# (xp,yp,zp) = self.plane.toLocalCoords(pnt)
(xp, yp, zp) = pnt.toTuple()
if centered[0]:
xp -= (length / 2.0)
xp -= length / 2.0
if centered[1]:
yp -= (width / 2.0)
yp -= width / 2.0
if centered[2]:
zp -= (height / 2.0)
zp -= height / 2.0
return Solid.makeBox(length, width, height, Vector(xp, yp, zp))
@ -2793,8 +2863,17 @@ class Workplane(CQ):
# combine everything
return self.union(boxes, clean=clean)
def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360,
centered=(True, True, True), combine=True, clean=True):
def sphere(
self,
radius,
direct=(0, 0, 1),
angle1=-90,
angle2=90,
angle3=360,
centered=(True, True, True),
combine=True,
clean=True,
):
"""
Returns a 3D sphere with the specified radius for each point on the stack
@ -2851,7 +2930,9 @@ class Workplane(CQ):
if not centered[2]:
zp += radius
return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3)
return Solid.makeSphere(
radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3
)
# We want a sphere for each point on the workplane
spheres = self.eachpoint(_makesphere, True)
@ -2862,8 +2943,21 @@ class Workplane(CQ):
else:
return self.union(spheres, clean=clean)
def wedge(self, dx, dy, dz, xmin, zmin, xmax, zmax, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1),
centered=(True, True, True), combine=True, clean=True):
def wedge(
self,
dx,
dy,
dz,
xmin,
zmin,
xmax,
zmax,
pnt=Vector(0, 0, 0),
dir=Vector(0, 0, 1),
centered=(True, True, True),
combine=True,
clean=True,
):
"""
:param dx: Distance along the X axis
:param dy: Distance along the Y axis
@ -2901,15 +2995,17 @@ class Workplane(CQ):
(xp, yp, zp) = pnt.toTuple()
if not centered[0]:
xp += dx / 2.
xp += dx / 2.0
if not centered[1]:
yp += dy / 2.
yp += dy / 2.0
if not centered[2]:
zp += dx / 2.
zp += dx / 2.0
return Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir)
return Solid.makeWedge(
dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir
)
# We want a wedge for each point on the workplane
wedges = self.eachpoint(_makewedge)
@ -2945,11 +3041,23 @@ class Workplane(CQ):
cleanObjects = [obj.clean() for obj in self.objects]
except AttributeError:
raise AttributeError(
"%s object doesn't support `clean()` method!" % obj.ShapeType())
"%s object doesn't support `clean()` method!" % obj.ShapeType()
)
return self.newObject(cleanObjects)
def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True,
font="Arial", kind='regular',halign='center',valign='center'):
def text(
self,
txt,
fontsize,
distance,
cut=True,
combine=False,
clean=True,
font="Arial",
kind="regular",
halign="center",
valign="center",
):
"""
Create a 3D text
@ -2976,8 +3084,16 @@ class Workplane(CQ):
and the resulting solid becomes the new context solid.
"""
r = Compound.makeText(txt,fontsize,distance,font=font,kind=kind,
halign=halign, valign=valign, position=self.plane)
r = Compound.makeText(
txt,
fontsize,
distance,
font=font,
kind=kind,
halign=halign,
valign=valign,
position=self.plane,
)
if cut:
newS = self._cutFromBase(r)
@ -2995,7 +3111,6 @@ class Workplane(CQ):
"""
if type(self.objects[0]) is Vector:
return '&lt {} &gt'.format(self.__repr__()[1:-1])
return "&lt {} &gt".format(self.__repr__()[1:-1])
else:
return Compound.makeCompound(self.objects)._repr_html_()

View File

@ -20,13 +20,22 @@ template = """
</div>
"""
template_content_indent = ' '
template_content_indent = " "
def cq_directive(name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
def cq_directive(
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
):
# only consider inline snippets
plot_code = '\n'.join(content)
plot_code = "\n".join(content)
# Since we don't have a filename, use a hash based on the content
# the script must define a variable called 'out', which is expected to
@ -52,22 +61,20 @@ def cq_directive(name, arguments, options, content, lineno,
lines = []
# get rid of new lines
out_svg = out_svg.replace('\n', '')
out_svg = out_svg.replace("\n", "")
txt_align = "left"
if "align" in options:
txt_align = options['align']
txt_align = options["align"]
lines.extend((template % locals()).split('\n'))
lines.extend((template % locals()).split("\n"))
lines.extend(['::', ''])
lines.extend([' %s' % row.rstrip()
for row in plot_code.split('\n')])
lines.append('')
lines.extend(["::", ""])
lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")])
lines.append("")
if len(lines):
state_machine.insert_input(
lines, state_machine.input_lines.source(0))
state_machine.insert_input(lines, state_machine.input_lines.source(0))
return []
@ -77,9 +84,10 @@ def setup(app):
setup.config = app.config
setup.confdir = app.confdir
options = {'height': directives.length_or_unitless,
'width': directives.length_or_percentage_or_unitless,
'align': directives.unchanged
options = {
"height": directives.length_or_unitless,
"width": directives.length_or_percentage_or_unitless,
"align": directives.unchanged,
}
app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)
app.add_directive("cq_plot", cq_directive, True, (0, 2, 0), **options)

View File

@ -9,6 +9,7 @@ import cadquery
CQSCRIPT = "<cqscript>"
def parse(script_source):
"""
Parses the script as a model, and returns a model.
@ -34,6 +35,7 @@ class CQModel(object):
the build method can be used to generate a 3d model
"""
def __init__(self, script_source):
"""
Create an object by parsing the supplied python script.
@ -100,14 +102,18 @@ class CQModel(object):
try:
self.set_param_values(build_parameters)
collector = ScriptCallback()
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
.add_entry("__name__", "__cqgi__") \
.add_entry("show_object", collector.show_object) \
.add_entry("debug", collector.debug) \
.add_entry("describe_parameter",collector.describe_parameter) \
env = (
EnvironmentBuilder()
.with_real_builtins()
.with_cadquery_objects()
.add_entry("__name__", "__cqgi__")
.add_entry("show_object", collector.show_object)
.add_entry("debug", collector.debug)
.add_entry("describe_parameter", collector.describe_parameter)
.build()
)
c = compile(self.ast_tree, CQSCRIPT, 'exec')
c = compile(self.ast_tree, CQSCRIPT, "exec")
exec(c, env)
result.set_debug(collector.debugObjects)
result.set_success_result(collector.outputObjects)
@ -124,7 +130,9 @@ class CQModel(object):
for k, v in params.items():
if k not in model_parameters:
raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
raise InvalidParameterError(
"Cannot set value '%s': not a parameter of the model." % k
)
p = model_parameters[k]
p.set_value(v)
@ -134,10 +142,12 @@ class ShapeResult(object):
"""
An object created by a build, including the user parameters provided
"""
def __init__(self):
self.shape = None
self.options = None
class BuildResult(object):
"""
The result of executing a CadQuery script.
@ -149,6 +159,7 @@ class BuildResult(object):
If unsuccessful, the exception property contains a reference to
the stack trace that occurred.
"""
def __init__(self):
self.buildTime = None
self.results = [] # list of ShapeResult
@ -176,6 +187,7 @@ class ScriptMetadata(object):
Defines the metadata for a parsed CQ Script.
the parameters property is a dict of InputParameter objects.
"""
def __init__(self):
self.parameters = {}
@ -214,6 +226,7 @@ class InputParameter:
provide additional metadata
"""
def __init__(self):
#: the default value for the variable.
@ -234,7 +247,9 @@ class InputParameter:
self.ast_node = None
@staticmethod
def create(ast_node, var_name, var_type, default_value, valid_values=None, desc=None):
def create(
ast_node, var_name, var_type, default_value, valid_values=None, desc=None
):
if valid_values is None:
valid_values = []
@ -251,8 +266,10 @@ class InputParameter:
def set_value(self, new_value):
if len(self.valid_values) > 0 and new_value not in self.valid_values:
raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
.format(str(new_value), self.name, str(self.valid_values)))
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} ".format(
str(new_value), self.name, str(self.valid_values)
)
)
if self.varType == NumberParameterType:
try:
@ -265,28 +282,33 @@ class InputParameter:
self.ast_node.n = f
except ValueError:
raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
.format(str(new_value), self.name))
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric.".format(
str(new_value), self.name
)
)
elif self.varType == StringParameterType:
self.ast_node.s = str(new_value)
elif self.varType == BooleanParameterType:
if new_value:
if hasattr(ast, 'NameConstant'):
if hasattr(ast, "NameConstant"):
self.ast_node.value = True
else:
self.ast_node.id = 'True'
self.ast_node.id = "True"
else:
if hasattr(ast, 'NameConstant'):
if hasattr(ast, "NameConstant"):
self.ast_node.value = False
else:
self.ast_node.id = 'False'
self.ast_node.id = "False"
else:
raise ValueError("Unknown Type of var: ", str(self.varType))
def __str__(self):
return "InputParameter: {name=%s, type=%s, defaultValue=%s" % (
self.name, str(self.varType), str(self.default_value))
self.name,
str(self.varType),
str(self.default_value),
)
class ScriptCallback(object):
@ -295,6 +317,7 @@ class ScriptCallback(object):
the show_object() method is exposed to CQ scripts, to allow them
to return objects to the execution environment
"""
def __init__(self):
self.outputObjects = []
self.debugObjects = []
@ -335,12 +358,12 @@ class ScriptCallback(object):
return len(self.outputObjects) > 0
class InvalidParameterError(Exception):
"""
Raised when an attempt is made to provide a new parameter value
that cannot be assigned to the model
"""
pass
@ -349,6 +372,7 @@ class NoOutputError(Exception):
Raised when the script does not execute the show_object() method to
return a solid
"""
pass
@ -386,6 +410,7 @@ class EnvironmentBuilder(object):
The environment includes the builtins, as well as
the other methods the script will need.
"""
def __init__(self):
self.env = {}
@ -393,12 +418,12 @@ class EnvironmentBuilder(object):
return self.with_builtins(__builtins__)
def with_builtins(self, env_dict):
self.env['__builtins__'] = env_dict
self.env["__builtins__"] = env_dict
return self
def with_cadquery_objects(self):
self.env['cadquery'] = cadquery
self.env['cq'] = cadquery
self.env["cadquery"] = cadquery
self.env["cq"] = cadquery
return self
def add_entry(self, name, value):
@ -408,10 +433,12 @@ class EnvironmentBuilder(object):
def build(self):
return self.env
class ParameterDescriptionFinder(ast.NodeTransformer):
"""
Visits a parse tree, looking for function calls to describe_parameter(var, description )
"""
def __init__(self, cq_model):
self.cqModel = cq_model
@ -420,7 +447,7 @@ class ParameterDescriptionFinder(ast.NodeTransformer):
Called when we see a function call. Is it describe_parameter?
"""
try:
if node.func.id == 'describe_parameter':
if node.func.id == "describe_parameter":
# looks like we have a call to our function.
# first parameter is the variable,
# second is the description
@ -433,6 +460,7 @@ class ParameterDescriptionFinder(ast.NodeTransformer):
pass
return node
class ConstantAssignmentFinder(ast.NodeTransformer):
"""
Visits a parse tree, and adds script parameters to the cqModel
@ -446,24 +474,42 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
if type(value_node) == ast.Num:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
InputParameter.create(
value_node, var_name, NumberParameterType, value_node.n
)
)
elif type(value_node) == ast.Str:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
InputParameter.create(
value_node, var_name, StringParameterType, value_node.s
)
)
elif type(value_node) == ast.Name:
if value_node.id == 'True':
if value_node.id == "True":
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
elif value_node.id == 'False':
InputParameter.create(
value_node, var_name, BooleanParameterType, True
)
)
elif value_node.id == "False":
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, False))
elif hasattr(ast, 'NameConstant') and type(value_node) == ast.NameConstant:
InputParameter.create(
value_node, var_name, BooleanParameterType, False
)
)
elif hasattr(ast, "NameConstant") and type(value_node) == ast.NameConstant:
if value_node.value == True:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
InputParameter.create(
value_node, var_name, BooleanParameterType, True
)
)
else:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, False))
InputParameter.create(
value_node, var_name, BooleanParameterType, False
)
)
except:
print("Unable to handle assignment for variable '%s'" % var_name)
pass
@ -479,7 +525,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
# Handle the NamedConstant type that is only present in Python 3
astTypes = [ast.Num, ast.Str, ast.Name]
if hasattr(ast, 'NameConstant'):
if hasattr(ast, "NameConstant"):
astTypes.append(ast.NameConstant)
if type(node.value) in astTypes:

View File

@ -4,6 +4,7 @@ from OCC.Core.Visualization import Tesselator
import tempfile
import os
import sys
if sys.version_info.major == 2:
import cStringIO as StringIO
else:
@ -116,7 +117,7 @@ def readAndDeleteFile(fileName):
return the contents as a string
"""
res = ""
with open(fileName, 'r') as f:
with open(fileName, "r") as f:
res = "{}".format(f.read())
os.remove(fileName)
@ -152,34 +153,34 @@ class AmfWriter(object):
self.tessellation = tessellation
def writeAmf(self, outFile):
amf = ET.Element('amf', units=self.units)
amf = ET.Element("amf", units=self.units)
# TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf, 'object', id="0")
mesh = ET.SubElement(object, 'mesh')
vertices = ET.SubElement(mesh, 'vertices')
volume = ET.SubElement(mesh, 'volume')
object = ET.SubElement(amf, "object", id="0")
mesh = ET.SubElement(object, "mesh")
vertices = ET.SubElement(mesh, "vertices")
volume = ET.SubElement(mesh, "volume")
# add vertices
for i_vert in range(self.tessellation.ObjGetVertexCount()):
v = self.tessellation.GetVertex(i_vert)
vtx = ET.SubElement(vertices, 'vertex')
coord = ET.SubElement(vtx, 'coordinates')
x = ET.SubElement(coord, 'x')
vtx = ET.SubElement(vertices, "vertex")
coord = ET.SubElement(vtx, "coordinates")
x = ET.SubElement(coord, "x")
x.text = str(v[0])
y = ET.SubElement(coord, 'y')
y = ET.SubElement(coord, "y")
y.text = str(v[1])
z = ET.SubElement(coord, 'z')
z = ET.SubElement(coord, "z")
z.text = str(v[2])
# add triangles
for i_tr in range(self.tessellation.ObjGetTriangleCount()):
t = self.tessellation.GetTriangleIndex(i_tr)
triangle = ET.SubElement(volume, 'triangle')
v1 = ET.SubElement(triangle, 'v1')
triangle = ET.SubElement(volume, "triangle")
v1 = ET.SubElement(triangle, "v1")
v1.text = str(t[0])
v2 = ET.SubElement(triangle, 'v2')
v2 = ET.SubElement(triangle, "v2")
v2.text = str(t[1])
v3 = ET.SubElement(triangle, 'v3')
v3 = ET.SubElement(triangle, "v3")
v3.text = str(t[2])
amf = ET.ElementTree(amf).write(outFile, xml_declaration=True)
@ -217,11 +218,11 @@ class JsonMesh(object):
def toJson(self):
return JSON_TEMPLATE % {
'vertices': str(self.vertices),
'faces': str(self.faces),
'nVertices': self.nVertices,
'nFaces': self.nFaces
};
"vertices": str(self.vertices),
"faces": str(self.faces),
"nVertices": self.nVertices,
"nFaces": self.nFaces,
}
def makeSVGedge(e):
@ -235,20 +236,16 @@ def makeSVGedge(e):
start = curve.FirstParameter()
end = curve.LastParameter()
points = GCPnts_QuasiUniformDeflection(curve,
DISCRETIZATION_TOLERANCE,
start,
end)
points = GCPnts_QuasiUniformDeflection(curve, DISCRETIZATION_TOLERANCE, start, end)
if points.IsDone():
point_it = (points.Value(i + 1) for i in
range(points.NbPoints()))
point_it = (points.Value(i + 1) for i in range(points.NbPoints()))
p = next(point_it)
cs.write('M{},{} '.format(p.X(), p.Y()))
cs.write("M{},{} ".format(p.X(), p.Y()))
for p in point_it:
cs.write('L{},{} '.format(p.X(), p.Y()))
cs.write("L{},{} ".format(p.X(), p.Y()))
return cs.getvalue()
@ -277,7 +274,7 @@ def getSVG(shape, opts=None):
Export a shape to SVG
"""
d = {'width': 800, 'height': 240, 'marginLeft': 200, 'marginTop': 20}
d = {"width": 800, "height": 240, "marginLeft": 200, "marginTop": 20}
if opts:
d.update(opts)
@ -285,17 +282,15 @@ def getSVG(shape, opts=None):
# need to guess the scale and the coordinate center
uom = guessUnitOfMeasure(shape)
width = float(d['width'])
height = float(d['height'])
marginLeft = float(d['marginLeft'])
marginTop = float(d['marginTop'])
width = float(d["width"])
height = float(d["height"])
marginLeft = float(d["marginLeft"])
marginTop = float(d["marginTop"])
hlr = HLRBRep_Algo()
hlr.Add(shape.wrapped)
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(),
DEFAULT_DIR)
)
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR))
hlr.Projector(projector)
hlr.Update()
@ -336,8 +331,7 @@ def getSVG(shape, opts=None):
# convert to native CQ objects
visible = list(map(Shape, visible))
hidden = list(map(Shape, hidden))
(hiddenPaths, visiblePaths) = getPaths(visible,
hidden)
(hiddenPaths, visiblePaths) = getPaths(visible, hidden)
# get bounding box -- these are all in 2-d space
bb = Compound.makeCompound(hidden + visible).BoundingBox()
@ -346,8 +340,10 @@ def getSVG(shape, opts=None):
unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
# compute amount to translate-- move the top left into view
(xTranslate, yTranslate) = ((0 - bb.xmin) + marginLeft /
unitScale, (0 - bb.ymax) - marginTop / unitScale)
(xTranslate, yTranslate) = (
(0 - bb.xmin) + marginLeft / unitScale,
(0 - bb.ymax) - marginTop / unitScale,
)
# compute paths ( again -- had to strip out freecad crap )
hiddenContent = ""
@ -369,7 +365,7 @@ def getSVG(shape, opts=None):
"width": str(width),
"height": str(height),
"textboxY": str(height - 30),
"uom": str(uom)
"uom": str(uom),
}
)
# svg = SVG_TEMPLATE % (
@ -386,7 +382,7 @@ def exportSVG(shape, fileName):
"""
svg = getSVG(shape.val())
f = open(fileName, 'w')
f = open(fileName, "w")
f.write(svg)
f.close()
@ -471,4 +467,4 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
</svg>
"""
PATHTEMPLATE = "\t\t\t<path d=\"%s\" />\n"
PATHTEMPLATE = '\t\t\t<path d="%s" />\n'

View File

@ -1,6 +1,16 @@
import math
from OCC.Core.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp_GTrsf, gp, gp_XYZ
from OCC.Core.gp import (
gp_Vec,
gp_Ax1,
gp_Ax3,
gp_Pnt,
gp_Dir,
gp_Trsf,
gp_GTrsf,
gp,
gp_XYZ,
)
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
@ -132,16 +142,13 @@ class Vector(object):
return self.wrapped.Angle(v.wrapped)
def distanceToLine(self):
raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
def projectToLine(self):
raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
def distanceToPlane(self):
raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
def projectToPlane(self, plane):
"""
@ -163,18 +170,19 @@ class Vector(object):
return self.Length
def __repr__(self):
return 'Vector: ' + str((self.x, self.y, self.z))
return "Vector: " + str((self.x, self.y, self.z))
def __str__(self):
return 'Vector: ' + str((self.x, self.y, self.z))
return "Vector: " + str((self.x, self.y, self.z))
def __eq__(self, other):
return self.wrapped.IsEqual(other.wrapped, 0.00001, 0.00001)
'''
"""
is not implemented in OCC
def __ne__(self, other):
return self.wrapped.__ne__(other)
'''
"""
def toPnt(self):
@ -222,44 +230,48 @@ class Matrix:
elif isinstance(matrix, (list, tuple)):
# Validate matrix size & 4x4 last row value
valid_sizes = all(
(isinstance(row, (list, tuple)) and (len(row) == 4))
for row in matrix
(isinstance(row, (list, tuple)) and (len(row) == 4)) for row in matrix
) and len(matrix) in (3, 4)
if not valid_sizes:
raise TypeError("Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(matrix))
raise TypeError(
"Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(
matrix
)
)
elif (len(matrix) == 4) and (tuple(matrix[3]) != (0, 0, 0, 1)):
raise ValueError("Expected the last row to be [0,0,0,1], but got: {!r}".format(matrix[3]))
raise ValueError(
"Expected the last row to be [0,0,0,1], but got: {!r}".format(
matrix[3]
)
)
# Assign values to matrix
self.wrapped = gp_GTrsf()
[self.wrapped.SetValue(i+1,j+1,e)
[
self.wrapped.SetValue(i + 1, j + 1, e)
for i, row in enumerate(matrix[:3])
for j,e in enumerate(row)]
for j, e in enumerate(row)
]
else:
raise TypeError(
"Invalid param to matrix constructor: {}".format(matrix))
raise TypeError("Invalid param to matrix constructor: {}".format(matrix))
def rotateX(self, angle):
self._rotate(gp.OX(),
angle)
self._rotate(gp.OX(), angle)
def rotateY(self, angle):
self._rotate(gp.OY(),
angle)
self._rotate(gp.OY(), angle)
def rotateZ(self, angle):
self._rotate(gp.OZ(),
angle)
self._rotate(gp.OZ(), angle)
def _rotate(self, direction, angle):
new = gp_Trsf()
new.SetRotation(direction,
angle)
new.SetRotation(direction, angle)
self.wrapped = self.wrapped * gp_GTrsf(new)
@ -279,8 +291,9 @@ class Matrix:
"""
trsf = self.wrapped
data = [[trsf.Value(i,j) for j in range(1,5)] for i in range(1,4)] + \
[[0.,0.,0.,1.]]
data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [
[0.0, 0.0, 0.0, 1.0]
]
return [data[j][i] for i in range(4) for j in range(4)]
@ -298,7 +311,7 @@ class Matrix:
else:
# gp_GTrsf doesn't provide access to the 4th row because it has
# an implied value as below:
return [0., 0., 0., 1.][c]
return [0.0, 0.0, 0.0, 1.0][c]
else:
raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc))
@ -352,95 +365,94 @@ class Plane(object):
namedPlanes = {
# origin, xDir, normal
'XY': Plane(origin, (1, 0, 0), (0, 0, 1)),
'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)),
'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)),
'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)),
'YX': Plane(origin, (0, 1, 0), (0, 0, -1)),
'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)),
'front': Plane(origin, (1, 0, 0), (0, 0, 1)),
'back': Plane(origin, (-1, 0, 0), (0, 0, -1)),
'left': Plane(origin, (0, 0, 1), (-1, 0, 0)),
'right': Plane(origin, (0, 0, -1), (1, 0, 0)),
'top': Plane(origin, (1, 0, 0), (0, 1, 0)),
'bottom': Plane(origin, (1, 0, 0), (0, -1, 0))
"XY": Plane(origin, (1, 0, 0), (0, 0, 1)),
"YZ": Plane(origin, (0, 1, 0), (1, 0, 0)),
"ZX": Plane(origin, (0, 0, 1), (0, 1, 0)),
"XZ": Plane(origin, (1, 0, 0), (0, -1, 0)),
"YX": Plane(origin, (0, 1, 0), (0, 0, -1)),
"ZY": Plane(origin, (0, 0, 1), (-1, 0, 0)),
"front": Plane(origin, (1, 0, 0), (0, 0, 1)),
"back": Plane(origin, (-1, 0, 0), (0, 0, -1)),
"left": Plane(origin, (0, 0, 1), (-1, 0, 0)),
"right": Plane(origin, (0, 0, -1), (1, 0, 0)),
"top": Plane(origin, (1, 0, 0), (0, 1, 0)),
"bottom": Plane(origin, (1, 0, 0), (0, -1, 0)),
}
try:
return namedPlanes[stdName]
except KeyError:
raise ValueError('Supported names are {}'.format(
list(namedPlanes.keys())))
raise ValueError("Supported names are {}".format(list(namedPlanes.keys())))
@classmethod
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
plane = Plane.named('XY', origin)
plane = Plane.named("XY", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
plane = Plane.named('YZ', origin)
plane = Plane.named("YZ", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
plane = Plane.named('ZX', origin)
plane = Plane.named("ZX", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
plane = Plane.named('XZ', origin)
plane = Plane.named("XZ", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
plane = Plane.named('YX', origin)
plane = Plane.named("YX", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
plane = Plane.named('ZY', origin)
plane = Plane.named("ZY", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
plane = Plane.named('front', origin)
plane = Plane.named("front", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)):
plane = Plane.named('back', origin)
plane = Plane.named("back", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
plane = Plane.named('left', origin)
plane = Plane.named("left", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)):
plane = Plane.named('right', origin)
plane = Plane.named("right", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
plane = Plane.named('top', origin)
plane = Plane.named("top", origin)
plane._setPlaneDir(xDir)
return plane
@classmethod
def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
plane = Plane.named('bottom', origin)
plane = Plane.named("bottom", origin)
plane._setPlaneDir(xDir)
return plane
@ -458,12 +470,12 @@ class Plane(object):
:return: a plane in the global space, with the xDirection of the plane in the specified direction.
"""
zDir = Vector(normal)
if (zDir.Length == 0.0):
raise ValueError('normal should be non null')
if zDir.Length == 0.0:
raise ValueError("normal should be non null")
xDir = Vector(xDir)
if (xDir.Length == 0.0):
raise ValueError('xDir should be non null')
if xDir.Length == 0.0:
raise ValueError("xDir should be non null")
self.zDir = zDir.normalized()
self._setPlaneDir(xDir)
@ -489,6 +501,7 @@ class Plane(object):
@property
def origin(self):
return self._origin
# TODO is this property rly needed -- why not handle this in the constructor
@origin.setter
@ -545,7 +558,7 @@ class Plane(object):
pass
'''
"""
# TODO: also use a set of points along the wire to test as well.
# TODO: would it be more efficient to create objects in the local
# coordinate system, and then transform to global
@ -562,7 +575,7 @@ class Plane(object):
# findOutsideBox actually inspects both ways, here we only want to
# know if one is inside the other
return bb == BoundBox.findOutsideBox2D(bb, tb)
'''
"""
def toLocalCoords(self, obj):
"""Project the provided coordinates onto this plane
@ -588,7 +601,9 @@ class Plane(object):
else:
raise ValueError(
"Don't know how to convert type {} to local coordinates".format(
type(obj)))
type(obj)
)
)
def toWorldCoords(self, tuplePoint):
"""Convert a point in local coordinates to global coordinates
@ -655,7 +670,7 @@ class Plane(object):
raise NotImplementedError
'''
"""
resultWires = []
for w in listOfShapes:
mirrored = w.transformGeometry(rotationMatrix.wrapped)
@ -681,21 +696,19 @@ class Plane(object):
resultWires.append(cadquery.Shape.cast(mirroredWire))
return resultWires'''
return resultWires"""
def mirrorInPlane(self, listOfShapes, axis='X'):
def mirrorInPlane(self, listOfShapes, axis="X"):
local_coord_system = gp_Ax3(self.origin.toPnt(),
self.zDir.toDir(),
self.xDir.toDir())
local_coord_system = gp_Ax3(
self.origin.toPnt(), self.zDir.toDir(), self.xDir.toDir()
)
T = gp_Trsf()
if axis == 'X':
T.SetMirror(gp_Ax1(self.origin.toPnt(),
local_coord_system.XDirection()))
elif axis == 'Y':
T.SetMirror(gp_Ax1(self.origin.toPnt(),
local_coord_system.YDirection()))
if axis == "X":
T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.XDirection()))
elif axis == "Y":
T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.YDirection()))
else:
raise NotImplementedError
@ -731,17 +744,16 @@ class Plane(object):
inverseT = gp_Trsf()
global_coord_system = gp_Ax3()
local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()),
local_coord_system = gp_Ax3(
gp_Pnt(*self.origin.toTuple()),
gp_Dir(*self.zDir.toTuple()),
gp_Dir(*self.xDir.toTuple())
gp_Dir(*self.xDir.toTuple()),
)
forwardT.SetTransformation(global_coord_system,
local_coord_system)
forwardT.SetTransformation(global_coord_system, local_coord_system)
forward.wrapped = gp_GTrsf(forwardT)
inverseT.SetTransformation(local_coord_system,
global_coord_system)
inverseT.SetTransformation(local_coord_system, global_coord_system)
inverse.wrapped = gp_GTrsf(inverseT)
# TODO verify if this is OK
@ -767,9 +779,7 @@ class BoundBox(object):
self.zmax = ZMax
self.zlen = ZMax - ZMin
self.center = Vector((XMax + XMin) / 2,
(YMax + YMin) / 2,
(ZMax + ZMin) / 2)
self.center = Vector((XMax + XMin) / 2, (YMax + YMin) / 2, (ZMax + ZMin) / 2)
self.DiagonalLength = self.wrapped.SquareExtent() ** 0.5
@ -810,25 +820,29 @@ class BoundBox(object):
the built-in implementation i do not understand.
"""
if (bb1.XMin < bb2.XMin and
bb1.XMax > bb2.XMax and
bb1.YMin < bb2.YMin and
bb1.YMax > bb2.YMax):
if (
bb1.XMin < bb2.XMin
and bb1.XMax > bb2.XMax
and bb1.YMin < bb2.YMin
and bb1.YMax > bb2.YMax
):
return bb1
if (bb2.XMin < bb1.XMin and
bb2.XMax > bb1.XMax and
bb2.YMin < bb1.YMin and
bb2.YMax > bb1.YMax):
if (
bb2.XMin < bb1.XMin
and bb2.XMax > bb1.XMax
and bb2.YMin < bb1.YMin
and bb2.YMax > bb1.YMax
):
return bb2
return None
@classmethod
def _fromTopoDS(cls, shape, tol=None, optimal=False):
'''
"""
Constructs a bounding box from a TopoDS_Shape
'''
"""
tol = TOL if tol is None else tol # tol = TOL (by default)
bbox = Bnd_Box()
bbox.SetGap(tol)
@ -845,12 +859,14 @@ class BoundBox(object):
def isInside(self, b2):
"""Is the provided bounding box inside this one?"""
if (b2.xmin > self.xmin and
b2.ymin > self.ymin and
b2.zmin > self.zmin and
b2.xmax < self.xmax and
b2.ymax < self.ymax and
b2.zmax < self.zmax):
if (
b2.xmin > self.xmin
and b2.ymin > self.ymin
and b2.zmin > self.zmin
and b2.xmax < self.xmax
and b2.ymax < self.ymax
and b2.zmax < self.zmax
):
return True
else:
return False

View File

@ -6,8 +6,7 @@ from xml.etree import ElementTree
from .geom import BoundBox
BOILERPLATE = \
'''
BOILERPLATE = """
<link rel='stylesheet' type='text/css' href='http://www.x3dom.org/download/x3dom.css'></link>
<div style='height: {height}px; width: 100%;' width='100%' height='{height}px'>
<x3d style='height: {height}px; width: 100%;' id='{id}' width='100%' height='{height}px'>
@ -35,18 +34,27 @@ BOILERPLATE = \
//document.getElementById('{id}').runtime.fitAll()
</script>
'''
"""
# https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file
# better if else
ROT = (0.77, 0.3, 0.55, 1.28)
ROT = (0.,0,0,1.)
ROT = (0.0, 0, 0, 1.0)
FOV = 0.2
def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, rot='{} {} {} {} '.format(*ROT)):
return BOILERPLATE.format(src=src,
def add_x3d_boilerplate(
src,
height=400,
center=(0, 0, 0),
d=(0, 0, 15),
fov=FOV,
rot="{} {} {} {} ".format(*ROT),
):
return BOILERPLATE.format(
src=src,
id=uuid4(),
height=height,
x=d[0],
@ -56,9 +64,12 @@ def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, ro
y0=center[1],
z0=center[2],
fov=fov,
rot=rot)
rot=rot,
)
def x3d_display(shape,
def x3d_display(
shape,
vertex_shader=None,
fragment_shader=None,
export_edges=True,
@ -67,11 +78,13 @@ def x3d_display(shape,
shininess=0.4,
transparency=0.4,
line_color=(0, 0, 0),
line_width=2.,
mesh_quality=.3):
line_width=2.0,
mesh_quality=0.3,
):
# Export to XML <Scene> tag
exporter = X3DExporter(shape,
exporter = X3DExporter(
shape,
vertex_shader,
fragment_shader,
export_edges,
@ -81,12 +94,13 @@ def x3d_display(shape,
transparency,
line_color,
line_width,
mesh_quality)
mesh_quality,
)
exporter.compute()
x3d_str = exporter.to_x3dfile_string(shape_id=0)
xml_et = ElementTree.fromstring(x3d_str)
scene_tag = xml_et.find('./Scene')
scene_tag = xml_et.find("./Scene")
# Viewport Parameters
bb = BoundBox._fromTopoDS(shape)
@ -98,6 +112,8 @@ def x3d_display(shape,
vec = quat * (vec) + c.wrapped
# return boilerplate + Scene
return add_x3d_boilerplate(ElementTree.tostring(scene_tag).decode('utf-8'),
return add_x3d_boilerplate(
ElementTree.tostring(scene_tag).decode("utf-8"),
d=(vec.X(), vec.Y(), vec.Z()),
center=(c.x,c.y,c.z))
center=(c.x, c.y, c.z),
)

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,22 @@ import re
import math
from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound
from collections import defaultdict
from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\
CaselessLiteral, Group, infixNotation, opAssoc, Forward,\
ZeroOrMore, Keyword
from pyparsing import (
Literal,
Word,
nums,
Optional,
Combine,
oneOf,
upcaseTokens,
CaselessLiteral,
Group,
infixNotation,
opAssoc,
Forward,
ZeroOrMore,
Keyword,
)
from functools import reduce
@ -81,7 +94,6 @@ class NearestToPointSelector(Selector):
self.pnt = pnt
def filter(self, objectList):
def dist(tShape):
return tShape.Center().sub(Vector(*self.pnt)).Length
# if tShape.ShapeType == 'Vertex':
@ -121,15 +133,18 @@ class BoxSelector(Selector):
def isInsideBox(p):
# using XOR for checking if x/y/z is in between regardless
# of order of x/y/z0 and x/y/z1
return ((p.x < x0) ^ (p.x < x1)) and \
((p.y < y0) ^ (p.y < y1)) and \
((p.z < z0) ^ (p.z < z1))
return (
((p.x < x0) ^ (p.x < x1))
and ((p.y < y0) ^ (p.y < y1))
and ((p.z < z0) ^ (p.z < z1))
)
for o in objectList:
if self.test_boundingbox:
bb = o.BoundingBox()
if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \
isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)):
if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and isInsideBox(
Vector(bb.xmax, bb.ymax, bb.zmax)
):
result.append(o)
else:
if isInsideBox(o.Center()):
@ -168,7 +183,9 @@ class BaseDirSelector(Selector):
if self.test(normal):
r.append(o)
elif type(o) == Edge and (o.geomType() == 'LINE' or o.geomType() == 'PLANE'):
elif type(o) == Edge and (
o.geomType() == "LINE" or o.geomType() == "PLANE"
):
# an edge is parallel to a direction if its underlying geometry is plane or line
tangent = o.tangentAt()
if self.test(tangent):
@ -247,8 +264,7 @@ class PerpendicularDirSelector(BaseDirSelector):
def test(self, vec):
angle = self.direction.getAngle(vec)
r = (abs(angle) < self.TOLERANCE) or (
abs(angle - math.pi) < self.TOLERANCE)
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE)
return not r
@ -314,17 +330,16 @@ class DirectionMinMaxSelector(Selector):
self.TOLERANCE = tolerance
def filter(self, objectList):
def distance(tShape):
return tShape.Center().dot(self.vector)
# import OrderedDict
from collections import OrderedDict
# make and distance to object dict
objectDict = {distance(el): el for el in objectList}
# transform it into an ordered dict
objectDict = OrderedDict(sorted(list(objectDict.items()),
key=lambda x: x[0]))
objectDict = OrderedDict(sorted(list(objectDict.items()), key=lambda x: x[0]))
# find out the max/min distance
if self.directionMax:
@ -370,8 +385,9 @@ class DirectionNthSelector(ParallelDirSelector):
objectDict[round(distance(el), digits)].append(el)
# choose the Nth unique rounded distance
nth_distance = sorted(list(objectDict.keys()),
reverse=not self.directionMax)[self.N]
nth_distance = sorted(list(objectDict.keys()), reverse=not self.directionMax)[
self.N
]
# map back to original objects and return
return objectDict[nth_distance]
@ -388,8 +404,9 @@ class BinarySelector(Selector):
self.right = right
def filter(self, objectList):
return self.filterResults(self.left.filter(objectList),
self.right.filter(objectList))
return self.filterResults(
self.left.filter(objectList), self.right.filter(objectList)
)
def filterResults(self, r_left, r_right):
raise NotImplementedError
@ -445,52 +462,56 @@ def _makeGrammar():
"""
# float definition
point = Literal('.')
plusmin = Literal('+') | Literal('-')
point = Literal(".")
plusmin = Literal("+") | Literal("-")
number = Word(nums)
integer = Combine(Optional(plusmin) + number)
floatn = Combine(integer + Optional(point + Optional(number)))
# vector definition
lbracket = Literal('(')
rbracket = Literal(')')
comma = Literal(',')
vector = Combine(lbracket + floatn('x') + comma +
floatn('y') + comma + floatn('z') + rbracket)
lbracket = Literal("(")
rbracket = Literal(")")
comma = Literal(",")
vector = Combine(
lbracket + floatn("x") + comma + floatn("y") + comma + floatn("z") + rbracket
)
# direction definition
simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ'])
direction = simple_dir('simple_dir') | vector('vector_dir')
simple_dir = oneOf(["X", "Y", "Z", "XY", "XZ", "YZ"])
direction = simple_dir("simple_dir") | vector("vector_dir")
# CQ type definition
cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'],
caseless=True)
cqtype = oneOf(
["Plane", "Cylinder", "Sphere", "Cone", "Line", "Circle", "Arc"], caseless=True
)
cqtype = cqtype.setParseAction(upcaseTokens)
# type operator
type_op = Literal('%')
type_op = Literal("%")
# direction operator
direction_op = oneOf(['>', '<'])
direction_op = oneOf([">", "<"])
# index definition
ix_number = Group(Optional('-') + Word(nums))
lsqbracket = Literal('[').suppress()
rsqbracket = Literal(']').suppress()
ix_number = Group(Optional("-") + Word(nums))
lsqbracket = Literal("[").suppress()
rsqbracket = Literal("]").suppress()
index = lsqbracket + ix_number('index') + rsqbracket
index = lsqbracket + ix_number("index") + rsqbracket
# other operators
other_op = oneOf(['|', '#', '+', '-'])
other_op = oneOf(["|", "#", "+", "-"])
# named view
named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom'])
named_view = oneOf(["front", "back", "left", "right", "top", "bottom"])
return direction('only_dir') | \
(type_op('type_op') + cqtype('cq_type')) | \
(direction_op('dir_op') + direction('dir') + Optional(index)) | \
(other_op('other_op') + direction('dir')) | \
named_view('named_view')
return (
direction("only_dir")
| (type_op("type_op") + cqtype("cq_type"))
| (direction_op("dir_op") + direction("dir") + Optional(index))
| (other_op("other_op") + direction("dir"))
| named_view("named_view")
)
_grammar = _makeGrammar() # make a grammar instance
@ -506,33 +527,34 @@ class _SimpleStringSyntaxSelector(Selector):
# define all token to object mappings
self.axes = {
'X': Vector(1, 0, 0),
'Y': Vector(0, 1, 0),
'Z': Vector(0, 0, 1),
'XY': Vector(1, 1, 0),
'YZ': Vector(0, 1, 1),
'XZ': Vector(1, 0, 1)
"X": Vector(1, 0, 0),
"Y": Vector(0, 1, 0),
"Z": Vector(0, 0, 1),
"XY": Vector(1, 1, 0),
"YZ": Vector(0, 1, 1),
"XZ": Vector(1, 0, 1),
}
self.namedViews = {
'front': (Vector(0, 0, 1), True),
'back': (Vector(0, 0, 1), False),
'left': (Vector(1, 0, 0), False),
'right': (Vector(1, 0, 0), True),
'top': (Vector(0, 1, 0), True),
'bottom': (Vector(0, 1, 0), False)
"front": (Vector(0, 0, 1), True),
"back": (Vector(0, 0, 1), False),
"left": (Vector(1, 0, 0), False),
"right": (Vector(1, 0, 0), True),
"top": (Vector(0, 1, 0), True),
"bottom": (Vector(0, 1, 0), False),
}
self.operatorMinMax = {
'>': True,
'<': False,
">": True,
"<": False,
}
self.operator = {
'+': DirectionSelector,
'-': lambda v: DirectionSelector(-v),
'#': PerpendicularDirSelector,
'|': ParallelDirSelector}
"+": DirectionSelector,
"-": lambda v: DirectionSelector(-v),
"#": PerpendicularDirSelector,
"|": ParallelDirSelector,
}
self.parseResults = parseResults
self.mySelector = self._chooseSelector(parseResults)
@ -541,23 +563,25 @@ class _SimpleStringSyntaxSelector(Selector):
"""
Sets up the underlying filters accordingly
"""
if 'only_dir' in pr:
if "only_dir" in pr:
vec = self._getVector(pr)
return DirectionSelector(vec)
elif 'type_op' in pr:
elif "type_op" in pr:
return TypeSelector(pr.cq_type)
elif 'dir_op' in pr:
elif "dir_op" in pr:
vec = self._getVector(pr)
minmax = self.operatorMinMax[pr.dir_op]
if 'index' in pr:
return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax)
if "index" in pr:
return DirectionNthSelector(
vec, int("".join(pr.index.asList())), minmax
)
else:
return DirectionMinMaxSelector(vec, minmax)
elif 'other_op' in pr:
elif "other_op" in pr:
vec = self._getVector(pr)
return self.operator[pr.other_op](vec)
@ -569,7 +593,7 @@ class _SimpleStringSyntaxSelector(Selector):
"""
Translate parsed vector string into a CQ Vector
"""
if 'vector_dir' in pr:
if "vector_dir" in pr:
vec = pr.vector_dir
return Vector(float(vec.x), float(vec.y), float(vec.z))
else:
@ -590,10 +614,10 @@ def _makeExpressionGrammar(atom):
"""
# define operators
and_op = Literal('and')
or_op = Literal('or')
delta_op = oneOf(['exc', 'except'])
not_op = Literal('not')
and_op = Literal("and")
or_op = Literal("or")
delta_op = oneOf(["exc", "except"])
not_op = Literal("not")
def atom_callback(res):
return _SimpleStringSyntaxSelector(res)
@ -622,11 +646,15 @@ def _makeExpressionGrammar(atom):
return InverseSelector(right)
# construct the final grammar and set all the callbacks
expr = infixNotation(atom,
[(and_op, 2, opAssoc.LEFT, and_callback),
expr = infixNotation(
atom,
[
(and_op, 2, opAssoc.LEFT, and_callback),
(or_op, 2, opAssoc.LEFT, or_callback),
(delta_op, 2, opAssoc.LEFT, exc_callback),
(not_op, 1, opAssoc.RIGHT, not_callback)])
(not_op, 1, opAssoc.RIGHT, not_callback),
],
)
return expr
@ -690,8 +718,7 @@ class StringSyntaxSelector(Selector):
Feed the input string through the parser and construct an relevant complex selector object
"""
self.selectorString = selectorString
parse_result = _expression_grammar.parseString(selectorString,
parseAll=True)
parse_result = _expression_grammar.parseString(selectorString, parseAll=True)
self.mySelector = parse_result.asList()[0]
def filter(self, objectList):

View File

@ -13,6 +13,7 @@
import sys, os
import os.path
# print "working path is %s" % os.getcwd()
# sys.path.append("../cadquery")
import cadquery
@ -32,32 +33,37 @@ import cadquery
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary','cadquery.cq_directive']
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.autosummary",
"cadquery.cq_directive",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'CadQuery'
copyright = u'Parametric Products Intellectual Holdings LLC, All Rights Reserved'
project = u"CadQuery"
copyright = u"Parametric Products Intellectual Holdings LLC, All Rights Reserved"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
version = "1.0"
# The full version, including alpha/beta/rc tags.
release = '1.0.0'
release = "1.0.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -71,7 +77,7 @@ release = '1.0.0'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
@ -88,7 +94,7 @@ add_module_names = True
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@ -99,7 +105,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'timlinux-linfiniti-sphinx'
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
@ -154,7 +160,7 @@ html_logo = "_static/cqlogo.png"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
@ -198,7 +204,7 @@ html_show_sphinx = False
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'CadQuerydoc'
htmlhelp_basename = "CadQuerydoc"
# -- Options for LaTeX output --------------------------------------------------
@ -206,10 +212,8 @@ htmlhelp_basename = 'CadQuerydoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
@ -217,8 +221,7 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'CadQuery.tex', u'CadQuery Documentation',
u'David Cowden', 'manual'),
("index", "CadQuery.tex", u"CadQuery Documentation", u"David Cowden", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
@ -246,10 +249,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'cadquery', u'CadQuery Documentation',
[u'David Cowden'], 1)
]
man_pages = [("index", "cadquery", u"CadQuery Documentation", [u"David Cowden"], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
@ -261,9 +261,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'CadQuery', u'CadQuery Documentation',
u'David Cowden', 'CadQuery', 'A Fluent CAD api',
'Miscellaneous'),
(
"index",
"CadQuery",
u"CadQuery Documentation",
u"David Cowden",
"CadQuery",
"A Fluent CAD api",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.

View File

@ -9,6 +9,7 @@ dependencies:
- pyparsing
- sphinx
- sphinx_rtd_theme
- black
- codecov
- pytest
- pytest-cov

View File

@ -13,8 +13,13 @@ center_hole_dia = 22.0 # Diameter of center hole in block
# 2. The highest (max) Z face is selected and a new workplane is created on it.
# 3. The new workplane is used to drill a hole through the block.
# 3a. The hole is automatically centered in the workplane.
result = (cq.Workplane("XY").box(length, height, thickness)
.faces(">Z").workplane().hole(center_hole_dia))
result = (
cq.Workplane("XY")
.box(length, height, thickness)
.faces(">Z")
.workplane()
.hole(center_hole_dia)
)
# Displays the result of this script
show_object(result)

View File

@ -26,12 +26,20 @@ cbore_depth = 2.1 # Bolt head pocket hole depth
# do not show up in the final displayed geometry.
# 6. The vertices of the rectangle (corners) are selected, and a counter-bored
# hole is placed at each of the vertices (all 4 of them at once).
result = (cq.Workplane("XY").box(length, height, thickness)
.faces(">Z").workplane().hole(center_hole_dia)
.faces(">Z").workplane()
result = (
cq.Workplane("XY")
.box(length, height, thickness)
.faces(">Z")
.workplane()
.hole(center_hole_dia)
.faces(">Z")
.workplane()
.rect(length - cbore_inset, height - cbore_inset, forConstruction=True)
.vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth)
.edges("|Z").fillet(2.0))
.vertices()
.cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth)
.edges("|Z")
.fillet(2.0)
)
# Displays the result of this script
show_object(result)

View File

@ -21,9 +21,12 @@ rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate
# plate with a rectangular hole in the center.
# 3a. circle() and rect() could be changed to any other shape to completely
# change the resulting plate and/or the hole in it.
result = (cq.Workplane("front").circle(circle_radius)
result = (
cq.Workplane("front")
.circle(circle_radius)
.rect(rectangle_width, rectangle_length)
.extrude(thickness))
.extrude(thickness)
)
# Displays the result of this script
show_object(result)

View File

@ -34,12 +34,16 @@ thickness = 0.25 # Thickness of the plate
# 7a. Without the close(), the 2D sketch will be left open and the extrude
# operation will provide unpredictable results.
# 8. The 2D sketch is extruded into a solid object of the specified thickness.
result = (cq.Workplane("front").lineTo(width, 0)
result = (
cq.Workplane("front")
.lineTo(width, 0)
.lineTo(width, 1.0)
.threePointArc((1.0, 1.5), (0.0, 1.0))
.sagittaArc((-0.5, 1.0), 0.2)
.radiusArc((-0.7, -0.2), -1.5)
.close().extrude(thickness))
.close()
.extrude(thickness)
)
# Displays the result of this script
show_object(result)

View File

@ -30,10 +30,13 @@ polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points
# like cutBlind() assume a positive cut direction, but cutThruAll() assumes
# instead that the cut is made from a max direction and cuts downward from
# that max through all objects.
result = (cq.Workplane("front").box(width, height, thickness)
result = (
cq.Workplane("front")
.box(width, height, thickness)
.pushPoints([(0, 0.75), (0, -0.75)])
.polygon(polygon_sides, polygon_dia)
.cutThruAll())
.cutThruAll()
)
# Displays the result of this script
show_object(result)

View File

@ -12,7 +12,7 @@ pts = [
(t / 2.0, (t - H / 2.0)),
(W / 2.0, (t - H / 2.0)),
(W / 2.0, H / -2.0),
(0, H/-2.0)
(0, H / -2.0),
]
# We generate half of the I-beam outline and then mirror it to create the full
@ -30,10 +30,7 @@ pts = [
# 3. Only half of the I-beam profile has been drawn so far. That half is
# mirrored around the Y-axis to create the complete I-beam profile.
# 4. The I-beam profile is extruded to the final length of the beam.
result = (cq.Workplane("front").moveTo(0, H/2.0)
.polyline(pts)
.mirrorY()
.extrude(L))
result = cq.Workplane("front").moveTo(0, H / 2.0).polyline(pts).mirrorY().extrude(L)
# Displays the result of this script
show_object(result)

View File

@ -13,7 +13,7 @@ sPnts = [
(1.5, 1.0),
(1.0, 1.25),
(0.5, 1.0),
(0, 1.0)
(0, 1.0),
]
# 2. Generate our plate with the spline feature and make sure it is a

View File

@ -13,10 +13,16 @@ import cadquery as cq
# 6. Selects the vertices of the for-construction rectangle.
# 7. Places holes at the center of each selected vertex.
# 7a. Since the workplane is rotated, this results in angled holes in the face.
result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z")
result = (
cq.Workplane("front")
.box(4.0, 4.0, 0.25)
.faces(">Z")
.workplane()
.transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0))
.rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25))
.rect(1.5, 1.5, forConstruction=True)
.vertices()
.hole(0.25)
)
# Displays the result of this script
show_object(result)

View File

@ -12,10 +12,15 @@ import cadquery as cq
# other geometry.
# 6. Selects the vertices of the for-construction rectangle.
# 7. Places holes at the center of each selected vertex.
result = (cq.Workplane("front").box(2, 2, 0.5)
.faces(">Z").workplane()
.rect(1.5, 1.5, forConstruction=True).vertices()
.hole(0.125))
result = (
cq.Workplane("front")
.box(2, 2, 0.5)
.faces(">Z")
.workplane()
.rect(1.5, 1.5, forConstruction=True)
.vertices()
.hole(0.125)
)
# Displays the result of this script
show_object(result)

View File

@ -11,10 +11,15 @@ import cadquery as cq
# 5. Creates a workplane 3 mm above the face the circle was drawn on.
# 6. Draws a 2D circle on the new, offset workplane.
# 7. Creates a loft between the circle and the rectangle.
result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z")
.circle(1.5).workplane(offset=3.0)
result = (
cq.Workplane("front")
.box(4.0, 4.0, 0.25)
.faces(">Z")
.circle(1.5)
.workplane(offset=3.0)
.rect(0.75, 0.5)
.loft(combine=True))
.loft(combine=True)
)
# Displays the result of this script
show_object(result)

View File

@ -11,9 +11,15 @@ import cadquery as cq
# function.
# 5a. When the depth of the counter-sink hole is set to None, the hole will be
# cut through.
result = (cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z")
.workplane().rect(3.5, 1.5, forConstruction=True)
.vertices().cskHole(0.125, 0.25, 82.0, depth=None))
result = (
cq.Workplane(cq.Plane.XY())
.box(4, 2, 0.5)
.faces(">Z")
.workplane()
.rect(3.5, 1.5, forConstruction=True)
.vertices()
.cskHole(0.125, 0.25, 82.0, depth=None)
)
# Displays the result of this script
show_object(result)

View File

@ -9,8 +9,7 @@ import cadquery as cq
# that new geometry can be built on.
# 4. Draws a 2D circle on the new workplane and then uses it to cut a hole
# all the way through the box.
c = (cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane()
.circle(0.25).cutThruAll())
c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll()
# 5. Selects the face furthest away from the origin in the +Y axis direction.
# 6. Creates an offset workplane that is set in the center of the object.

View File

@ -1,11 +1,7 @@
import cadquery as cq
# Points we will use to create spline and polyline paths to sweep over
pts = [
(0, 1),
(1, 2),
(2, 4)
]
pts = [(0, 1), (1, 2), (2, 4)]
# Spline path generated from our list of points (tuples)
path = cq.Workplane("XZ").spline(pts)

View File

@ -4,37 +4,80 @@ import cadquery as cq
path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0)
# Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0
defaultSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0).
workplane(offset=10.0).circle(1.0).
workplane(offset=10.0).circle(2.0).sweep(path, multisection=True))
defaultSweep = (
cq.Workplane("YZ")
.workplane(offset=-10.0)
.circle(2.0)
.workplane(offset=10.0)
.circle(1.0)
.workplane(offset=10.0)
.circle(2.0)
.sweep(path, multisection=True)
)
# We can sweep thrue different shapes
recttocircleSweep = (cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0).
workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0).
workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True))
recttocircleSweep = (
cq.Workplane("YZ")
.workplane(offset=-10.0)
.rect(2.0, 2.0)
.workplane(offset=8.0)
.circle(1.0)
.workplane(offset=4.0)
.circle(1.0)
.workplane(offset=8.0)
.rect(2.0, 2.0)
.sweep(path, multisection=True)
)
circletorectSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0).
workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0).
workplane(offset=7.0).circle(1.0).sweep(path, multisection=True))
circletorectSweep = (
cq.Workplane("YZ")
.workplane(offset=-10.0)
.circle(1.0)
.workplane(offset=7.0)
.rect(2.0, 2.0)
.workplane(offset=6.0)
.rect(2.0, 2.0)
.workplane(offset=7.0)
.circle(1.0)
.sweep(path, multisection=True)
)
# Placement of the Shape is important otherwise could produce unexpected shape
specialSweep = (cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0).
sweep(path, multisection=True))
specialSweep = (
cq.Workplane("YZ")
.circle(1.0)
.workplane(offset=10.0)
.rect(2.0, 2.0)
.sweep(path, multisection=True)
)
# Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0
path = (cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4).
threePointArc((4, 0), (0, -4)).lineTo(-5, -4))
path = (
cq.Workplane("XZ")
.moveTo(-5, 4)
.lineTo(0, 4)
.threePointArc((4, 0), (0, -4))
.lineTo(-5, -4)
)
# Placement of different shapes should follow the path
# cylinder r=1.5 along first line
# then sweep allong arc from r=1.5 to r=1.0
# then cylinder r=1.0 along last line
arcSweep = (cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5).
workplane(offset=5).circle(1.5).
moveTo(0, -8).circle(1.0).
workplane(offset=-5).circle(1.0).
sweep(path, multisection=True))
arcSweep = (
cq.Workplane("YZ")
.workplane(offset=-5)
.moveTo(0, 4)
.circle(1.5)
.workplane(offset=5)
.circle(1.5)
.moveTo(0, -8)
.circle(1.0)
.workplane(offset=-5)
.circle(1.0)
.sweep(path, multisection=True)
)
# Translate the resulting solids so that they do not overlap and display them left to right
@ -43,5 +86,3 @@ show_object(circletorectSweep.translate((0, 5, 0)))
show_object(recttocircleSweep.translate((0, 10, 0)))
show_object(specialSweep.translate((0, 15, 0)))
show_object(arcSweep.translate((0, -5, 0)))

View File

@ -32,23 +32,37 @@ s = cq.Workplane("XY").box(total_length, total_width, height)
s = s.faces("<Z").shell(-1.0 * t)
# make the bumps on the top
s = (s.faces(">Z").workplane().
rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0)
.extrude(bumpHeight))
s = (
s.faces(">Z")
.workplane()
.rarray(pitch, pitch, lbumps, wbumps, True)
.circle(bumpDiam / 2.0)
.extrude(bumpHeight)
)
# add posts on the bottom. posts are different diameter depending on geometry
# solid studs for 1 bump, tubes for multiple, none for 1x1
tmp = s.faces("<Z").workplane(invert=True)
if lbumps > 1 and wbumps > 1:
tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True).
circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t))
tmp = (
tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True)
.circle(postDiam / 2.0)
.circle(bumpDiam / 2.0)
.extrude(height - t)
)
elif lbumps > 1:
tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True).
circle(t).extrude(height - t))
tmp = (
tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True)
.circle(t)
.extrude(height - t)
)
elif wbumps > 1:
tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True).
circle(t).extrude(height - t))
tmp = (
tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True)
.circle(t)
.extrude(height - t)
)
else:
tmp = s

View File

@ -16,42 +16,47 @@ from setuptools import setup
# if we are building in travis, use the build number as the sub-minor version
version = '0.5-SNAPSHOT'
if 'TRAVIS_TAG' in os.environ.keys():
version= os.environ['TRAVIS_TAG']
version = "0.5-SNAPSHOT"
if "TRAVIS_TAG" in os.environ.keys():
version = os.environ["TRAVIS_TAG"]
setup(
name='cadquery',
name="cadquery",
version=version,
url='https://github.com/dcowden/cadquery',
license='Apache Public License 2.0',
author='David Cowden',
author_email='dave.cowden@gmail.com',
description='CadQuery is a parametric scripting language for creating and traversing CAD models',
long_description=open('README.md').read(),
packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'],
url="https://github.com/dcowden/cadquery",
license="Apache Public License 2.0",
author="David Cowden",
author_email="dave.cowden@gmail.com",
description="CadQuery is a parametric scripting language for creating and traversing CAD models",
long_description=open("README.md").read(),
packages=[
"cadquery",
"cadquery.contrib",
"cadquery.occ_impl",
"cadquery.plugins",
"tests",
],
include_package_data=True,
zip_safe=False,
platforms='any',
test_suite='tests',
platforms="any",
test_suite="tests",
classifiers=[
'Development Status :: 5 - Production/Stable',
"Development Status :: 5 - Production/Stable",
#'Development Status :: 6 - Mature',
#'Development Status :: 7 - Inactive',
'Intended Audience :: Developers',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Information Technology',
'Intended Audience :: Science/Research',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX',
'Operating System :: MacOS',
'Operating System :: Unix',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Internet',
'Topic :: Scientific/Engineering'
]
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX",
"Operating System :: MacOS",
"Operating System :: Unix",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet",
"Topic :: Scientific/Engineering",
],
)

View File

@ -6,21 +6,23 @@ import os
def readFileAsString(fileName):
f = open(fileName, 'r')
f = open(fileName, "r")
s = f.read()
f.close()
return s
def writeStringToFile(strToWrite, fileName):
f = open(fileName, 'w')
f = open(fileName, "w")
f.write(strToWrite)
f.close()
def makeUnitSquareWire():
V = Vector
return Wire.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)])
return Wire.makePolygon(
[V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)]
)
def makeUnitCube():
@ -38,26 +40,24 @@ def toTuple(v):
elif type(v) == Vector:
return v.toTuple()
else:
raise RuntimeError(
"dont know how to convert type %s to tuple" % str(type(v)))
raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v)))
class BaseTest(unittest.TestCase):
def assertTupleAlmostEquals(self, expected, actual, places):
for i, j in zip(actual, expected):
self.assertAlmostEqual(i, j, places)
__all__ = [
'TestCadObjects',
'TestCadQuery',
'TestCQGI',
'TestCQSelectors',
'TestCQSelectors',
'TestExporters',
'TestImporters',
'TestJupyter',
'TestWorkplanes',
'TestAssembleEdges',
"TestCadObjects",
"TestCadQuery",
"TestCQGI",
"TestCQSelectors",
"TestCQSelectors",
"TestExporters",
"TestImporters",
"TestJupyter",
"TestWorkplanes",
"TestAssembleEdges",
]

View File

@ -3,9 +3,11 @@ import sys
import unittest
from tests import BaseTest
from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ, gp_XYZ
from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex,
from OCC.BRepBuilderAPI import (
BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace)
BRepBuilderAPI_MakeFace,
)
from OCC.GC import GC_MakeCircle
@ -13,11 +15,9 @@ from cadquery import *
class TestCadObjects(BaseTest):
def _make_circle(self):
circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()),
2.)
circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()), 2.0)
return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
def testVectorConstructors(self):
@ -40,9 +40,9 @@ class TestCadObjects(BaseTest):
v9 = Vector()
self.assertTupleAlmostEquals((0, 0, 0), v9.toTuple(), 4)
v9.x = 1.
v9.y = 2.
v9.z = 3.
v9.x = 1.0
v9.y = 2.0
v9.z = 3.0
self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4)
def testVertex(self):
@ -70,20 +70,22 @@ class TestCadObjects(BaseTest):
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3)
def testEdgeWrapperMakeCircle(self):
halfCircleEdge = Edge.makeCircle(radius=10, pnt=(
0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180)
halfCircleEdge = Edge.makeCircle(
radius=10, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180
)
# self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3)
self.assertTupleAlmostEquals(
(10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3)
(10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3
)
self.assertTupleAlmostEquals(
(-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3)
(-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3
)
def testFaceWrapperMakePlane(self):
mplane = Face.makePlane(10, 10)
self.assertTupleAlmostEquals(
(0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3)
self.assertTupleAlmostEquals((0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3)
def testCenterOfBoundBox(self):
pass
@ -109,12 +111,15 @@ class TestCadObjects(BaseTest):
Workplane.cyl = cylinders
# Now test. here we want weird workplane to see if the objects are transformed right
s = Workplane("XY").rect(
2.0, 3.0, forConstruction=True).vertices().cyl(0.25, 0.5)
s = (
Workplane("XY")
.rect(2.0, 3.0, forConstruction=True)
.vertices()
.cyl(0.25, 0.5)
)
self.assertEqual(4, len(s.val().Solids()))
self.assertTupleAlmostEquals(
(0.0, 0.0, 0.25), s.val().Center().toTuple(), 3)
self.assertTupleAlmostEquals((0.0, 0.0, 0.25), s.val().Center().toTuple(), 3)
def testDot(self):
v1 = Vector(2, 2, 2)
@ -163,24 +168,30 @@ class TestCadObjects(BaseTest):
# test passing Plane object
point = Vector(10, 11, 12).projectToPlane(Plane(base, x_dir, normal))
self.assertTupleAlmostEquals(point.toTuple(), (59/7, 55/7, 51/7),
decimal_places)
self.assertTupleAlmostEquals(
point.toTuple(), (59 / 7, 55 / 7, 51 / 7), decimal_places
)
def testMatrixCreationAndAccess(self):
def matrix_vals(m):
return [[m[r, c] for c in range(4)] for r in range(4)]
# default constructor creates a 4x4 identity matrix
m = Matrix()
identity = [[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]]
identity = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
self.assertEqual(identity, matrix_vals(m))
vals4x4 = [[1., 0., 0., 1.],
[0., 1., 0., 2.],
[0., 0., 1., 3.],
[0., 0., 0., 1.]]
vals4x4 = [
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 2.0],
[0.0, 0.0, 1.0, 3.0],
[0.0, 0.0, 0.0, 1.0],
]
vals4x4_tuple = tuple(tuple(r) for r in vals4x4)
# test constructor with 16-value input
@ -197,10 +208,12 @@ class TestCadObjects(BaseTest):
self.assertEqual(vals4x4, matrix_vals(m))
# Test 16-value input with invalid values for the last 4
invalid = [[1., 0., 0., 1.],
[0., 1., 0., 2.],
[0., 0., 1., 3.],
[1., 2., 3., 4.]]
invalid = [
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 2.0],
[0.0, 0.0, 1.0, 3.0],
[1.0, 2.0, 3.0, 4.0],
]
with self.assertRaises(ValueError):
Matrix(invalid)
@ -212,7 +225,7 @@ class TestCadObjects(BaseTest):
# Invalid sub-type
with self.assertRaises(TypeError):
Matrix([[1, 2, 3, 4], 'abc', [1, 2, 3, 4]])
Matrix([[1, 2, 3, 4], "abc", [1, 2, 3, 4]])
# test out-of-bounds access
m = Matrix()
@ -221,8 +234,7 @@ class TestCadObjects(BaseTest):
with self.assertRaises(IndexError):
m[4, 0]
with self.assertRaises(IndexError):
m['ab']
m["ab"]
def testTranslate(self):
e = Edge.makeCircle(2, (1, 2, 3))
@ -231,54 +243,53 @@ class TestCadObjects(BaseTest):
self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3)
def testVertices(self):
e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0),
gp_Pnt(1, 1, 0)).Edge())
e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(1, 1, 0)).Edge())
self.assertEqual(2, len(e.Vertices()))
def testPlaneEqual(self):
# default orientation
self.assertEqual(
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)),
Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1))
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)),
)
# moved origin
self.assertEqual(
Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)),
Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1))
Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)),
)
# moved x-axis
self.assertEqual(
Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)),
Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1))
Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)),
)
# moved z-axis
self.assertEqual(
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)),
Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1))
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)),
)
def testPlaneNotEqual(self):
# type difference
for value in [None, 0, 1, 'abc']:
for value in [None, 0, 1, "abc"]:
self.assertNotEqual(
Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)),
value
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), value
)
# origin difference
self.assertNotEqual(
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)),
Plane(origin=(0,0,1), xDir=(1,0,0), normal=(0,0,1))
Plane(origin=(0, 0, 1), xDir=(1, 0, 0), normal=(0, 0, 1)),
)
# x-axis difference
self.assertNotEqual(
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)),
Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1))
Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)),
)
# z-axis difference
self.assertNotEqual(
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)),
Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1))
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)),
)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -42,8 +42,9 @@ class TestCQGI(BaseTest):
model = cqgi.CQModel(TESTSCRIPT)
metadata = model.metadata
self.assertEqual(set(metadata.parameters.keys()), {
'height', 'width', 'a', 'b', 'foo'})
self.assertEqual(
set(metadata.parameters.keys()), {"height", "width", "a", "b", "foo"}
)
def test_build_with_debug(self):
model = cqgi.CQModel(TEST_DEBUG_SCRIPT)
@ -51,7 +52,7 @@ class TestCQGI(BaseTest):
debugItems = result.debugObjects
self.assertTrue(len(debugItems) == 2)
self.assertTrue(debugItems[0].shape == "bar")
self.assertTrue(debugItems[0].options == {"color": 'yellow'})
self.assertTrue(debugItems[0].options == {"color": "yellow"})
self.assertTrue(debugItems[1].shape == 2.0)
self.assertTrue(debugItems[1].options == {})
@ -65,7 +66,7 @@ class TestCQGI(BaseTest):
def test_build_with_different_params(self):
model = cqgi.CQModel(TESTSCRIPT)
result = model.build({'height': 3.0})
result = model.build({"height": 3.0})
self.assertTrue(result.results[0].shape == "3.0|3.0|bar|1.0")
def test_describe_parameters(self):
@ -76,9 +77,9 @@ class TestCQGI(BaseTest):
"""
)
model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a']
a_param = model.metadata.parameters["a"]
self.assertTrue(a_param.default_value == 2.0)
self.assertTrue(a_param.desc == 'FirstLetter')
self.assertTrue(a_param.desc == "FirstLetter")
self.assertTrue(a_param.varType == cqgi.NumberParameterType)
def test_describe_parameter_invalid_doesnt_fail_script(self):
@ -89,8 +90,8 @@ class TestCQGI(BaseTest):
"""
)
model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a']
self.assertTrue(a_param.name == 'a')
a_param = model.metadata.parameters["a"]
self.assertTrue(a_param.name == "a")
def test_build_with_exception(self):
badscript = textwrap.dedent(
@ -115,7 +116,7 @@ class TestCQGI(BaseTest):
with self.assertRaises(Exception) as context:
model = cqgi.CQModel(badscript)
self.assertTrue('invalid syntax' in context.exception.args)
self.assertTrue("invalid syntax" in context.exception.args)
def test_that_two_results_are_returned(self):
script = textwrap.dedent(
@ -140,7 +141,7 @@ class TestCQGI(BaseTest):
show_object(h)
"""
)
result = cqgi.parse(script).build({'h': 33.33})
result = cqgi.parse(script).build({"h": 33.33})
self.assertEqual(result.results[0].shape, "33.33")
def test_that_assigning_string_to_number_fails(self):
@ -150,9 +151,8 @@ class TestCQGI(BaseTest):
show_object(h)
"""
)
result = cqgi.parse(script).build({'h': "a string"})
self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
result = cqgi.parse(script).build({"h": "a string"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
def test_that_assigning_unknown_var_fails(self):
script = textwrap.dedent(
@ -162,9 +162,8 @@ class TestCQGI(BaseTest):
"""
)
result = cqgi.parse(script).build({'w': "var is not there"})
self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
result = cqgi.parse(script).build({"w": "var is not there"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
def test_that_cq_objects_are_visible(self):
script = textwrap.dedent(
@ -198,10 +197,10 @@ class TestCQGI(BaseTest):
"""
)
result = cqgi.parse(script).build({'h': False})
result = cqgi.parse(script).build({"h": False})
self.assertTrue(result.success)
self.assertEqual(result.first_result.shape, '*False*')
self.assertEqual(result.first_result.shape, "*False*")
def test_that_only_top_level_vars_are_detected(self):
script = textwrap.dedent(

View File

@ -12,7 +12,6 @@ from tests import BaseTest
class TestExporters(BaseTest):
def _exportBox(self, eType, stringsToFind):
"""
Exports a test object, and then looks for
@ -28,24 +27,25 @@ class TestExporters(BaseTest):
exporters.exportShape(p, eType, s, 0.1)
result = '{}'.format(s.getvalue())
result = "{}".format(s.getvalue())
for q in stringsToFind:
self.assertTrue(result.find(q) > -1)
return result
def testSTL(self):
self._exportBox(exporters.ExportTypes.STL, ['facet normal'])
self._exportBox(exporters.ExportTypes.STL, ["facet normal"])
def testSVG(self):
self._exportBox(exporters.ExportTypes.SVG, ['<svg', '<g transform'])
self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
def testAMF(self):
self._exportBox(exporters.ExportTypes.AMF, ['<amf units', '</object>'])
self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"])
def testSTEP(self):
self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA'])
self._exportBox(exporters.ExportTypes.STEP, ["FILE_SCHEMA"])
def testTJS(self):
self._exportBox(exporters.ExportTypes.TJS, [
'vertices', 'formatVersion', 'faces'])
self._exportBox(
exporters.ExportTypes.TJS, ["vertices", "formatVersion", "faces"]
)

View File

@ -36,12 +36,18 @@ class TestImporters(BaseTest):
self.assertTrue(importedShape.val().ShapeType() == "Solid")
# Check the number of faces and vertices per face to make sure we have a box shape
self.assertTrue(importedShape.faces("+X").size() ==
1 and importedShape.faces("+X").vertices().size() == 4)
self.assertTrue(importedShape.faces("+Y").size() ==
1 and importedShape.faces("+Y").vertices().size() == 4)
self.assertTrue(importedShape.faces("+Z").size() ==
1 and importedShape.faces("+Z").vertices().size() == 4)
self.assertTrue(
importedShape.faces("+X").size() == 1
and importedShape.faces("+X").vertices().size() == 4
)
self.assertTrue(
importedShape.faces("+Y").size() == 1
and importedShape.faces("+Y").vertices().size() == 4
)
self.assertTrue(
importedShape.faces("+Z").size() == 1
and importedShape.faces("+Z").vertices().size() == 4
)
def testSTEP(self):
"""
@ -55,7 +61,8 @@ class TestImporters(BaseTest):
not segfault.
"""
tmpfile = OUTDIR + "/badSTEP.step"
with open(tmpfile, 'w') as f: f.write("invalid STEP file")
with open(tmpfile, "w") as f:
f.write("invalid STEP file")
with self.assertRaises(ValueError):
importers.importShape(importers.ImportTypes.STEP, tmpfile)
@ -69,6 +76,8 @@ class TestImporters(BaseTest):
objs = importers.importShape(importers.ImportTypes.STEP, filename)
self.assertEqual(2, len(objs.all()))
if __name__ == '__main__':
if __name__ == "__main__":
import unittest
unittest.main()

View File

@ -2,9 +2,10 @@ from tests import BaseTest
import cadquery
class TestJupyter(BaseTest):
def test_repr_html(self):
cube = cadquery.Workplane('XY').box(1, 1, 1)
cube = cadquery.Workplane("XY").box(1, 1, 1)
shape = cube.val()
self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid)

View File

@ -1,4 +1,4 @@
__author__ = 'dcowden'
__author__ = "dcowden"
"""
Tests for CadQuery Selectors
@ -20,22 +20,19 @@ from cadquery import selectors
class TestCQSelectors(BaseTest):
def testWorkplaneCenter(self):
"Test Moving workplane center"
s = Workplane(Plane.XY())
# current point and world point should be equal
self.assertTupleAlmostEquals(
(0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
# move origin and confirm center moves
s.center(-2.0, -2.0)
# current point should be 0,0, but
self.assertTupleAlmostEquals(
(-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
self.assertTupleAlmostEquals((-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
def testVertices(self):
t = makeUnitSquareWire() # square box
@ -43,8 +40,7 @@ class TestCQSelectors(BaseTest):
self.assertEqual(4, c.vertices().size())
self.assertEqual(4, c.edges().size())
self.assertEqual(0, c.vertices().edges().size()
) # no edges on any vertices
self.assertEqual(0, c.vertices().edges().size()) # no edges on any vertices
# but selecting all edges still yields all vertices
self.assertEqual(4, c.edges().vertices().size())
self.assertEqual(1, c.wires().size()) # just one wire
@ -71,8 +67,7 @@ class TestCQSelectors(BaseTest):
def testFirst(self):
c = CQ(makeUnitCube())
self.assertEqual(type(c.vertices().first().val()), Vertex)
self.assertEqual(
type(c.vertices().first().first().first().val()), Vertex)
self.assertEqual(type(c.vertices().first().first().first().val()), Vertex)
def testCompounds(self):
c = CQ(makeUnitSquareWire())
@ -99,11 +94,11 @@ class TestCQSelectors(BaseTest):
def testFaceTypesFilter(self):
"Filters by face type"
c = CQ(makeUnitCube())
self.assertEqual(c.faces().size(), c.faces('%PLANE').size())
self.assertEqual(c.faces().size(), c.faces('%plane').size())
self.assertEqual(0, c.faces('%sphere').size())
self.assertEqual(0, c.faces('%cone').size())
self.assertEqual(0, c.faces('%SPHERE').size())
self.assertEqual(c.faces().size(), c.faces("%PLANE").size())
self.assertEqual(c.faces().size(), c.faces("%plane").size())
self.assertEqual(0, c.faces("%sphere").size())
self.assertEqual(0, c.faces("%cone").size())
self.assertEqual(0, c.faces("%SPHERE").size())
def testPerpendicularDirFilter(self):
c = CQ(makeUnitCube())
@ -131,10 +126,12 @@ class TestCQSelectors(BaseTest):
# faces parallel to Z axis
self.assertEqual(2, c.faces("|Z").size())
# TODO: provide short names for ParallelDirSelector
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(
Vector((0, 0, 1)))).size()) # same thing as above
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(
Vector((0, 0, -1)))).size()) # same thing as above
self.assertEqual(
2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, 1)))).size()
) # same thing as above
self.assertEqual(
2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, -1)))).size()
) # same thing as above
# just for fun, vertices on faces parallel to z
self.assertEqual(8, c.faces("|Z").vertices().size())
@ -178,97 +175,96 @@ class TestCQSelectors(BaseTest):
self.assertEqual(4, len(el))
def testNthDistance(self):
c = Workplane('XY').pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)
c = Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)
# 2nd face
val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x, -1.5)
# 2nd face with inversed selection vector
val = c.faces(selectors.DirectionNthSelector(
Vector(-1, 0, 0), 1)).val()
val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x, 1.5)
# 2nd last face
val = c.faces(selectors.DirectionNthSelector(
Vector(1, 0, 0), -2)).val()
val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -2)).val()
self.assertAlmostEqual(val.Center().x, 1.5)
# Last face
val = c.faces(selectors.DirectionNthSelector(
Vector(1, 0, 0), -1)).val()
val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -1)).val()
self.assertAlmostEqual(val.Center().x, 2.5)
# check if the selected face if normal to the specified Vector
self.assertAlmostEqual(
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
# repeat the test using string based selector
# 2nd face
val = c.faces('>(1,0,0)[1]').val()
val = c.faces(">(1,0,0)[1]").val()
self.assertAlmostEqual(val.Center().x, -1.5)
val = c.faces('>X[1]').val()
val = c.faces(">X[1]").val()
self.assertAlmostEqual(val.Center().x, -1.5)
# 2nd face with inversed selection vector
val = c.faces('>(-1,0,0)[1]').val()
val = c.faces(">(-1,0,0)[1]").val()
self.assertAlmostEqual(val.Center().x, 1.5)
val = c.faces('<X[1]').val()
val = c.faces("<X[1]").val()
self.assertAlmostEqual(val.Center().x, 1.5)
# 2nd last face
val = c.faces('>X[-2]').val()
val = c.faces(">X[-2]").val()
self.assertAlmostEqual(val.Center().x, 1.5)
# Last face
val = c.faces('>X[-1]').val()
val = c.faces(">X[-1]").val()
self.assertAlmostEqual(val.Center().x, 2.5)
# check if the selected face if normal to the specified Vector
self.assertAlmostEqual(
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
# test selection of multiple faces with the same distance
c = Workplane('XY')\
.box(1, 4, 1, centered=(False, True, False)).faces('<Z')\
.box(2, 2, 2, centered=(True, True, False)).faces('>Z')\
c = (
Workplane("XY")
.box(1, 4, 1, centered=(False, True, False))
.faces("<Z")
.box(2, 2, 2, centered=(True, True, False))
.faces(">Z")
.box(1, 1, 1, centered=(True, True, False))
)
# select 2nd from the bottom (NB python indexing is 0-based)
vals = c.faces('>Z[1]').vals()
vals = c.faces(">Z[1]").vals()
self.assertEqual(len(vals), 2)
val = c.faces('>Z[1]').val()
val = c.faces(">Z[1]").val()
self.assertAlmostEqual(val.Center().z, 1)
# do the same but by selecting 3rd from the top
vals = c.faces('<Z[2]').vals()
vals = c.faces("<Z[2]").vals()
self.assertEqual(len(vals), 2)
val = c.faces('<Z[2]').val()
val = c.faces("<Z[2]").val()
self.assertAlmostEqual(val.Center().z, 1)
# do the same but by selecting 2nd last from the bottom
vals = c.faces('<Z[-2]').vals()
vals = c.faces("<Z[-2]").vals()
self.assertEqual(len(vals), 2)
val = c.faces('<Z[-2]').val()
val = c.faces("<Z[-2]").val()
self.assertAlmostEqual(val.Center().z, 1)
# verify that <Z[-1] is equivalent to <Z
val1 = c.faces('<Z[-1]').val()
val2 = c.faces('<Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(),
val2.Center().toTuple(),
3)
val1 = c.faces("<Z[-1]").val()
val2 = c.faces("<Z").val()
self.assertTupleAlmostEquals(
val1.Center().toTuple(), val2.Center().toTuple(), 3
)
# verify that >Z[-1] is equivalent to >Z
val1 = c.faces('>Z[-1]').val()
val2 = c.faces('>Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(),
val2.Center().toTuple(),
3)
val1 = c.faces(">Z[-1]").val()
val2 = c.faces(">Z").val()
self.assertTupleAlmostEquals(
val1.Center().toTuple(), val2.Center().toTuple(), 3
)
def testNearestTo(self):
c = CQ(makeUnitCube())
@ -302,7 +298,7 @@ class TestCQSelectors(BaseTest):
((0.9, -0.1, -0.1), (1.1, 0.1, 0.1), (1.0, 0.0, 0.0)),
((0.9, 0.9, -0.1), (1.1, 1.1, 0.1), (1.0, 1.0, 0.0)),
((-0.1, 0.9, -0.1), (0.1, 1.1, 0.1), (0.0, 1.0, 0.0)),
((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0))
((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)),
]
for d in test_data_vertices:
@ -318,11 +314,13 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
# test multiple vertices selection
vl = c.vertices(selectors.BoxSelector(
(-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))).vals()
vl = c.vertices(
selectors.BoxSelector((-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))
).vals()
self.assertEqual(2, len(vl))
vl = c.vertices(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))).vals()
vl = c.vertices(
selectors.BoxSelector((-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))
).vals()
self.assertEqual(4, len(vl))
# test edge selection
@ -331,7 +329,7 @@ class TestCQSelectors(BaseTest):
((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)),
((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)),
((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)),
((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0))
((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)),
]
for d in test_data_edges:
@ -347,11 +345,9 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
# test multiple edge selection
el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
self.assertEqual(2, len(el))
el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
self.assertEqual(3, len(el))
# test face selection
@ -360,7 +356,7 @@ class TestCQSelectors(BaseTest):
((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)),
((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)),
((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)),
((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0))
((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)),
]
for d in test_data_faces:
@ -376,22 +372,23 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
# test multiple face selection
fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
self.assertEqual(2, len(fl))
fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
self.assertEqual(3, len(fl))
# test boundingbox option
el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals()
el = c.edges(
selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)
).vals()
self.assertEqual(1, len(el))
fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals()
fl = c.faces(
selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)
).vals()
self.assertEqual(0, len(fl))
fl = c.faces(selectors.BoxSelector(
(-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals()
fl = c.faces(
selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)
).vals()
self.assertEqual(1, len(fl))
def testAndSelector(self):
@ -400,12 +397,13 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector
BS = selectors.BoxSelector
el = c.edges(selectors.AndSelector(
S('|X'), BS((-2, -2, 0.1), (2, 2, 2)))).vals()
el = c.edges(
selectors.AndSelector(S("|X"), BS((-2, -2, 0.1), (2, 2, 2)))
).vals()
self.assertEqual(2, len(el))
# test 'and' (intersection) operator
el = c.edges(S('|X') & BS((-2, -2, 0.1), (2, 2, 2))).vals()
el = c.edges(S("|X") & BS((-2, -2, 0.1), (2, 2, 2))).vals()
self.assertEqual(2, len(el))
# test using extended string syntax
@ -455,27 +453,27 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector
fl = c.faces(selectors.InverseSelector(S('>Z'))).vals()
fl = c.faces(selectors.InverseSelector(S(">Z"))).vals()
self.assertEqual(5, len(fl))
el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals()
el = c.faces(">Z").edges(selectors.InverseSelector(S(">X"))).vals()
self.assertEqual(3, len(el))
# test invert operator
fl = c.faces(-S('>Z')).vals()
fl = c.faces(-S(">Z")).vals()
self.assertEqual(5, len(fl))
el = c.faces('>Z').edges(-S('>X')).vals()
el = c.faces(">Z").edges(-S(">X")).vals()
self.assertEqual(3, len(el))
# test using extended string syntax
fl = c.faces('not >Z').vals()
fl = c.faces("not >Z").vals()
self.assertEqual(5, len(fl))
el = c.faces('>Z').edges('not >X').vals()
el = c.faces(">Z").edges("not >X").vals()
self.assertEqual(3, len(el))
def testComplexStringSelector(self):
c = CQ(makeUnitCube())
v = c.vertices('(>X and >Y) or (<X and <Y)').vals()
v = c.vertices("(>X and >Y) or (<X and <Y)").vals()
self.assertEqual(4, len(v))
def testFaceCount(self):
@ -503,25 +501,27 @@ class TestCQSelectors(BaseTest):
gram = selectors._expression_grammar
expressions = ['+X ',
'-Y',
'|(1,0,0)',
'#(1.,1.4114,-0.532)',
'%Plane',
'>XZ',
'<Z[-2]',
'>(1,4,55.)[20]',
'|XY',
'<YZ[0]',
'front',
'back',
'left',
'right',
'top',
'bottom',
'not |(1,1,0) and >(0,0,1) or XY except >(1,1,1)[-1]',
'(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)',
'not ( <X or >X or <Y or >Y )']
expressions = [
"+X ",
"-Y",
"|(1,0,0)",
"#(1.,1.4114,-0.532)",
"%Plane",
">XZ",
"<Z[-2]",
">(1,4,55.)[20]",
"|XY",
"<YZ[0]",
"front",
"back",
"left",
"right",
"top",
"bottom",
"not |(1,1,0) and >(0,0,1) or XY except >(1,1,1)[-1]",
"(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)",
"not ( <X or >X or <Y or >Y )",
]
for e in expressions:
gram.parseString(e, parseAll=True)

View File

@ -16,31 +16,30 @@ zInvAxis_ = Vector(0, 0, -1)
class TestWorkplanes(BaseTest):
def testYZPlaneOrigins(self):
# xy plane-- with origin at x=0.25
base = Vector(0.25, 0, 0)
p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0))
# origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
# (0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(
base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2)
base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2
)
def testXYPlaneOrigins(self):
base = Vector(0, 0, 0.25)
p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1))
# origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
# (0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2)
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2
)
def testXZPlaneOrigins(self):
base = Vector(0, 0.25, 0)
@ -48,33 +47,39 @@ class TestWorkplanes(BaseTest):
# (0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2)
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2
)
# origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
def testPlaneBasics(self):
p = Plane.XY()
# local to world
self.assertTupleAlmostEquals(
(1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2)
(1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2
)
self.assertTupleAlmostEquals(
(-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2)
(-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2
)
# world to local
self.assertTupleAlmostEquals(
(-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2)
(-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2
)
self.assertTupleAlmostEquals(
(1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2)
(1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2
)
p = Plane.YZ()
self.assertTupleAlmostEquals(
(0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2)
(0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2
)
# world to local
self.assertTupleAlmostEquals(
(1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2)
(1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2
)
p = Plane.XZ()
r = p.toWorldCoords((1, 1)).toTuple()
@ -82,62 +87,68 @@ class TestWorkplanes(BaseTest):
# world to local
self.assertTupleAlmostEquals(
(1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2)
(1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2
)
def testOffsetPlanes(self):
"Tests that a plane offset from the origin works ok too"
p = Plane.XY(origin=(10.0, 10.0, 0))
self.assertTupleAlmostEquals(
(11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2)
self.assertTupleAlmostEquals((2.0, 2.0), p.toLocalCoords(
Vector(12.0, 12.0, 0)).toTuple(), 2)
(11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2
)
self.assertTupleAlmostEquals(
(2.0, 2.0), p.toLocalCoords(Vector(12.0, 12.0, 0)).toTuple(), 2
)
# TODO test these offsets in the other dimensions too
p = Plane.YZ(origin=(0, 2, 2))
self.assertTupleAlmostEquals(
(0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2)
self.assertTupleAlmostEquals((10, 10.0, 0.0), p.toLocalCoords(
Vector(0.0, 12.0, 12.0)).toTuple(), 2)
(0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2
)
self.assertTupleAlmostEquals(
(10, 10.0, 0.0), p.toLocalCoords(Vector(0.0, 12.0, 12.0)).toTuple(), 2
)
p = Plane.XZ(origin=(2, 0, 2))
r = p.toWorldCoords((1.0, 1.0)).toTuple()
self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2)
self.assertTupleAlmostEquals((10.0, 10.0), p.toLocalCoords(
Vector(12.0, 0.0, 12.0)).toTuple(), 2)
self.assertTupleAlmostEquals(
(10.0, 10.0), p.toLocalCoords(Vector(12.0, 0.0, 12.0)).toTuple(), 2
)
def testXYPlaneBasics(self):
p = Plane.named('XY')
p = Plane.named("XY")
self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)
def testYZPlaneBasics(self):
p = Plane.named('YZ')
p = Plane.named("YZ")
self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
def testZXPlaneBasics(self):
p = Plane.named('ZX')
p = Plane.named("ZX")
self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
def testXZPlaneBasics(self):
p = Plane.named('XZ')
p = Plane.named("XZ")
self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
def testYXPlaneBasics(self):
p = Plane.named('YX')
p = Plane.named("YX")
self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
def testZYPlaneBasics(self):
p = Plane.named('ZY')
p = Plane.named("ZY")
self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)