added export test cases

This commit is contained in:
Dave Cowden 2013-04-21 17:45:29 -04:00
parent 1848b1226a
commit 59d2af7a6c
5 changed files with 164 additions and 74 deletions

View File

@ -1,20 +1,20 @@
""" """
Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery. This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful, CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details. Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public 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/> License along with this library; If not, see <http://www.gnu.org/licenses/>
""" """
import time,math import time,math
@ -834,18 +834,26 @@ class Workplane(CQ):
self.parent = None self.parent = None
self.ctx = CQContext() self.ctx = CQContext()
def transformed(self,rotate=Vector(0,0,0),offset=Vector(0,0,0)): def transformed(self,rotate=(0,0,0),offset=(0,0,0)):
""" """
Create a new workplane based on the current one. Create a new workplane based on the current one.
The origin of the new plane is located at the existing origin+offset vector, where offset is given in The origin of the new plane is located at the existing origin+offset vector, where offset is given in
coordinates local to the current plane coordinates local to the current plane
The new plane is rotated through the angles specified by the components of the rotation vector The new plane is rotated through the angles specified by the components of the rotation vector
:param rotate: vector of angles to rotate, in degrees relative to work plane coordinates :param rotate: 3-tuple of angles to rotate, in degrees relative to work plane coordinates
:param offset: vector to offset the new plane, in local work plane coordinates :param offset: 3-tuple to offset the new plane, in local work plane coordinates
:return: a new work plane, transformed as requested :return: a new work plane, transformed as requested
""" """
#old api accepted a vector, so we'll check for that.
if rotate.__class__.__name__ == 'Vector':
rotate = rotate.toTuple()
if offset.__class__.__name__ == 'Vector':
offset = offset.toTuple()
p = self.plane.rotated(rotate) p = self.plane.rotated(rotate)
p.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() )) p.setOrigin3d(self.plane.toWorldCoords(offset ))
ns = self.newObject([p.origin]) ns = self.newObject([p.origin])
ns.plane = p ns.plane = p
@ -1215,39 +1223,24 @@ class Workplane(CQ):
Future Enhancements: Future Enhancements:
faster implementation: this one transforms 3 times to accomplish the result faster implementation: this one transforms 3 times to accomplish the result
""" """
#compute rotation matrix ( global --> local --> rotate --> global )
#rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
rm = self.plane.computeTransform(matrix)
#convert edges to a wire, if there are pending edges #convert edges to a wire, if there are pending edges
n = self.wire(forConstruction=False) n = self.wire(forConstruction=False)
#attempt to consolidate wires together. #attempt to consolidate wires together.
consolidated = n.consolidateWires() consolidated = n.consolidateWires()
#ok, mirror all the wires. rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(),matrix)
#There might be a better way, but to do this rotation takes 3 steps for w in rotatedWires:
#transform geometry to local coordinates consolidated.objects.append(w)
#then rotate about x consolidated._addPendingWire(w)
#then transform back to global coordiante
#!!!TODO: needs refactoring. rm.wrapped is a hack, w.transformGeometry is needing a FreeCAD matrix,
#so this code is dependent on a freecad matrix even when we dont explicitly import it.
#
originalWires = consolidated.wires().vals()
for w in originalWires:
mirrored = w.transformGeometry(rm.wrapped)
consolidated.objects.append(mirrored)
consolidated._addPendingWire(mirrored)
#attempt again to consolidate all of the wires #attempt again to consolidate all of the wires
c = consolidated.consolidateWires() c = consolidated.consolidateWires()
#c = consolidated
return c return c
def mirrorY(self): def mirrorY(self):
@ -2174,4 +2167,4 @@ class Workplane(CQ):
else: else:
#combine everything #combine everything
return self.union(boxes) return self.union(boxes)

View File

@ -1,23 +1,27 @@
""" """
Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery. This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful, CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details. Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public 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/> 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
import FreeCAD import FreeCAD,tempfile,os
from FreeCAD import Drawing from FreeCAD import Drawing
try: try:
@ -25,17 +29,82 @@ try:
except ImportError: except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
class ExportFormats: class ExportTypes:
STL = "STL" STL = "STL"
BREP = "BREP"
STEP = "STEP" STEP = "STEP"
AMF = "AMF" AMF = "AMF"
IGES = "IGES" SVG = "SVG"
TJS = "TJS"
class UNITS: class UNITS:
MM = "mm" MM = "mm"
IN = "in" IN = "in"
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): def guessUnitOfMeasure(shape):
""" """
Guess the unit of measure of a shape. Guess the unit of measure of a shape.
@ -55,16 +124,16 @@ def guessUnitOfMeasure(shape):
if sum(dimList) < 10: if sum(dimList) < 10:
return UNITS.IN return UNITS.IN
return UNITS.MM return UNITS.MM
class AmfWriter(object):
class AmfExporter(object):
def __init__(self,tessellation): def __init__(self,tessellation):
self.units = "mm" self.units = "mm"
self.tessellation = tessellation self.tessellation = tessellation
def writeAmf(self,outFileName): 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 #TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf,'object',id="0") object = ET.SubElement(amf,'object',id="0")
@ -94,14 +163,14 @@ class AmfExporter(object):
v3.text = str(t[2]) v3.text = str(t[2])
ET.ElementTree(amf).write(outFileName,encoding='ISO-8859-1') ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
""" """
Objects that represent Objects that represent
three.js JSON object notation three.js JSON object notation
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0 https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
""" """
class JsonExporter(object): class JsonMesh(object):
def __init__(self): def __init__(self):
self.vertices = []; self.vertices = [];
@ -174,7 +243,7 @@ def getSVG(shape,opts=None):
""" """
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20} d = {'width':800,'height':240,'marginLeft':200,'marginTop':20}
if opts: if opts:
d.update(opts) d.update(opts)
@ -235,11 +304,11 @@ def getSVG(shape,opts=None):
def exportSVG(shape, fileName): def exportSVG(shape, fileName):
""" """
accept a cadquery shape, and export it to the provided file 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 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 export a view of a part to svg
""" """
svg = getSVG(shape.val().wrapped) svg = getSVG(shape.val().wrapped)
f = open(fileName,'w') f = open(fileName,'w')
f.write(svg) f.write(svg)

View File

@ -428,7 +428,7 @@ class Plane:
return Vector(self.rG.multiply(v.wrapped)) return Vector(self.rG.multiply(v.wrapped))
def rotated(self,rotate=Vector(0,0,0)): def rotated(self,rotate=(0,0,0)):
""" """
returns a copy of this plane, rotated about the specified axes, as measured from horizontal returns a copy of this plane, rotated about the specified axes, as measured from horizontal
@ -440,10 +440,12 @@ class Plane:
rotations are done in order x,y,z. if you need a different order, manually chain together multiple .rotate() rotations are done in order x,y,z. if you need a different order, manually chain together multiple .rotate()
commands commands
:param roate: Vector [xDegrees,yDegrees,zDegrees] :param rotate: Vector [xDegrees,yDegrees,zDegrees]
:return: a copy of this plane rotated as requested :return: a copy of this plane rotated as requested
""" """
if rotate.__class__.__name__ != 'Vector':
rotate = Vector(rotate)
#convert to radians #convert to radians
rotate = rotate.multiply(math.pi / 180.0 ) rotate = rotate.multiply(math.pi / 180.0 )
@ -460,6 +462,32 @@ class Plane:
newP= Plane(self.origin,newXdir,newZdir) newP= Plane(self.origin,newXdir,newZdir)
return newP 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): def _calcTransforms(self):
""" """
Computes transformation martrices to convert betwene local and global coordinates Computes transformation martrices to convert betwene local and global coordinates
@ -484,7 +512,7 @@ class Plane:
""" """
Computes the 2-d projection of the supplied matrix Computes the 2-d projection of the supplied matrix
""" """
rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG) rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG)
return Matrix(rm) return Matrix(rm)

View File

@ -12,5 +12,5 @@ suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCa
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters))
unittest.TextTestRunner().run(suite) unittest.TextTestRunner().run(suite)

View File

@ -46,4 +46,4 @@ class BaseTest(unittest.TestCase):
for i,j in zip(actual,expected): for i,j in zip(actual,expected):
self.assertAlmostEquals(i,j,places) self.assertAlmostEquals(i,j,places)
__all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes'] __all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes','TestExporters','TestCQSelectors']