Fixing accidental submodule creation.

This commit is contained in:
Jeremy Wright 2014-12-04 23:34:54 -05:00
parent 8c8fb5b068
commit 9b9d75db2e
15 changed files with 6212 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit 40efd2599a867d633037fe3d47fe8299325f7b50

2255
CadQuery/Libs/cadquery/CQ.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
***
Core CadQuery implementation.
No files should depend on or import FreeCAD , pythonOCC, or other CAD Kernel libraries!!!
Dependencies should be on the classes provided by implementation packages, which in turn
can depend on CAD libraries.
***

View File

@ -0,0 +1,19 @@
#these items point to the freecad implementation
from .freecad_impl.geom import Plane,BoundBox,Vector,Matrix,sortWiresByBuildOrder
from .freecad_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound
from .freecad_impl import exporters
from .freecad_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 .CQ import CQ,CQContext,Workplane
__all__ = [
'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters', 'importers', 'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector','TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins'
]
__version__ = "0.1.7"

View File

@ -0,0 +1,18 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""

View File

@ -0,0 +1,85 @@
"""
A special directive for including a cq object.
"""
import sys, os, shutil, imp, warnings, cStringIO, re,traceback
from cadquery import *
import StringIO
from docutils.parsers.rst import directives
template = """
.. raw:: html
<div class="cq" style="text-align:%(txtAlign)s;float:left;">
%(outSVG)s
</div>
<div style="clear:both;">
</div>
"""
template_content_indent = ' '
def cq_directive(name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
#only consider inline snippets
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
#be a CQ object
outSVG = "Your Script Did not assign the 'result' variable!"
try:
_s = StringIO.StringIO()
exec(plot_code)
exporters.exportShape(result,"SVG",_s)
outSVG = _s.getvalue()
except:
traceback.print_exc()
outSVG = traceback.format_exc()
#now out
# Now start generating the lines of output
lines = []
#get rid of new lines
outSVG = outSVG.replace('\n','')
txtAlign = "left"
if options.has_key("align"):
txtAlign = options['align']
lines.extend((template % locals()).split('\n'))
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))
return []
def setup(app):
setup.app = 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
}
app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)

View File

@ -0,0 +1,3 @@
It is ok for files in this directory to import FreeCAD, FreeCAD.Base, and FreeCAD.Part.
Other modules should _not_ depend on FreeCAD

View File

@ -0,0 +1,88 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""
import os, sys
def _fc_path():
"""Find FreeCAD"""
_PATH = ""
if _PATH:
return _PATH
#look for FREECAD_LIB env variable
if os.environ.has_key('FREECAD_LIB'):
_PATH = os.environ.get('FREECAD_LIB')
if os.path.exists( _PATH):
return _PATH
if sys.platform.startswith('linux'):
#Make some dangerous assumptions...
for _PATH in [
os.path.join(os.path.expanduser("~"), "lib/freecad/lib"),
"/usr/local/lib/freecad/lib",
"/usr/lib/freecad/lib",
]:
if os.path.exists(_PATH):
return _PATH
elif sys.platform.startswith('win'):
#try all the usual suspects
for _PATH in [
"c:/Program Files/FreeCAD0.12/bin",
"c:/Program Files/FreeCAD0.13/bin",
"c:/Program Files/FreeCAD0.14/bin",
"c:/Program Files/FreeCAD0.15/bin",
"c:/Program Files/FreeCAD0.16/bin",
"c:/Program Files/FreeCAD0.17/bin",
"c:/Program Files (x86)/FreeCAD0.12/bin",
"c:/Program Files (x86)/FreeCAD0.13/bin",
"c:/Program Files (x86)/FreeCAD0.14/bin",
"c:/Program Files (x86)/FreeCAD0.15/bin",
"c:/Program Files (x86)/FreeCAD0.16/bin",
"c:/Program Files (x86)/FreeCAD0.17/bin",
"c:/apps/FreeCAD0.12/bin",
"c:/apps/FreeCAD0.13/bin",
"c:/apps/FreeCAD0.14/bin",
"c:/apps/FreeCAD0.15/bin",
"c:/apps/FreeCAD0.16/bin",
"c:/apps/FreeCAD0.17/bin",
"c:/Program Files/FreeCAD 0.12/bin",
"c:/Program Files/FreeCAD 0.13/bin",
"c:/Program Files/FreeCAD 0.14/bin",
"c:/Program Files/FreeCAD 0.15/bin",
"c:/Program Files/FreeCAD 0.16/bin",
"c:/Program Files/FreeCAD 0.17/bin",
"c:/Program Files (x86)/FreeCAD 0.12/bin",
"c:/Program Files (x86)/FreeCAD 0.13/bin",
"c:/Program Files (x86)/FreeCAD 0.14/bin",
"c:/Program Files (x86)/FreeCAD 0.15/bin",
"c:/Program Files (x86)/FreeCAD 0.16/bin",
"c:/Program Files (x86)/FreeCAD 0.17/bin",
"c:/apps/FreeCAD 0.12/bin",
"c:/apps/FreeCAD 0.13/bin",
"c:/apps/FreeCAD 0.14/bin",
"c:/apps/FreeCAD 0.15/bin",
"c:/apps/FreeCAD 0.16/bin",
"c:/apps/FreeCAD 0.17/bin",
]:
if os.path.exists(_PATH):
return _PATH
#Make sure that the correct FreeCAD path shows up in Python's system path
sys.path.insert(0, _fc_path())

View File

@ -0,0 +1,419 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
An exporter should provide functionality to accept a shape, and return
a string containing the model content.
"""
import cadquery
#from .verutil import fc_import
#FreeCAD = fc_import("FreeCAD")
import FreeCAD
import tempfile,os,StringIO
import Drawing
#Drawing = fc_import("FreeCAD.Drawing")
#_FCVER = freecad_version()
#if _FCVER>=(0,13):
#import Drawing as FreeCADDrawing #It's in FreeCAD lib path
#elif _FCVER>=(0,12):
#import FreeCAD.Drawing as FreeCADDrawing
#else:
#raise RuntimeError, "Invalid freecad version: %s" % str(".".join(_FCVER))
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
class ExportTypes:
STL = "STL"
STEP = "STEP"
AMF = "AMF"
SVG = "SVG"
TJS = "TJS"
class UNITS:
MM = "mm"
IN = "in"
def toString(shape,exportType,tolerance=0.1):
s= StringIO.StringIO()
exportShape(shape,exportType,s,tolerance)
return s.getvalue()
def exportShape(shape,exportType,fileLike,tolerance=0.1):
"""
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
object, the first value is exported
:param exportFormat: the exportFormat to use
:param tolerance: the tolerance, in model units
:param fileLike: a file like object to which the content will be written.
The object should be already open and ready to write. The caller is responsible
for closing the object
"""
if isinstance(shape,cadquery.CQ):
shape = shape.val()
if exportType == ExportTypes.TJS:
#tessellate the model
tess = shape.tessellate(tolerance)
mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!!
#add vertices
for vec in tess[0]:
mesher.addVertex(vec.x, vec.y, vec.z)
#add faces
for f in tess[1]:
mesher.addTriangleFace(f[0],f[1], f[2])
fileLike.write( mesher.toJson())
elif exportType == ExportTypes.SVG:
fileLike.write(getSVG(shape.wrapped))
elif exportType == ExportTypes.AMF:
tess = shape.tessellate(tolerance)
aw = AmfWriter(tess).writeAmf(fileLike)
else:
#all these types required writing to a file and then
#re-reading. this is due to the fact that FreeCAD writes these
(h, outFileName) = tempfile.mkstemp()
#weird, but we need to close this file. the next step is going to write to
#it from c code, so it needs to be closed.
os.close(h)
if exportType == ExportTypes.STEP:
shape.exportStep(outFileName)
elif exportType == ExportTypes.STL:
shape.wrapped.exportStl(outFileName)
else:
raise ValueError("No idea how i got here")
res = readAndDeleteFile(outFileName)
fileLike.write(res)
def readAndDeleteFile(fileName):
"""
read data from file provided, and delete it when done
return the contents as a string
"""
res = ""
with open(fileName,'r') as f:
res = f.read()
os.remove(fileName)
return res
def guessUnitOfMeasure(shape):
"""
Guess the unit of measure of a shape.
"""
bb = shape.BoundBox
dimList = [ bb.XLength, bb.YLength,bb.ZLength ]
#no real part would likely be bigger than 10 inches on any side
if max(dimList) > 10:
return UNITS.MM
#no real part would likely be smaller than 0.1 mm on all dimensions
if min(dimList) < 0.1:
return UNITS.IN
#no real part would have the sum of its dimensions less than about 5mm
if sum(dimList) < 10:
return UNITS.IN
return UNITS.MM
class AmfWriter(object):
def __init__(self,tessellation):
self.units = "mm"
self.tessellation = tessellation
def writeAmf(self,outFile):
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')
#add vertices
for v in self.tessellation[0]:
vtx = ET.SubElement(vertices,'vertex')
coord = ET.SubElement(vtx,'coordinates')
x = ET.SubElement(coord,'x')
x.text = str(v.x)
y = ET.SubElement(coord,'y')
y.text = str(v.y)
z = ET.SubElement(coord,'z')
z.text = str(v.z)
#add triangles
for t in self.tessellation[1]:
triangle = ET.SubElement(volume,'triangle')
v1 = ET.SubElement(triangle,'v1')
v1.text = str(t[0])
v2 = ET.SubElement(triangle,'v2')
v2.text = str(t[1])
v3 = ET.SubElement(triangle,'v3')
v3.text = str(t[2])
ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
"""
Objects that represent
three.js JSON object notation
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
"""
class JsonMesh(object):
def __init__(self):
self.vertices = [];
self.faces = [];
self.nVertices = 0;
self.nFaces = 0;
def addVertex(self,x,y,z):
self.nVertices += 1;
self.vertices.extend([x,y,z]);
#add triangle composed of the three provided vertex indices
def addTriangleFace(self, i,j,k):
#first position means justa simple triangle
self.nFaces += 1;
self.faces.extend([0,int(i),int(j),int(k)]);
"""
Get a json model from this model.
For now we'll forget about colors, vertex normals, and all that stuff
"""
def toJson(self):
return JSON_TEMPLATE % {
'vertices' : str(self.vertices),
'faces' : str(self.faces),
'nVertices': self.nVertices,
'nFaces' : self.nFaces
};
def getPaths(freeCadSVG):
"""
freeCad svg is worthless-- except for paths, which are fairly useful
this method accepts svg from fReeCAD and returns a list of strings suitable for inclusion in a path element
returns two lists-- one list of visible lines, and one list of hidden lines
HACK ALERT!!!!!
FreeCAD does not give a way to determine which lines are hidden and which are not
the only way to tell is that hidden lines are in a <g> with 0.15 stroke and visible are 0.35 stroke.
so we actually look for that as a way to parse.
to make it worse, elementTree xpath attribute selectors do not work in python 2.6, and we
cannot use python 2.7 due to freecad. So its necessary to look for the pure strings! ick!
"""
hiddenPaths = []
visiblePaths = []
if len(freeCadSVG) > 0:
#yuk, freecad returns svg fragments. stupid stupid
fullDoc = "<root>%s</root>" % freeCadSVG
e = ET.ElementTree(ET.fromstring(fullDoc))
segments = e.findall(".//g")
for s in segments:
paths = s.findall("path")
if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK
mylist = hiddenPaths
else:
mylist = visiblePaths
for p in paths:
mylist.append(p.get("d"))
return (hiddenPaths,visiblePaths)
else:
return ([],[])
def getSVG(shape,opts=None):
"""
Export a shape to SVG
"""
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20}
if opts:
d.update(opts)
#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'])
#TODO: provide option to give 3 views
viewVector = FreeCAD.Base.Vector(-1.75,1.1,5)
(visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector)
(hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented!
#get bounding box -- these are all in 2-d space
bb = visibleG0.BoundBox
bb.add(visibleG1.BoundBox)
bb.add(hiddenG0.BoundBox)
bb.add(hiddenG1.BoundBox)
#width pixels for x, height pixesl for y
unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 )
#compute amount to translate-- move the top left into view
(xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale)
#compute paths ( again -- had to strip out freecad crap )
hiddenContent = ""
for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p
visibleContent = ""
for p in visiblePaths:
visibleContent += PATHTEMPLATE % p
svg = SVG_TEMPLATE % (
{
"unitScale" : str(unitScale),
"strokeWidth" : str(1.0/unitScale),
"hiddenContent" : hiddenContent ,
"visibleContent" :visibleContent,
"xTranslate" : str(xTranslate),
"yTranslate" : str(yTranslate),
"width" : str(width),
"height" : str(height),
"textboxY" :str(height - 30),
"uom" : str(uom)
}
)
#svg = SVG_TEMPLATE % (
# {"content": projectedContent}
#)
return svg
def exportSVG(shape, fileName):
"""
accept a cadquery shape, and export it to the provided file
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
export a view of a part to svg
"""
svg = getSVG(shape.val().wrapped)
f = open(fileName,'w')
f.write(svg)
f.close()
JSON_TEMPLATE= """\
{
"metadata" :
{
"formatVersion" : 3,
"generatedBy" : "ParametricParts",
"vertices" : %(nVertices)d,
"faces" : %(nFaces)d,
"normals" : 0,
"colors" : 0,
"uvs" : 0,
"materials" : 1,
"morphTargets" : 0
},
"scale" : 1.0,
"materials": [ {
"DbgColor" : 15658734,
"DbgIndex" : 0,
"DbgName" : "Material",
"colorAmbient" : [0.0, 0.0, 0.0],
"colorDiffuse" : [0.6400000190734865, 0.10179081114814892, 0.126246120426746],
"colorSpecular" : [0.5, 0.5, 0.5],
"shading" : "Lambert",
"specularCoef" : 50,
"transparency" : 1.0,
"vertexColors" : false
}],
"vertices": %(vertices)s,
"morphTargets": [],
"normals": [],
"colors": [],
"uvs": [[]],
"faces": %(faces)s
}
"""
SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="%(width)s"
height="%(height)s"
>
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
<!-- hidden lines -->
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
%(hiddenContent)s
</g>
<!-- solid lines -->
<g stroke="rgb(0, 0, 0)" fill="none">
%(visibleContent)s
</g>
</g>
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
<text x="80" y="-30" style="stroke:#000000">X </text>
<line x1="30" y1="-30" x2="30" y2="-75" stroke-width="3" stroke="#000000" />
<text x="25" y="-85" style="stroke:#000000">Y </text>
<line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3" stroke="#000000" />
<text x="65" y="-5" style="stroke:#000000">Z </text>
<!--
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
-->
</g>
</svg>
"""
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n"

View File

@ -0,0 +1,589 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""
import math,sys
#import FreeCAD
#from .verutil import fc_import
#FreeCAD = fc_import("FreeCAD")
import FreeCAD
#Turns out we don't need the Part module here.
def sortWiresByBuildOrder(wireList,plane,result=[]):
"""
Tries to determine how wires should be combined into faces.
Assume:
The wires make up one or more faces, which could have 'holes'
Outer wires are listed ahead of inner wires
there are no wires inside wires inside wires ( IE, islands -- we can deal with that later on )
none of the wires are construction wires
Compute:
one or more sets of wires, with the outer wire listed first, and inner ones
Returns, list of lists.
"""
result = []
remainingWires = list(wireList)
while remainingWires:
outerWire = remainingWires.pop(0)
group = [outerWire]
otherWires = list(remainingWires)
for w in otherWires:
if plane.isWireInside(outerWire,w):
group.append(w)
remainingWires.remove(w)
result.append(group)
return result
class Vector(object):
"""
Create a 3-dimensional vector
:param *args: a 3-d vector, with x-y-z parts.
you can either provide:
* a FreeCAD vector
* a vector ( in which case it is copied )
* a 3-tuple
* three float values, x, y, and z
FreeCAD's vector implementation has a dumb
implementation for multiply and add-- they modify the existing
value and return a copy as well.
This vector is immutable-- all mutations return a copy!
"""
def __init__(self,*args):
if len(args) == 3:
fV = FreeCAD.Base.Vector(args[0],args[1],args[2])
elif len(args) == 1:
if type(args[0]) is tuple:
fV = FreeCAD.Base.Vector(args[0][0],args[0][1],args[0][2])
elif type(args[0] is FreeCAD.Base.Vector):
fV = args[0]
elif type(args[0] is Vector):
fV = args[0].wrapped
else:
fV = args[0]
else:
raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
self.wrapped = fV
self.Length = fV.Length
self.x = fV.x
self.y = fV.y
self.z = fV.z
def toTuple(self):
return (self.x,self.y,self.z)
#TODO: is it possible to create a dynamic proxy without all this code?
def cross(self,v):
return Vector( self.wrapped.cross(v.wrapped))
def dot(self,v):
return self.wrapped.dot(v.wrapped)
def sub(self,v):
return self.wrapped.sub(v.wrapped)
def add(self,v):
return Vector( self.wrapped.add(v.wrapped))
def multiply(self,scale):
"""
Return self multiplied by the provided scalar
Note: FreeCAD has a bug here, where the
base is also modified
"""
tmp = FreeCAD.Base.Vector(self.wrapped)
return Vector( tmp.multiply(scale))
def normalize(self):
"""
Return normalized version this vector.
Note: FreeCAD has a bug here, where the
base is also modified
"""
tmp = FreeCAD.Base.Vector(self.wrapped)
tmp.normalize()
return Vector( tmp )
def Center(self):
"""
The center of myself is myself.
Provided so that vectors, vertexes, and other shapes all support a common interface,
when Center() is requested for all objects on the stack
"""
return self
def getAngle(self,v):
return self.wrapped.getAngle(v.wrapped)
def distanceToLine(self):
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!")
def distanceToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
def projectToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
def __hash__(self):
return self.wrapped.__hash__()
def __add__(self,v):
return self.add(v)
def __len__(self):
return self.Length
def __repr__(self):
return self.wrapped.__repr__()
def __str__(self):
return self.wrapped.__str__()
def __len__(self,other):
return self.wrapped.__len__(other)
def __lt__(self,other):
return self.wrapped.__lt__(other)
def __gt__(self,other):
return self.wrapped.__gt__(other)
def __ne__(self,other):
return self.wrapped.__ne__(other)
def __le__(self,other):
return self.wrapped.__le__(other)
def __ge__(self,other):
return self.wrapped.__ge__(other)
def __eq__(self,other):
return self.wrapped.__eq__(other)
class Matrix:
"""
A 3d , 4x4 transformation matrix.
Used to move geometry in space.
"""
def __init__(self,matrix=None):
if matrix == None:
self.wrapped = FreeCAD.Base.Matrix()
else:
self.wrapped = matrix
def rotateX(self,angle):
self.wrapped.rotateX(angle)
def rotateY(self,angle):
self.wrapped.rotateY(angle)
class Plane:
"""
A 2d coordinate system in space, with the x-y axes on the a plane, and a particular point as the origin.
A plane allows the use of 2-d coordinates, which are later converted to global, 3d coordinates when
the operations are complete.
Frequently, it is not necessary to create work planes, as they can be created automatically from faces.
"""
@classmethod
def named(cls,stdName,origin=(0,0,0)):
"""
Create a predefined Plane based on the conventional names.
:param stdName: one of (XY|YZ|XZ|front|back|left|right|top|bottom
:type stdName: string
:param origin: the desired origin, specified in global coordinates
:type origin: 3-tuple of the origin of the new plane, in global coorindates.
Available named planes are as follows. Direction references refer to the global
directions
=========== ======= ======= ======
Name xDir yDir zDir
=========== ======= ======= ======
XY +x +y +z
YZ +y +z +x
XZ +x +z -y
front +x +y +z
back -x +y -z
left +z +y -x
right -z +y +x
top +x -z +y
bottom +x +z -y
=========== ======= ======= ======
"""
namedPlanes = {
#origin, xDir, normal
'XY' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
'YZ' : Plane(Vector(origin),Vector((0,1,0)),Vector((1,0,0))),
'XZ' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0))),
'front': Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
'back': Plane(Vector(origin),Vector((-1,0,0)),Vector((0,0,-1))),
'left': Plane(Vector(origin),Vector((0,0,1)),Vector((-1,0,0))),
'right': Plane(Vector(origin),Vector((0,0,-1)),Vector((1,0,0))),
'top': Plane(Vector(origin),Vector((1,0,0)),Vector((0,1,0))),
'bottom': Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0)))
}
if namedPlanes.has_key(stdName):
return namedPlanes[stdName]
else:
raise ValueError("Supported names are %s " % str(namedPlanes.keys()) )
@classmethod
def XY(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('XY',origin)
@classmethod
def YZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('YZ',origin)
@classmethod
def XZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('XZ',origin)
@classmethod
def front(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('front',origin)
@classmethod
def back(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('back',origin)
@classmethod
def left(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('left',origin)
@classmethod
def right(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('right',origin)
@classmethod
def top(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('top',origin)
@classmethod
def bottom(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
return Plane.named('bottom',origin)
def __init__(self, origin, xDir, normal ):
"""
Create a Plane with an arbitrary orientation
TODO: project x and y vectors so they work even if not orthogonal
:param origin: the origin
:type origin: a three-tuple of the origin, in global coordinates
:param xDir: a vector representing the xDirection.
:type xDir: a three-tuple representing a vector, or a FreeCAD Vector
:param normal: the normal direction for the new plane
:type normal: a FreeCAD Vector
:raises: ValueError if the specified xDir is not orthogonal to the provided normal.
:return: a plane in the global space, with the xDirection of the plane in the specified direction.
"""
self.xDir = xDir.normalize()
self.yDir = normal.cross(self.xDir).normalize()
self.zDir = normal.normalize()
#stupid freeCAD!!!!! multiply has a bug that changes the original also!
self.invZDir = self.zDir.multiply(-1.0)
self.setOrigin3d(origin)
def setOrigin3d(self,originVector):
"""
Move the origin of the plane, leaving its orientation and xDirection unchanged.
:param originVector: the new center of the plane, *global* coordinates
:type originVector: a FreeCAD Vector.
:return: void
"""
self.origin = originVector
self._calcTransforms()
def setOrigin2d(self,x,y):
"""
Set a new origin based of the plane. The plane's orientation and xDrection are unaffected.
:param float x: offset in the x direction
:param float y: offset in the y direction
:return: void
the new coordinates are specified in terms of the current 2-d system. As an example::
p = Plane.XY()
p.setOrigin2d(2,2)
p.setOrigin2d(2,2)
results in a plane with its origin at (x,y)=(4,4) in global coordinates. The both operations were relative to
local coordinates of the plane.
"""
self.setOrigin3d(self.toWorldCoords((x,y)))
def isWireInside(self,baseWire,testWire):
"""
Determine if testWire is inside baseWire, after both wires are projected into the current plane
:param baseWire: a reference wire
:type baseWire: a FreeCAD wire
:param testWire: another wire
:type testWire: a FreeCAD wire
:return: True if testWire is inside baseWire, otherwise False
If either wire does not lie in the current plane, it is projected into the plane first.
*WARNING*: This method is not 100% reliable. It uses bounding box tests, but needs
more work to check for cases when curves are complex.
Future Enhancements:
* Discretizing points along each curve to provide a more reliable test
"""
#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
#coordinates upon extrusion?
tBaseWire = baseWire.transformGeometry(self.fG)
tTestWire = testWire.transformGeometry(self.fG)
#these bounding boxes will have z=0, since we transformed them into the space of the plane
bb = tBaseWire.BoundingBox()
tb = tTestWire.BoundingBox()
#findOutsideBox actually inspects both ways, here we only want to
#know if one is inside the other
x = BoundBox.findOutsideBox2D(bb,tb)
return x == bb
def toLocalCoords(self,obj):
"""
Project the provided coordinates onto this plane.
:param obj: an object or vector to convert
:type vector: a vector or shape
:return: an object of the same type as the input, but converted to local coordinates
Most of the time, the z-coordinate returned will be zero, because most operations
based on a plane are all 2-d. Occasionally, though, 3-d points outside of the current plane are transformed.
One such example is :py:meth:`Workplane.box`, where 3-d corners of a box are transformed to orient the box in space
correctly.
"""
if isinstance(obj,Vector):
return Vector(self.fG.multiply(obj.wrapped))
elif isinstance(obj,Shape):
return obj.transformShape(self.rG)
else:
raise ValueError("Dont know how to convert type %s to local coordinates" % str(type(obj)))
def toWorldCoords(self, tuplePoint):
"""
Convert a point in local coordinates to global coordinates.
:param tuplePoint: point in local coordinates to convert
:type tuplePoint: a 2 or three tuple of float. the third value is taken to be zero if not supplied
:return: a Vector in global coordinates
"""
if len(tuplePoint) == 2:
v = Vector(tuplePoint[0], tuplePoint[1], 0)
else:
v = Vector(tuplePoint[0],tuplePoint[1],tuplePoint[2])
return Vector(self.rG.multiply(v.wrapped))
def rotated(self,rotate=(0,0,0)):
"""
returns a copy of this plane, rotated about the specified axes, as measured from horizontal
Since the z axis is always normal the plane, rotating around Z will always produce a plane
that is parallel to this one
the origin of the workplane is unaffected by the rotation.
rotations are done in order x,y,z. if you need a different order, manually chain together multiple .rotate()
commands
:param rotate: Vector [xDegrees,yDegrees,zDegrees]
:return: a copy of this plane rotated as requested
"""
if rotate.__class__.__name__ != 'Vector':
rotate = Vector(rotate)
#convert to radians
rotate = rotate.multiply(math.pi / 180.0 )
#compute rotation matrix
m = FreeCAD.Base.Matrix()
m.rotateX(rotate.x)
m.rotateY(rotate.y)
m.rotateZ(rotate.z)
#compute the new plane
newXdir = Vector(m.multiply(self.xDir.wrapped))
newZdir = Vector(m.multiply(self.zDir.wrapped))
newP= Plane(self.origin,newXdir,newZdir)
return newP
def rotateShapes(self,listOfShapes,rotationMatrix):
"""
rotate the listOfShapes by the rotationMatrix supplied.
@param listOfShapes is a list of shape objects
@param rotationMatrix is a geom.Matrix object.
returns a list of shape objects rotated according to the rotationMatrix
"""
#compute rotation matrix ( global --> local --> rotate --> global )
#rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
rm = self.computeTransform(rotationMatrix)
#There might be a better way, but to do this rotation takes 3 steps
#transform geometry to local coordinates
#then rotate about x
#then transform back to global coordiante
resultWires = []
for w in listOfShapes:
mirrored = w.transformGeometry(rotationMatrix.wrapped)
resultWires.append(mirrored)
return resultWires
def _calcTransforms(self):
"""
Computes transformation martrices to convert betwene local and global coordinates
"""
#r is the forward transformation matrix from world to local coordinates
#ok i will be really honest-- i cannot understand exactly why this works
#something bout the order of the transaltion and the rotation.
# the double-inverting is strange, and i dont understand it.
r = FreeCAD.Base.Matrix()
#forward transform must rotate and adjust for origin
(r.A11, r.A12, r.A13 ) = (self.xDir.x, self.xDir.y, self.xDir.z )
(r.A21, r.A22, r.A23 ) = (self.yDir.x, self.yDir.y, self.yDir.z )
(r.A31, r.A32, r.A33 ) = (self.zDir.x, self.zDir.y, self.zDir.z )
invR = r.inverse()
(invR.A14,invR.A24,invR.A34) = (self.origin.x,self.origin.y,self.origin.z)
( self.rG,self.fG ) = ( invR,invR.inverse() )
def computeTransform(self,tMatrix):
"""
Computes the 2-d projection of the supplied matrix
"""
rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG)
return Matrix(rm)
class BoundBox(object):
"A BoundingBox for an object or set of objects. Wraps the FreeCAD one"
def __init__(self,bb):
self.wrapped = bb
self.xmin = bb.XMin
self.xmax = bb.XMax
self.xlen = bb.XLength
self.ymin = bb.YMin
self.ymax = bb.YMax
self.ylen = bb.YLength
self.zmin = bb.ZMin
self.zmax = bb.ZMax
self.zlen = bb.ZLength
self.center = Vector(bb.Center)
self.DiagonalLength = bb.DiagonalLength
def add(self,obj):
"""
returns a modified (expanded) bounding box
obj can be one of several things:
1. a 3-tuple corresponding to x,y, and z amounts to add
2. a vector, containing the x,y,z values to add
3. another bounding box, where a new box will be created that encloses both
this bounding box is not changed
"""
tmp = FreeCAD.Base.BoundBox(self.wrapped)
if type(obj) is tuple:
tmp.add(obj[0],obj[1],obj[2])
elif type(obj) is Vector:
tmp.add(obj.fV)
elif type(obj) is BoundBox:
tmp.add(obj.wrapped)
return BoundBox(tmp)
@classmethod
def findOutsideBox2D(cls,b1, b2):
"""
compares bounding boxes. returns none if neither is inside the other. returns
the outer one if either is outside the other
BoundBox.isInside works in 3d, but this is a 2d bounding box, so it doesnt work correctly
plus, there was all kinds of rounding error in the built-in implementation i do not understand.
Here we assume that the b
"""
bb1 = b1.wrapped
bb2 = b2.wrapped
if bb1.XMin < bb2.XMin and\
bb1.XMax > bb2.XMax and\
bb1.YMin < bb2.YMin and\
bb1.YMax > bb2.YMax:
return b1
if bb2.XMin < bb1.XMin and\
bb2.XMax > bb1.XMax and\
bb2.YMin < bb1.YMin and\
bb2.YMax > bb1.YMax:
return b2
return None
def isInside(self,anotherBox):
"""
is the provided bounding box inside this one?
"""
return self.wrapped.isInside(anotherBox.wrapped)

View File

@ -0,0 +1,64 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
An exporter should provide functionality to accept a shape, and return
a string containing the model content.
"""
import cadquery
from .shapes import Shape
#from .verutil import fc_import
# FreeCAD = fc_import("FreeCAD")
# Part = fc_import("FreeCAD.Part")
import FreeCAD
import Part
class ImportTypes:
STEP = "STEP"
class UNITS:
MM = "mm"
IN = "in"
def importShape(importType,fileName):
"""
Imports a file based on the type (STEP, STL, etc)
:param importType: The type of file that we're importing
:param fileName: THe name of the file that we're importing
"""
#Check to see what type of file we're working with
if importType == ImportTypes.STEP:
return importStep(fileName)
#Loads a STEP file into a CQ object
def importStep(fileName):
"""
Accepts a file name and loads the STEP file into a cadquery shape
:param fileName: The path and name of the STEP file to be imported
"""
#Now read and return the shape
try:
rshape = Part.read(fileName)
r = Shape.cast(rshape)
#print "loadStep: " + str(r)
#print "Faces=%d" % cadquery.CQ(r).solids().size()
return cadquery.CQ(r)
except:
raise ValueError("STEP File Could not be loaded")

View File

@ -0,0 +1,859 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
Wrapper Classes for FreeCAD
These classes provide a stable interface for 3d objects,
independent of the FreeCAD interface.
Future work might include use of pythonOCC, OCC, or even
another CAD kernel directly, so this interface layer is quite important.
Funny, in java this is one of those few areas where i'd actually spend the time
to make an interface and an implementation, but for new these are just rolled together
This interface layer provides three distinct values:
1. It allows us to avoid changing key api points if we change underlying implementations.
It would be a disaster if script and plugin authors had to change models because we
changed implmentations
2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
its docs are terrible. This allows us to provie good documentation via docstrings
for each wrapper
3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them
4. allows for enhanced functionality. Many objects are missing features we need. For example
we need a 'forConstruciton' flag on the Wire object. this allows adding those kinds of things
5. allow changing interfaces when we'd like. there are few cases where the freecad api is not
very userfriendly: we like to change those when necesary. As an example, in the freecad api,
all factory methods are on the 'Part' object, but it is very useful to know what kind of
object each one returns, so these are better grouped by the type of object they return.
(who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ?
"""
from cadquery import Vector, BoundBox
import FreeCAD
#from .verutil import fc_import
#FreeCADPart = fc_import("FreeCAD.Part")
import Part as FreeCADPart
class Shape(object):
"""
Represents a shape in the system.
Wrappers the FreeCAD api
"""
def __init__(self, obj):
self.wrapped = obj
self.forConstruction = False
@classmethod
def cast(cls, obj, forConstruction=False):
"Returns the right type of wrapper, given a FreeCAD object"
s = obj.ShapeType
if type(obj) == FreeCAD.Base.Vector:
return Vector(obj)
tr = None
# TODO: there is a clever way to do this i'm sure with a lookup
# but it is not a perfect mapping, because we are trying to hide
# a bit of the complexity of Compounds in FreeCAD.
if s == 'Vertex':
tr = Vertex(obj)
elif s == 'Edge':
tr = Edge(obj)
elif s == 'Wire':
tr = Wire(obj)
elif s == 'Face':
tr = Face(obj)
elif s == 'Shell':
tr = Shell(obj)
elif s == 'Solid':
tr = Solid(obj)
elif s == 'Compound':
#compound of solids, lets return a solid instead
if len(obj.Solids) > 1:
tr = Solid(obj)
elif len(obj.Solids) == 1:
tr = Solid(obj.Solids[0])
elif len(obj.Wires) > 0:
tr = Wire(obj)
else:
tr = Compound(obj)
else:
raise ValueError("cast:unknown shape type %s" % s)
tr.forConstruction = forConstruction
return tr
# TODO: all these should move into the exporters folder.
# we dont need a bunch of exporting code stored in here!
#
def exportStl(self, fileName):
self.wrapped.exportStl(fileName)
def exportStep(self, fileName):
self.wrapped.exportStep(fileName)
def exportShape(self, fileName, fileFormat):
if fileFormat == ExportFormats.STL:
self.wrapped.exportStl(fileName)
elif fileFormat == ExportFormats.BREP:
self.wrapped.exportBrep(fileName)
elif fileFormat == ExportFormats.STEP:
self.wrapped.exportStep(fileName)
elif fileFormat == ExportFormats.AMF:
# not built into FreeCAD
#TODO: user selected tolerance
tess = self.wrapped.tessellate(0.1)
aw = amfUtils.AMFWriter(tess)
aw.writeAmf(fileName)
elif fileFormat == ExportFormats.IGES:
self.wrapped.exportIges(fileName)
else:
raise ValueError("Unknown export format: %s" % format)
def geomType(self):
"""
Gets the underlying geometry type
:return: a string according to the geometry type.
Implementations can return any values desired, but the
values the user uses in type filters should correspond to these.
As an example, if a user does::
CQ(object).faces("%mytype")
The expectation is that the geomType attribute will return 'mytype'
The return values depend on the type of the shape:
Vertex: always 'Vertex'
Edge: LINE, ARC, CIRCLE, SPLINE
Face: PLANE, SPHERE, CONE
Solid: 'Solid'
Shell: 'Shell'
Compound: 'Compound'
Wire: 'Wire'
"""
return self.wrapped.ShapeType
def isType(self, obj, strType):
"""
Returns True if the shape is the specified type, false otherwise
contrast with ShapeType, which will raise an exception
if the provide object is not a shape at all
"""
if hasattr(obj, 'ShapeType'):
return obj.ShapeType == strType
else:
return False
def hashCode(self):
return self.wrapped.hashCode()
def isNull(self):
return self.wrapped.isNull()
def isSame(self, other):
return self.wrapped.isSame(other.wrapped)
def isEqual(self, other):
return self.wrapped.isEqual(other.wrapped)
def isValid(self):
return self.wrapped.isValid()
def BoundingBox(self):
return BoundBox(self.wrapped.BoundBox)
def Center(self):
try:
return Vector(self.wrapped.CenterOfMass)
except:
pass
def Closed(self):
return self.wrapped.Closed
def ShapeType(self):
return self.wrapped.ShapeType
def Vertices(self):
return [Vertex(i) for i in self.wrapped.Vertexes]
def Edges(self):
return [Edge(i) for i in self.wrapped.Edges]
def Compounds(self):
return [Compound(i) for i in self.wrapped.Compounds]
def Wires(self):
return [Wire(i) for i in self.wrapped.Wires]
def Faces(self):
return [Face(i) for i in self.wrapped.Faces]
def Shells(self):
return [Shell(i) for i in self.wrapped.Shells]
def Solids(self):
return [Solid(i) for i in self.wrapped.Solids]
def Area(self):
return self.wrapped.Area
def Length(self):
return self.wrapped.Length
def rotate(self, startVector, endVector, angleDegrees):
"""
Rotates a shape around an axis
:param startVector: start point of rotation axis either a 3-tuple or a Vector
:param endVector: end point of rotation axis, either a 3-tuple or a Vector
:param angleDegrees: angle to rotate, in degrees
:return: a copy of the shape, rotated
"""
if type(startVector) == tuple:
startVector = Vector(startVector)
if type(endVector) == tuple:
endVector = Vector(endVector)
tmp = self.wrapped.copy()
tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees)
return Shape.cast(tmp)
def translate(self, vector):
if type(vector) == tuple:
vector = Vector(vector)
tmp = self.wrapped.copy()
tmp.translate(vector.wrapped)
return Shape.cast(tmp)
def scale(self, factor):
tmp = self.wrapped.copy()
tmp.scale(factor)
return Shape.cast(tmp)
def copy(self):
return Shape.cast(self.wrapped.copy())
def transformShape(self, tMatrix):
"""
tMatrix is a matrix object.
returns a copy of the ojbect, transformed by the provided matrix,
with all objects keeping their type
"""
tmp = self.wrapped.copy()
tmp.transformShape(tMatrix)
r = Shape.cast(tmp)
r.forConstruction = self.forConstruction
return r
def transformGeometry(self, tMatrix):
"""
tMatrix is a matrix object.
returns a copy of the object, but with geometry transformed insetad of just
rotated.
WARNING: transformGeometry will sometimes convert lines and circles to splines,
but it also has the ability to handle skew and stretching transformations.
If your transformation is only translation and rotation, it is safer to use transformShape,
which doesnt change the underlying type of the geometry, but cannot handle skew transformations
"""
tmp = self.wrapped.copy()
tmp = tmp.transformGeometry(tMatrix)
return Shape.cast(tmp)
def __hash__(self):
return self.wrapped.hashCode()
class Vertex(Shape):
def __init__(self, obj, forConstruction=False):
"""
Create a vertex from a FreeCAD Vertex
"""
self.wrapped = obj
self.forConstruction = forConstruction
self.X = obj.X
self.Y = obj.Y
self.Z = obj.Z
def toTuple(self):
return (self.X, self.Y, self.Z)
def Center(self):
"""
The center of a vertex is itself!
"""
return Vector(self.wrapped.Point)
class Edge(Shape):
def __init__(self, obj):
"""
An Edge
"""
self.wrapped = obj
# self.startPoint = None
# self.endPoint = None
self.edgetypes = {
FreeCADPart.Line: 'LINE',
FreeCADPart.ArcOfCircle: 'ARC',
FreeCADPart.Circle: 'CIRCLE'
}
def geomType(self):
t = type(self.wrapped.Curve)
if self.edgetypes.has_key(t):
return self.edgetypes[t]
else:
return "Unknown Edge Curve Type: %s" % str(t)
def startPoint(self):
"""
:return: a vector representing the start poing of this edge
Note, circles may have the start and end points the same
"""
# work around freecad bug where valueAt is unreliable
curve = self.wrapped.Curve
return Vector(curve.value(self.wrapped.ParameterRange[0]))
def endPoint(self):
"""
:return: a vector representing the end point of this edge.
Note, circles may have the start and end points the same
"""
# warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.ParameterRange[1]) has
# a bug with curves other than arcs, but using the underlying curve directly seems to work
# that's the solution i'm using below
curve = self.wrapped.Curve
v = Vector(curve.value(self.wrapped.ParameterRange[1]))
return v
def tangentAt(self, locationVector=None):
"""
Compute tangent vector at the specified location.
:param locationVector: location to use. Use the center point if None
:return: tangent vector
"""
if locationVector is None:
locationVector = self.Center()
p = self.wrapped.Curve.parameter(locationVector.wrapped)
return Vector(self.wrapped.tangentAt(p))
@classmethod
def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360):
return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2))
@classmethod
def makeSpline(cls, listOfVector):
"""
Interpolate a spline through the provided points.
:param cls:
:param listOfVector: a list of Vectors that represent the points
:return: an Edge
"""
vecs = [v.wrapped for v in listOfVector]
spline = FreeCADPart.BSplineCurve()
spline.interpolate(vecs, False)
return Edge(spline.toShape())
@classmethod
def makeThreePointArc(cls, v1, v2, v3):
"""
Makes a three point arc through the provided points
:param cls:
:param v1: start vector
:param v2: middle vector
:param v3: end vector
:return: an edge object through the three points
"""
arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped)
e = Edge(arc.toShape())
return e # arcane and undocumented, this creates an Edge object
@classmethod
def makeLine(cls, v1, v2):
"""
Create a line between two points
:param v1: Vector that represents the first point
:param v2: Vector that represents the second point
:return: A linear edge between the two provided points
"""
return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple()))
class Wire(Shape):
def __init__(self, obj):
"""
A Wire
"""
self.wrapped = obj
@classmethod
def combine(cls, listOfWires):
"""
Attempt to combine a list of wires into a new wire.
the wires are returned in a list.
:param cls:
:param listOfWires:
:return:
"""
return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires]))
@classmethod
def assembleEdges(cls, listOfEdges):
"""
Attempts to build a wire that consists of the edges in the provided list
:param cls:
:param listOfEdges: a list of Edge objects
:return: a wire with the edges assembled
"""
fCEdges = [a.wrapped for a in listOfEdges]
wa = Wire(FreeCADPart.Wire(fCEdges))
return wa
@classmethod
def makeCircle(cls, radius, center, normal):
"""
Makes a Circle centered at the provided point, having normal in the provided direction
:param radius: floating point radius of the circle, must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:return:
"""
w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)]))
return w
@classmethod
def makePolygon(cls, listOfVertices, forConstruction=False):
# convert list of tuples into Vectors.
w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices]))
w.forConstruction = forConstruction
return w
@classmethod
def makeHelix(cls, pitch, height, radius, angle=360.0):
"""
Make a helix with a given pitch, height and radius
By default a cylindrical surface is used to create the helix. If
the fourth parameter is set (the apex given in degree) a conical surface is used instead'
"""
return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle))
class Face(Shape):
def __init__(self, obj):
"""
A Face
"""
self.wrapped = obj
self.facetypes = {
# TODO: bezier,bspline etc
FreeCADPart.Plane: 'PLANE',
FreeCADPart.Sphere: 'SPHERE',
FreeCADPart.Cone: 'CONE'
}
def geomType(self):
t = type(self.wrapped.Surface)
if self.facetypes.has_key(t):
return self.facetypes[t]
else:
return "Unknown Face Surface Type: %s" % str(t)
def normalAt(self, locationVector=None):
"""
Computes the normal vector at the desired location on the face.
:returns: a vector representing the direction
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
:type locationVector: a vector that lies on the surface.
"""
if locationVector == None:
locationVector = self.Center()
(u, v) = self.wrapped.Surface.parameter(locationVector.wrapped)
return Vector(self.wrapped.normalAt(u, v).normalize())
@classmethod
def makePlane(cls, length, width, basePnt=None, dir=None):
return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir)))
@classmethod
def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
"""
'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
Create a ruled surface out of two edges or wires. If wires are used then
these must have the same
"""
return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist))
def cut(self, faceToCut):
"Remove a face from another one"
return Shape.cast(self.obj.cut(faceToCut.obj))
def fuse(self, faceToJoin):
return Shape.cast(self.obj.fuse(faceToJoin.obj))
def intersect(self, faceToIntersect):
"""
computes the intersection between the face and the supplied one.
The result could be a face or a compound of faces
"""
return Shape.cast(self.obj.common(faceToIntersect.obj))
class Shell(Shape):
def __init__(self, wrapped):
"""
A Shell
"""
self.wrapped = wrapped
@classmethod
def makeShell(cls, listOfFaces):
return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
class Solid(Shape):
def __init__(self, obj):
"""
A Solid
"""
self.wrapped = obj
@classmethod
def isSolid(cls, obj):
"""
Returns true if the object is a FreeCAD solid, false otherwise
"""
if hasattr(obj, 'ShapeType'):
if obj.ShapeType == 'Solid' or \
(obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
return True
return False
@classmethod
def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)):
"""
makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d
imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
"""
return Shape.cast(FreeCADPart.makeBox(length, width, height, pnt.wrapped, dir.wrapped))
@classmethod
def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
"""
'makeCone(radius1,radius2,height,[pnt,dir,angle]) --
Make a cone with given radii and height\nBy default pnt=Vector(0,0,0),
dir=Vector(0,0,1) and angle=360'
"""
return Shape.cast(FreeCADPart.makeCone(radius1, radius2, height, pnt.wrapped, dir.wrapped, angleDegrees))
@classmethod
def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
"""
makeCylinder(radius,height,[pnt,dir,angle]) --
Make a cylinder with a given radius and height
By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360'
"""
return Shape.cast(FreeCADPart.makeCylinder(radius, height, pnt.wrapped, dir.wrapped, angleDegrees))
@classmethod
def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None):
"""
makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
Make a torus with agiven radii and angles
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
,angle1=360 and angle=360'
"""
return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2))
@classmethod
def sweep(cls, profileWire, pathWire):
"""
make a solid by sweeping the profileWire along the specified path
:param cls:
:param profileWire:
:param pathWire:
:return:
"""
# needs to use freecad wire.makePipe or makePipeShell
# needs to allow free-space wires ( those not made from a workplane )
@classmethod
def makeLoft(cls, listOfWire):
"""
makes a loft from a list of wires
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
wants to make an infinitely thin shell for a real FreeCADPart.
"""
# the True flag requests building a solid instead of a shell.
return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True))
@classmethod
def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
"""
'makeWedge(xmin, ymin, zmin, z2min, x2min,
xmax, ymax, zmax, z2max, x2max,[pnt, dir])
Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec
tor(0,0,1)'
"""
return Shape.cast(
FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir))
@classmethod
def makeSphere(cls, radius, pnt=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
"""
'makeSphere(radius,[pnt, dir, angle1,angle2,angle3]) --
Make a sphere with a giv
en radius\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360'
"""
return Solid(FreeCADPart.makeSphere(radius, pnt, angleDegrees1, angleDegrees2, angleDegrees3))
@classmethod
def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees):
"""
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
construction methods used here are different enough that they should be separate.
At a high level, the steps followed ar:
(1) accept a set of wires
(2) create another set of wires like this one, but which are transformed and rotated
(3) create a ruledSurface between the sets of wires
(40 create a shell and compute the resulting object
:param outerWire: the outermost wire, a cad.Wire
:param innerWires: a list of inner wires, a list of cad.Wire
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
vecNormal, located at vecCenter. ( a cad.Vector )
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
:param angleDegrees: the angle to rotate through while extruding
:return: a cad.Solid object
"""
# from this point down we are dealing with FreeCAD wires not cad.wires
startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires]
endWires = []
p1 = vecCenter.wrapped
p2 = vecCenter.add(vecNormal).wrapped
# make translated and rotated copy of each wire
for w in startWires:
w2 = w.copy()
w2.translate(vecNormal.wrapped)
w2.rotate(p1, p2, angleDegrees)
endWires.append(w2)
# make a ruled surface for each set of wires
sides = []
for w1, w2 in zip(startWires, endWires):
rs = FreeCADPart.makeRuledSurface(w1, w2)
sides.append(rs)
#make faces for the top and bottom
startFace = FreeCADPart.Face(startWires)
endFace = FreeCADPart.Face(endWires)
#collect all the faces from the sides
faceList = [startFace]
for s in sides:
faceList.extend(s.Faces)
faceList.append(endFace)
shell = FreeCADPart.makeShell(faceList)
solid = FreeCADPart.makeSolid(shell)
return Shape.cast(solid)
@classmethod
def extrudeLinear(cls, outerWire, innerWires, vecNormal):
"""
Attempt to extrude the list of wires into a prismatic solid in the provided direction
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param vecNormal: a vector along which to extrude the wires
:return: a Solid object
The wires must not intersect
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
there are many geometries that are invalid. In general, the following conditions must be met:
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
"""
# one would think that fusing faces into a compound and then extruding would work,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
# but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
#FreeCAD allows this in one operation, but others might not
freeCADWires = [outerWire.wrapped]
for w in innerWires:
freeCADWires.append(w.wrapped)
f = FreeCADPart.Face(freeCADWires)
result = f.extrude(vecNormal.wrapped)
return Shape.cast(result)
@classmethod
def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd):
"""
Attempt to revolve the list of wires into a solid in the provided direction
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param angleDegrees: the angle to revolve through.
:type angleDegrees: float, anything less than 360 degrees will leave the shape open
:param axisStart: the start point of the axis of rotation
:type axisStart: tuple, a two tuple
:param axisEnd: the end point of the axis of rotation
:type axisEnd: tuple, a two tuple
:return: a Solid object
The wires must not intersect
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
* the wire(s) that you're revolving cannot be centered
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
"""
freeCADWires = [outerWire.wrapped]
for w in innerWires:
freeCADWires.append(w.wrapped)
f = FreeCADPart.Face(freeCADWires)
rotateCenter = FreeCAD.Base.Vector(axisStart)
rotateAxis = FreeCAD.Base.Vector(axisEnd)
#Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
rotateAxis = rotateCenter.sub(rotateAxis)
#FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
return Shape.cast(result)
def tessellate(self, tolerance):
return self.wrapped.tessellate(tolerance)
def intersect(self, toIntersect):
"""
computes the intersection between this solid and the supplied one
The result could be a face or a compound of faces
"""
return Shape.cast(self.wrapped.common(toIntersect.wrapped))
def cut(self, solidToCut):
"Remove a solid from another one"
return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
def fuse(self, solidToJoin):
return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped))
def fillet(self, radius, edgeList):
"""
Fillets the specified edges of this solid.
:param radius: float > 0, the radius of the fillet
:param edgeList: a list of Edge objects, which must belong to this solid
:return: Filleted solid
"""
nativeEdges = [e.wrapped for e in edgeList]
return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges))
def shell(self, faceList, thickness, tolerance=0.0001):
"""
make a shelled solid of given by removing the list of faces
:param faceList: list of face objects, which must be part of the solid.
:param thickness: floating point thickness. positive shells outwards, negative shells inwards
:param tolerance: modelling tolerance of the method, default=0.0001
:return: a shelled solid
**WARNING** The underlying FreeCAD implementation can very frequently have problems
with shelling complex geometries!
"""
nativeFaces = [f.wrapped for f in faceList]
return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance))
class Compound(Shape):
def __init__(self, obj):
"""
An Edge
"""
self.wrapped = obj
def Center(self):
# TODO: compute the weighted average instead of the first solid
return self.Solids()[0].Center()
@classmethod
def makeCompound(cls, listOfShapes):
"""
Create a compound out of a list of shapes
"""
solids = [s.wrapped for s in listOfShapes]
c = FreeCADPart.Compound(solids)
return Shape.cast(c)
def fuse(self, toJoin):
return Shape.cast(self.wrapped.fuse(toJoin.wrapped))
def tessellate(self, tolerance):
return self.wrapped.tessellate(tolerance)

View File

@ -0,0 +1,18 @@
"""
CadQuery
Copyright (C) 2014 Parametric Products Intellectual Holdings, LLC
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""

View File

@ -0,0 +1,363 @@
"""
Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""
import re
import math
from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
class Selector(object):
"""
Filters a list of objects
Filters must provide a single method that filters objects.
"""
def filter(self,objectList):
"""
Filter the provided list
:param objectList: list to filter
:type objectList: list of FreeCAD primatives
:return: filtered list
The default implementation returns the original list unfiltered
"""
return objectList
class NearestToPointSelector(Selector):
"""
Selects object nearest the provided point.
If the object is a vertex or point, the distance
is used. For other kinds of shapes, the center of mass
is used to to compute which is closest.
Applicability: All Types of Shapes
Example::
CQ(aCube).vertices(NearestToPointSelector((0,1,0))
returns the vertex of the unit cube closest to the point x=0,y=1,z=0
"""
def __init__(self,pnt ):
self.pnt = pnt
def filter(self,objectList):
def dist(tShape):
return tShape.Center().sub(self.pnt).Length
#if tShape.ShapeType == 'Vertex':
# return tShape.Point.sub(toVector(self.pnt)).Length
#else:
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
return [ min(objectList,key=dist) ]
class BaseDirSelector(Selector):
"""
A selector that handles selection on the basis of a single
direction vector
"""
def __init__(self,vector,tolerance=0.0001 ):
self.direction = vector
self.TOLERANCE = tolerance
def test(self,vec):
"Test a specified vector. Subclasses override to provide other implementations"
return True
def filter(self,objectList):
"""
There are lots of kinds of filters, but
for planes they are always based on the normal of the plane,
and for edges on the tangent vector along the edge
"""
r = []
for o in objectList:
#no really good way to avoid a switch here, edges and faces are simply different!
if type(o) == Face:
# a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
normal = o.normalAt(None)
if self.test(normal):
r.append(o)
elif type(o) == Edge and o.geomType() == 'LINE':
#an edge is parallel to a direction if it is a line, and the line is parallel to the dir
tangent = o.tangentAt(None)
if self.test(tangent):
r.append(o)
return r
class ParallelDirSelector(BaseDirSelector):
"""
Selects objects parallel with the provided direction
Applicability:
Linear Edges
Planar Faces
Use the string syntax shortcut \|(X|Y|Z) if you want to select
based on a cardinal direction.
Example::
CQ(aCube).faces(ParallelDirSelector((0,0,1))
selects faces with a normals in the z direction, and is equivalent to::
CQ(aCube).faces("|Z")
"""
def test(self,vec):
return self.direction.cross(vec).Length < self.TOLERANCE
class DirectionSelector(BaseDirSelector):
"""
Selects objects aligned with the provided direction
Applicability:
Linear Edges
Planar Faces
Use the string syntax shortcut +/-(X|Y|Z) if you want to select
based on a cardinal direction.
Example::
CQ(aCube).faces(DirectionSelector((0,0,1))
selects faces with a normals in the z direction, and is equivalent to::
CQ(aCube).faces("+Z")
"""
def test(self,vec):
return abs(self.direction.getAngle(vec) < self.TOLERANCE)
class PerpendicularDirSelector(BaseDirSelector):
"""
Selects objects perpendicular with the provided direction
Applicability:
Linear Edges
Planar Faces
Use the string syntax shortcut #(X|Y|Z) if you want to select
based on a cardinal direction.
Example::
CQ(aCube).faces(PerpendicularDirSelector((0,0,1))
selects faces with a normals perpendicular to the z direction, and is equivalent to::
CQ(aCube).faces("#Z")
"""
def test(self,vec):
angle = self.direction.getAngle(vec)
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE )
return not r
class TypeSelector(Selector):
"""
Selects objects of the prescribed topological type.
Applicability:
Faces: Plane,Cylinder,Sphere
Edges: Line,Circle,Arc
You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces,
and %(LINE|ARC|CIRCLE) for edges.
For example this::
CQ(aCube).faces ( TypeSelector("PLANE") )
will select 6 faces, and is equivalent to::
CQ(aCube).faces( "%PLANE" )
"""
def __init__(self,typeString):
self.typeString = typeString.upper()
def filter(self,objectList):
r = []
for o in objectList:
if o.geomType() == self.typeString:
r.append(o)
return r
class DirectionMinMaxSelector(Selector):
"""
Selects objects closest or farthest in the specified direction
Used for faces, points, and edges
Applicability:
All object types. for a vertex, its point is used. for all other kinds
of objects, the center of mass of the object is used.
You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to
select based on a cardinal direction.
For example this::
CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True )
Means to select the face having the center of mass farthest in the positive z direction,
and is the same as:
CQ(aCube).faces( ">Z" )
Future Enhancements:
provide a nicer way to select in arbitrary directions. IE, a bit more code could
allow '>(0,0,1)' to work.
"""
def __init__(self,vector,directionMax=True):
self.vector = vector
self.max = max
self.directionMax = directionMax
def filter(self,objectList):
#then sort by distance from origin, along direction specified
def distance(tShape):
return tShape.Center().dot(self.vector)
#if tShape.ShapeType == 'Vertex':
# pnt = tShape.Point
#else:
# pnt = tShape.Center()
#return pnt.dot(self.vector)
if self.directionMax:
return [ max(objectList,key=distance) ]
else:
return [ min(objectList,key=distance) ]
class StringSyntaxSelector(Selector):
"""
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
are also available ( usually with more functionality ) through the creation of full-fledged
selector objects. see :py:class:`Selector` and its subclasses
Filtering works differently depending on the type of object list being filtered.
:param selectorString: A two-part selector string, [selector][axis]
:return: objects that match the specified selector
***Modfiers*** are ``('|','+','-','<','>','%')``
:\|:
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
:#:
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
:+:
positive direction (same as :py:class:`DirectionSelector` )
:-:
negative direction (same as :py:class:`DirectionSelector` )
:>:
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
:<:
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
:%:
curve/surface type (same as :py:class:`TypeSelector`)
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
Selectors are a complex topic: see :ref:`selector_reference` for more information
"""
def __init__(self,selectorString):
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)
}
namedViews = {
'front': ('>','Z' ),
'back': ('<','Z'),
'left':('<', 'X'),
'right': ('>', 'X'),
'top': ('>','Y'),
'bottom': ('<','Y')
}
self.selectorString = selectorString
r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE)
m = r.match(selectorString)
if m != None:
if namedViews.has_key(selectorString):
(a,b) = namedViews[selectorString]
self.mySelector = self._chooseSelector(a,b )
else:
self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1])
else:
raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ")
def _chooseSelector(self,selType,selAxis):
"""Sets up the underlying filters accordingly"""
if selType == "%":
return TypeSelector(selAxis)
#all other types need to select axis as a vector
#get the axis vector first, will throw an except if an unknown axis is used
try:
vec = self.axes[selAxis]
except KeyError:
raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes)))
if selType in (None, "+"):
#use direction filter
return DirectionSelector(vec)
elif selType == '-':
#just use the reverse of the direction vector
return DirectionSelector(vec.multiply(-1.0))
elif selType == "|":
return ParallelDirSelector(vec)
elif selType == ">":
return DirectionMinMaxSelector(vec,True)
elif selType == "<":
return DirectionMinMaxSelector(vec,False)
elif selType == '#':
return PerpendicularDirSelector(vec)
else:
raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ")
def filter(self,objectList):
"""
selects minimum, maximum, positive or negative values relative to a direction
[+\|-\|<\|>\|] \<X\|Y\|Z>
"""
return self.mySelector.filter(objectList)

File diff suppressed because it is too large Load Diff