diff --git a/cadquery/CQ.py b/cadquery/CQ.py index d63ba04..26f5370 100644 --- a/cadquery/CQ.py +++ b/cadquery/CQ.py @@ -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. - - 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. + 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. + 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 + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see """ import time,math @@ -834,18 +834,26 @@ class Workplane(CQ): self.parent = None 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. 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 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 offset: vector to offset the new plane, in local work plane coordinates + :param rotate: 3-tuple of angles to rotate, in degrees relative to 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 """ + + #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.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() )) + p.setOrigin3d(self.plane.toWorldCoords(offset )) ns = self.newObject([p.origin]) ns.plane = p @@ -1215,39 +1223,24 @@ class Workplane(CQ): Future Enhancements: 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 n = self.wire(forConstruction=False) #attempt to consolidate wires together. consolidated = n.consolidateWires() - #ok, mirror all the wires. - - #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 - - #!!!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) + rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(),matrix) + + for w in rotatedWires: + consolidated.objects.append(w) + consolidated._addPendingWire(w) #attempt again to consolidate all of the wires c = consolidated.consolidateWires() - #c = consolidated return c def mirrorY(self): @@ -2174,4 +2167,4 @@ class Workplane(CQ): else: #combine everything return self.union(boxes) - + diff --git a/cadquery/freecad_impl/exporters.py b/cadquery/freecad_impl/exporters.py index be2da12..78c27b8 100644 --- a/cadquery/freecad_impl/exporters.py +++ b/cadquery/freecad_impl/exporters.py @@ -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. - - 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. + 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. + 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 + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see + + 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 try: @@ -25,17 +29,82 @@ try: except ImportError: import xml.etree.ElementTree as ET -class ExportFormats: +class ExportTypes: STL = "STL" - BREP = "BREP" STEP = "STEP" AMF = "AMF" - IGES = "IGES" - + SVG = "SVG" + TJS = "TJS" + class UNITS: MM = "mm" 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): """ Guess the unit of measure of a shape. @@ -55,16 +124,16 @@ def guessUnitOfMeasure(shape): if sum(dimList) < 10: return UNITS.IN - return UNITS.MM + return UNITS.MM + - -class AmfExporter(object): +class AmfWriter(object): def __init__(self,tessellation): self.units = "mm" self.tessellation = tessellation - def writeAmf(self,outFileName): + 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") @@ -94,14 +163,14 @@ class AmfExporter(object): 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 three.js JSON object notation https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0 """ -class JsonExporter(object): +class JsonMesh(object): def __init__(self): self.vertices = []; @@ -174,7 +243,7 @@ def getSVG(shape,opts=None): """ d = {'width':800,'height':240,'marginLeft':200,'marginTop':20} - + if opts: d.update(opts) @@ -235,11 +304,11 @@ def getSVG(shape,opts=None): 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 + 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) diff --git a/cadquery/freecad_impl/geom.py b/cadquery/freecad_impl/geom.py index 4c193ac..ae9ed51 100644 --- a/cadquery/freecad_impl/geom.py +++ b/cadquery/freecad_impl/geom.py @@ -428,7 +428,7 @@ class Plane: 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 @@ -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() commands - :param roate: Vector [xDegrees,yDegrees,zDegrees] + :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 ) @@ -460,6 +462,32 @@ class Plane: 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 @@ -484,7 +512,7 @@ class Plane: """ Computes the 2-d projection of the supplied matrix """ - + rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG) return Matrix(rm) diff --git a/runtests.py b/runtests.py index f358a5c..7020ef4 100644 --- a/runtests.py +++ b/runtests.py @@ -12,5 +12,5 @@ suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCa suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) - +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters)) unittest.TextTestRunner().run(suite) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index faff2a3..7c6e821 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -46,4 +46,4 @@ class BaseTest(unittest.TestCase): for i,j in zip(actual,expected): self.assertAlmostEquals(i,j,places) -__all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes'] +__all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes','TestExporters','TestCQSelectors']