+
+This Prusa i3 extruder support uses cadquery to build the model (https://github.com/adam-urbanczyk/cadquery-models) :
+
+
+
+
+
+The mach30 project used cadquery to develop a tool that will create a rocket thruster directly from the appropriate equations (https://opendesignengine.net/projects/yavin-thruster/wiki):
+
+
+
+
+This example uses Jupyter notebook to produce a really cool web-based scripting environment ( https://github.com/RustyVermeer/avnb/blob/master/readme.md ) :
+
+
+
+
+
+
+
+
+
+We would love to link to your cadquery based project. Just let us know and we'll add it here.
+
+
+
+
+Why CadQuery instead of OpenSCAD?
+========================================
+
+CadQuery is based on OpenCasCade. CadQuery shares many features with OpenSCAD, another open source, script based, parametric model generator.
+
+The primary advantage of OpenSCAD is the large number of already existing model libaries that exist already. So why not simply use OpenSCAD?
+
+CadQuery scripts have several key advantages over OpenSCAD:
+
+1. **The scripts use a standard programming language**, python, and thus can benefit from the associated infrastructure.
+ This includes many standard libraries and IDEs
+
+2. **More powerful CAD kernel** OpenCascade is much more powerful than CGAL. Features supported natively
+ by OCC include NURBS, splines, surface sewing, STL repair, STEP import/export, and other complex operations,
+ in addition to the standard CSG operations supported by CGAL
+
+3. **Ability to import/export STEP** We think the ability to begin with a STEP model, created in a CAD package,
+ and then add parametric features is key. This is possible in OpenSCAD using STL, but STL is a lossy format
+
+4. **Less Code and easier scripting** CadQuery scripts require less code to create most objects, because it is possible to locate
+ features based on the position of other features, workplanes, vertices, etc.
+
+5. **Better Performance** CadQuery scripts can build STL, STEP, and AMF faster than OpenSCAD.
+
+License
+========
+
+CadQuery is licensed under the terms of the Apache Public License, version 2.0.
+A copy of the license can be found at http://www.apache.org/licenses/LICENSE-2.0
+
+CadQuery GUI Interfaces
+=======================
+
+There are currently several known CadQuery GUIs:
+
+### CadQuery FreeCAD Module
+You can use CadQuery inside of FreeCAD. There's an excellent plugin module here https://github.com/jmwright/cadquery-freecad-module
+
+### CadQuery GUI (under active development)
+Work is underway on a stand-alone gui here: https://github.com/jmwright/cadquery-gui
+
+### ParametricParts.com
+If you are impatient and want to see a working example with no installation, have a look at this lego brick example http://parametricparts.com/parts/vqb5dy69/.
+
+The script that generates the model is on the 'modelscript' tab.
+
+
+Installing -- FreeStanding Installation
+========================================
+
+Use these steps if you would like to write CadQuery scripts as a python API. In this case, FreeCAD is used only as a CAD kernel.
+
+1. install FreeCAD, version 0.15 or greater for your platform. https://github.com/FreeCAD/FreeCAD/releases.
+
+2. adjust your path if necessary. FreeCAD bundles a python interpreter, but you'll probably want to use your own,
+ preferably one that has virtualenv available. To use FreeCAD from any python interpreter, just append the FreeCAD
+ lib directory to your path. On (*Nix)::
+
+```python
+ import sys
+ sys.path.append('/usr/lib/freecad/lib')
+```
+
+ or on Windows::
+
+```python
+ import sys
+ sys.path.append('/c/apps/FreeCAD/bin')
+```
+
+ *NOTE* FreeCAD on Windows will not work with python 2.7-- you must use pthon 2.6.X!!!!
+
+3. install cadquery::
+```bash
+ pip install cadquery
+```
+4. installing cadquery should install pyparsing as well, but if not::
+```bash
+ pip install pyparsing
+```
+5. test your installation::
+```python
+ from cadquery import *
+ box = Workplane("XY").box(1,2,3)
+ exporters.toString(box,'STL')
+```
+You're up and running!
+
+Installing -- Using CadQuery from Inside FreeCAD
+=================================================
+
+Use the CadQuery module for FreeCAD here:
+ https://github.com/jmwright/cadquery-freecad-module
+
+It includes a distribution of the latest version of cadquery.
+
+Roadmap/Future Work
+=======================
+
+Work has begun on Cadquery 2.0, which will feature:
+
+ 1. Feature trees, for more powerful selection
+ 2. Direct use of OpenCascade Community Edition(OCE), so that it is no longer required to install FreeCAD
+ 3. https://github.com/jmwright/cadquery-gui, which will allow visualization of workplanes
+
+The project page can be found here: https://github.com/dcowden/cadquery/projects/1
+
+A more detailed description of the plan for CQ 2.0 is here: https://docs.google.com/document/d/1cXuxBkVeYmGOo34MGRdG7E3ILypQqkrJ26oVf3CUSPQ
+
+Where does the name CadQuery come from?
+========================================
+
+CadQuery is inspired by jQuery, a popular framework that
+revolutionized web development involving javascript.
+
+If you are familiar with how jQuery, you will probably recognize several jQuery features that CadQuery uses:
+
+* A fluent api to create clean, easy to read code
+* Language features that make selection and iteration incredibly easy
+*
+* Ability to use the library along side other python libraries
+* Clear and complete documentation, with plenty of samples.
diff --git a/Libs/cadquery/README.txt b/Libs/cadquery/README.txt
new file mode 100644
index 0000000..a356b5b
--- /dev/null
+++ b/Libs/cadquery/README.txt
@@ -0,0 +1,125 @@
+What is a CadQuery?
+========================================
+
+CadQuery is an intuitive, easy-to-use python based language for building parametric 3D CAD models. CadQuery is for 3D CAD what jQuery is for javascript. Imagine selecting Faces of a 3d object the same way you select DOM objects with JQuery!
+
+CadQuery has several goals:
+
+* Build models with scripts that are as close as possible to how you'd describe the object to a human.
+* Create parametric models that can be very easily customized by end users
+* Output high quality CAD formats like STEP and AMF in addition to traditional STL
+* Provide a non-proprietary, plain text model format that can be edited and executed with only a web browser
+
+Using CadQuery, you can write short, simple scripts that produce high quality CAD models. It is easy to make many different objects using a single script that can be customized.
+
+Getting Started With CadQuery
+========================================
+
+The easiest way to get started with CadQuery is to Install FreeCAD ( version 14 recommended ) (http://www.freecadweb.org/) , and then to use our CadQuery-FreeCAD plugin here:
+
+https://github.com/jmwright/cadquery-freecad-module
+
+
+It includes the latest version of cadquery alreadby bundled, and has super-easy installation on Mac, Windows, and Unix.
+
+It has tons of awesome features like integration with FreeCAD so you can see your objects, code-autocompletion, an examples bundle, and script saving/loading. Its definitely the best way to kick the tires!
+
+
+Recently Added Features
+========================================
+
+* 12/5/14 -- New FreeCAD/CadQuery Module! https://github.com/jmwright/cadquery-freecad-module
+* 10/25/14 -- Added Revolution Feature ( thanks Jeremy ! )
+
+
+Why CadQuery instead of OpenSCAD?
+========================================
+
+CadQuery is based on OpenCasCade. CadQuery shares many features with OpenSCAD, another open source, script based, parametric model generator.
+
+The primary advantage of OpenSCAD is the large number of already existing model libaries that exist already. So why not simply use OpenSCAD?
+
+CadQuery scripts have several key advantages over OpenSCAD:
+
+1. **The scripts use a standard programming language**, python, and thus can benefit from the associated infrastructure.
+ This includes many standard libraries and IDEs
+
+2. **More powerful CAD kernel** OpenCascade is much more powerful than CGAL. Features supported natively
+ by OCC include NURBS, splines, surface sewing, STL repair, STEP import/export, and other complex operations,
+ in addition to the standard CSG operations supported by CGAL
+
+3. **Ability to import/export STEP** We think the ability to begin with a STEP model, created in a CAD package,
+ and then add parametric features is key. This is possible in OpenSCAD using STL, but STL is a lossy format
+
+4. **Less Code and easier scripting** CadQuery scripts require less code to create most objects, because it is possible to locate
+ features based on the position of other features, workplanes, vertices, etc.
+
+5. **Better Performance** CadQuery scripts can build STL, STEP, and AMF faster than OpenSCAD.
+
+License
+========
+
+CadQuery is licensed under the terms of the LGPLv3. http://www.gnu.org/copyleft/lesser.html
+
+Where is the GUI?
+==================
+
+If you would like IDE support, you can use CadQuery inside of FreeCAD. There's an excellent plugin module here https://github.com/jmwright/cadquery-freecad-module
+
+CadQuery also provides the backbone of http://parametricparts.com, so the easiest way to see it in action is to review the samples and objects there.
+
+Installing -- FreeStanding Installation
+========================================
+
+Use these steps if you would like to write CadQuery scripts as a python API. In this case, FreeCAD is used only as a CAD kernel.
+
+1. install FreeCAD, version 0.14 or greater for your platform. http://sourceforge.net/projects/free-cad/.
+
+2. adjust your path if necessary. FreeCAD bundles a python interpreter, but you'll probably want to use your own,
+ preferably one that has virtualenv available. To use FreeCAD from any python interpreter, just append the FreeCAD
+ lib directory to your path. On (*Nix)::
+
+ import sys
+ sys.path.append('/usr/lib/freecad/lib')
+
+ or on Windows::
+
+ import sys
+ sys.path.append('/c/apps/FreeCAD/bin')
+
+ *NOTE* FreeCAD on Windows will not work with python 2.7-- you must use pthon 2.6.X!!!!
+
+3. install cadquery::
+
+ pip install cadquery
+
+3. test your installation::
+
+ from cadquery import *
+ box = Workplane("XY").box(1,2,3)
+ exporters.toString(box,'STL')
+
+You're up and running!
+
+Installing -- Using CadQuery from Inside FreeCAD
+=================================================
+
+Use the Excellent CadQuery-FreeCAD plugin here:
+ https://github.com/jmwright/cadquery-freecad-module
+
+It includes a distribution of the latest version of cadquery.
+
+Where does the name CadQuery come from?
+========================================
+
+CadQuery is inspired by ( `jQuery `_ ), a popular framework that
+revolutionized web development involving javascript.
+
+If you are familiar with how jQuery, you will probably recognize several jQuery features that CadQuery uses:
+
+* A fluent api to create clean, easy to read code
+* Language features that make selection and iteration incredibly easy
+*
+* Ability to use the library along side other python libraries
+* Clear and complete documentation, with plenty of samples.
+
diff --git a/Libs/cadquery/build-docs.sh b/Libs/cadquery/build-docs.sh
new file mode 100755
index 0000000..bef2f78
--- /dev/null
+++ b/Libs/cadquery/build-docs.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+sphinx-build -b html doc target/docs
\ No newline at end of file
diff --git a/Libs/cadquery/build_docker.sh b/Libs/cadquery/build_docker.sh
new file mode 100644
index 0000000..3f83507
--- /dev/null
+++ b/Libs/cadquery/build_docker.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+#builds and tests the docker image
+docker build -t dcowden/cadquery .
+
+# set up tests
+CQ_TEST_DIR=/tmp/cq_docker-test
+mkdir -p $CQ_TEST_DIR
+rm -rf $CQ_TEST_DIR/*.*
+cp examples/FreeCAD/Ex001_Simple_Block.py $CQ_TEST_DIR
+
+
+fail_test( ){
+ "Test Failed."
+}
+
+echo "Running Tests..."
+echo "No arguments prints documentation..."
+docker run dcowden/cadquery | grep "CadQuery Docker Image" || fail_test
+echo "OK"
+
+echo "Std in and stdout..."
+cat $CQ_TEST_DIR/Ex001_Simple_Block.py | docker run -i dcowden/cadquery build --in_spec stdin --out_spec stdout | grep "ISO-10303-21" || fail_test
+echo "OK"
+
+echo "Mount a directory and produce output..."
+docker run -i -v $CQ_TEST_DIR:/home/cq dcowden/cadquery build --in_spec Ex001_Simple_Block.py --format STEP
+ls $CQ_TEST_DIR | grep "cqobject-1.STEP" || fail_test
+echo "OK"
+
+echo "Future Server EntryPoint"
+docker run -i dcowden/cadquery runserver | grep "Future CadQuery Server" || fail_test
+echo "OK"
diff --git a/Libs/cadquery/cadquery/README.txt b/Libs/cadquery/cadquery/README.txt
new file mode 100644
index 0000000..ab8dc7e
--- /dev/null
+++ b/Libs/cadquery/cadquery/README.txt
@@ -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.
+
+***
\ No newline at end of file
diff --git a/Libs/cadquery/cadquery/__init__.py b/Libs/cadquery/cadquery/__init__.py
new file mode 100644
index 0000000..a4bac34
--- /dev/null
+++ b/Libs/cadquery/cadquery/__init__.py
@@ -0,0 +1,21 @@
+#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 *
+from .cq import *
+
+
+__all__ = [
+ 'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
+ 'Shape','Vertex','Edge','Wire','Face','Solid','Shell','Compound','exporters', 'importers',
+ 'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector',
+ 'TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins',
+]
+
+__version__ = "1.0.0"
diff --git a/Libs/cadquery/cadquery/contrib/__init__.py b/Libs/cadquery/cadquery/contrib/__init__.py
new file mode 100644
index 0000000..67c7b68
--- /dev/null
+++ b/Libs/cadquery/cadquery/contrib/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Copyright (C) 2011-2015 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
+"""
diff --git a/Libs/cadquery/cadquery/cq.py b/Libs/cadquery/cadquery/cq.py
new file mode 100644
index 0000000..6c43d32
--- /dev/null
+++ b/Libs/cadquery/cadquery/cq.py
@@ -0,0 +1,2565 @@
+"""
+ Copyright (C) 2011-2015 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
+"""
+
+import time
+import math
+from cadquery import *
+from cadquery import selectors
+from cadquery import exporters
+
+
+class CQContext(object):
+ """
+ A shared context for modeling.
+
+ All objects in the same CQ chain share a reference to this same object instance
+ which allows for shared state when needed,
+ """
+ def __init__(self):
+ self.pendingWires = [] # a list of wires that have been created and need to be extruded
+ self.pendingEdges = [] # a list of created pending edges that need to be joined into wires
+ # a reference to the first point for a set of edges.
+ # Used to determine how to behave when close() is called
+ self.firstPoint = None
+ self.tolerance = 0.0001 # user specified tolerance
+
+
+class CQ(object):
+ """
+ Provides enhanced functionality for a wrapped CAD primitive.
+
+ Examples include feature selection, feature creation, 2d drawing
+ using work planes, and 3d operations like fillets, shells, and splitting
+ """
+
+ def __init__(self, obj):
+ """
+ Construct a new CadQuery (CQ) object that wraps a CAD primitive.
+
+ :param obj: Object to Wrap.
+ :type obj: A CAD Primitive ( wire,vertex,face,solid,edge )
+ """
+ self.objects = []
+ self.ctx = CQContext()
+ self.parent = None
+
+ if obj: # guarded because sometimes None for internal use
+ self.objects.append(obj)
+
+ def newObject(self, objlist):
+ """
+ Make a new CQ object.
+
+ :param objlist: The stack of objects to use
+ :type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc )
+
+ The parent of the new object will be set to the current object,
+ to preserve the chain correctly.
+
+ Custom plugins and subclasses should use this method to create new CQ objects
+ correctly.
+ """
+ r = CQ(None) # create a completely blank one
+ r.parent = self
+ r.ctx = self.ctx # context solid remains the same
+ r.objects = list(objlist)
+ return r
+
+ def _collectProperty(self, propName):
+ """
+ Collects all of the values for propName,
+ for all items on the stack.
+ FreeCAD objects do not implement id correctly,
+ so hashCode is used to ensure we don't add the same
+ object multiple times.
+
+ One weird use case is that the stack could have a solid reference object
+ on it. This is meant to be a reference to the most recently modified version
+ of the context solid, whatever it is.
+ """
+ all = {}
+ for o in self.objects:
+
+ # tricky-- if an object is a compound of solids,
+ # do not return all of the solids underneath-- typically
+ # then we'll keep joining to ourself
+ if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound':
+ for i in getattr(o, 'Compounds')():
+ all[i.hashCode()] = i
+ else:
+ if hasattr(o, propName):
+ for i in getattr(o, propName)():
+ all[i.hashCode()] = i
+
+ return list(all.values())
+
+ def split(self, keepTop=False, keepBottom=False):
+ """
+ Splits a solid on the stack into two parts, optionally keeping the separate parts.
+
+ :param boolean keepTop: True to keep the top, False or None to discard it
+ :param boolean keepBottom: True to keep the bottom, False or None to discard it
+ :raises: ValueError if keepTop and keepBottom are both false.
+ :raises: ValueError if there is not a solid in the current stack or the parent chain
+ :returns: CQ object with the desired objects on the stack.
+
+ The most common operation splits a solid and keeps one half. This sample creates
+ split bushing::
+
+ #drill a hole in the side
+ c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()F
+ #now cut it in half sideways
+ c.faces(">Y").workplane(-0.5).split(keepTop=True)
+ """
+
+ solid = self.findSolid()
+
+ if (not keepTop) and (not keepBottom):
+ raise ValueError("You have to keep at least one half")
+
+ maxDim = solid.BoundingBox().DiagonalLength * 10.0
+ topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim)
+ bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim)
+
+ top = solid.cut(bottomCutBox)
+ bottom = solid.cut(topCutBox)
+
+ if keepTop and keepBottom:
+ # Put both on the stack, leave original unchanged.
+ return self.newObject([top, bottom])
+ else:
+ # Put the one we are keeping on the stack, and also update the
+ # context solidto the one we kept.
+ if keepTop:
+ solid.wrapped = top.wrapped
+ return self.newObject([top])
+ else:
+ solid.wrapped = bottom.wrapped
+ return self.newObject([bottom])
+
+ def combineSolids(self, otherCQToCombine=None):
+ """
+ !!!DEPRECATED!!! use union()
+ Combines all solids on the current stack, and any context object, together
+ into a single object.
+
+ After the operation, the returned solid is also the context solid.
+
+ :param otherCQToCombine: another CadQuery to combine.
+ :return: a cQ object with the resulting combined solid on the stack.
+
+ Most of the time, both objects will contain a single solid, which is
+ combined and returned on the stack of the new object.
+ """
+ #loop through current stack objects, and combine them
+ #TODO: combine other types of objects as well, like edges and wires
+ toCombine = self.solids().vals()
+
+ if otherCQToCombine:
+ for obj in otherCQToCombine.solids().vals():
+ toCombine.append(obj)
+
+ if len(toCombine) < 1:
+ raise ValueError("Cannot Combine: at least one solid required!")
+
+ #get context solid and we don't want to find our own objects
+ ctxSolid = self.findSolid(searchStack=False, searchParents=True)
+
+ if ctxSolid is None:
+ ctxSolid = toCombine.pop(0)
+
+ #now combine them all. make sure to save a reference to the ctxSolid pointer!
+ s = ctxSolid
+ for tc in toCombine:
+ s = s.fuse(tc)
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def all(self):
+ """
+ Return a list of all CQ objects on the stack.
+
+ useful when you need to operate on the elements
+ individually.
+
+ Contrast with vals, which returns the underlying
+ objects for all of the items on the stack
+ """
+ return [self.newObject([o]) for o in self.objects]
+
+ def size(self):
+ """
+ Return the number of objects currently on the stack
+ """
+ return len(self.objects)
+
+ def vals(self):
+ """
+ get the values in the current list
+
+ :rtype: list of FreeCAD objects
+ :returns: the values of the objects on the stack.
+
+ Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack
+ """
+ return self.objects
+
+ def add(self, obj):
+ """
+ Adds an object or a list of objects to the stack
+
+ :param obj: an object to add
+ :type obj: a CQ object, CAD primitive, or list of CAD primitives
+ :return: a CQ object with the requested operation performed
+
+ If an CQ object, the values of that object's stack are added. If a list of cad primitives,
+ they are all added. If a single CAD primitive it is added
+
+ Used in rare cases when you need to combine the results of several CQ results
+ into a single CQ object. Shelling is one common example
+ """
+ if type(obj) == list:
+ self.objects.extend(obj)
+ elif type(obj) == CQ or type(obj) == Workplane:
+ self.objects.extend(obj.objects)
+ else:
+ self.objects.append(obj)
+ return self
+
+ def val(self):
+ """
+ Return the first value on the stack
+
+ :return: the first value on the stack.
+ :rtype: A FreeCAD object or a SolidReference
+ """
+ return self.objects[0]
+
+ def toFreecad(self):
+ """
+ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code
+ needed when rendering a model in FreeCAD's 3D view.
+ :return: The wrapped FreeCAD object
+ :rtype A FreeCAD object or a SolidReference
+ """
+
+ return self.objects[0].wrapped
+
+ def workplane(self, offset=0.0, invert=False, centerOption='CenterOfMass'):
+ """
+ Creates a new 2-D workplane, located relative to the first face on the stack.
+
+ :param offset: offset for the work plane in the Z direction. Default
+ :param invert: invert the Z direction from that of the face.
+ :type offset: float or None=0.0
+ :type invert: boolean or None=False
+ :rtype: Workplane object ( which is a subclass of CQ )
+
+ The first element on the stack must be a face, a set of
+ co-planar faces or a vertex. If a vertex, then the parent
+ item on the chain immediately before the vertex must be a
+ face.
+
+ The result will be a 2-d working plane
+ with a new coordinate system set up as follows:
+
+ * The origin will be located in the *center* of the
+ face/faces, if a face/faces was selected. If a vertex was
+ selected, the origin will be at the vertex, and located
+ on the face.
+ * The Z direction will be normal to the plane of the face,computed
+ at the center point.
+ * The X direction will be parallel to the x-y plane. If the workplane is parallel to
+ the global x-y plane, the x direction of the workplane will co-incide with the
+ global x direction.
+
+ Most commonly, the selected face will be planar, and the workplane lies in the same plane
+ of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from
+ an existing surface, and even more rarely to define a workplane based on a face that is
+ not planar.
+
+ To create a workplane without first having a face, use the Workplane() method.
+
+ Future Enhancements:
+ * Allow creating workplane from planar wires
+ * Allow creating workplane based on an arbitrary point on a face, not just the center.
+ For now you can work around by creating a workplane and then offsetting the center
+ afterwards.
+ """
+ def _isCoPlanar(f0, f1):
+ """Test if two faces are on the same plane."""
+ p0 = f0.Center()
+ p1 = f1.Center()
+ n0 = f0.normalAt()
+ n1 = f1.normalAt()
+
+ # test normals (direction of planes)
+ if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or
+ (abs(n0.y-n1.y) < self.ctx.tolerance) or
+ (abs(n0.z-n1.z) < self.ctx.tolerance)):
+ return False
+
+ # test if p1 is on the plane of f0 (offset of planes)
+ return abs(n0.dot(p0.sub(p1)) < self.ctx.tolerance)
+
+ def _computeXdir(normal):
+ """
+ Figures out the X direction based on the given normal.
+ :param :normal The direction that's normal to the plane.
+ :type :normal A Vector
+ :return A vector representing the X direction.
+ """
+ xd = Vector(0, 0, 1).cross(normal)
+ if xd.Length < self.ctx.tolerance:
+ #this face is parallel with the x-y plane, so choose x to be in global coordinates
+ xd = Vector(1, 0, 0)
+ return xd
+
+ if len(self.objects) > 1:
+ # are all objects 'PLANE'?
+ if not all(o.geomType() == 'PLANE' for o in self.objects):
+ raise ValueError("If multiple objects selected, they all must be planar faces.")
+
+ # are all faces co-planar with each other?
+ if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
+ raise ValueError("Selected faces must be co-planar.")
+
+ if centerOption == 'CenterOfMass':
+ center = Shape.CombinedCenter(self.objects)
+ elif centerOption == 'CenterOfBoundBox':
+ center = Shape.CombinedCenterOfBoundBox(self.objects)
+
+ normal = self.objects[0].normalAt()
+ xDir = _computeXdir(normal)
+
+ else:
+ obj = self.objects[0]
+
+ if isinstance(obj, Face):
+ if centerOption == 'CenterOfMass':
+ center = obj.Center()
+ elif centerOption == 'CenterOfBoundBox':
+ center = obj.CenterOfBoundBox()
+ normal = obj.normalAt(center)
+ xDir = _computeXdir(normal)
+ else:
+ if hasattr(obj, 'Center'):
+ if centerOption == 'CenterOfMass':
+ center = obj.Center()
+ elif centerOption == 'CenterOfBoundBox':
+ center = obj.CenterOfBoundBox()
+ normal = self.plane.zDir
+ xDir = self.plane.xDir
+ else:
+ raise ValueError("Needs a face or a vertex or point on a work plane")
+
+ #invert if requested
+ if invert:
+ normal = normal.multiply(-1.0)
+
+ #offset origin if desired
+ offsetVector = normal.normalized().multiply(offset)
+ offsetCenter = center.add(offsetVector)
+
+ #make the new workplane
+ plane = Plane(offsetCenter, xDir, normal)
+ s = Workplane(plane)
+ s.parent = self
+ s.ctx = self.ctx
+
+ #a new workplane has the center of the workplane on the stack
+ return s
+
+ def first(self):
+ """
+ Return the first item on the stack
+ :returns: the first item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject(self.objects[0:1])
+
+ def item(self, i):
+ """
+
+ Return the ith item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject([self.objects[i]])
+
+ def last(self):
+ """
+ Return the last item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject([self.objects[-1]])
+
+ def end(self):
+ """
+ Return the parent of this CQ element
+ :rtype: a CQ object
+ :raises: ValueError if there are no more parents in the chain.
+
+ For example::
+
+ CQ(obj).faces("+Z").vertices().end()
+
+ will return the same as::
+
+ CQ(obj).faces("+Z")
+ """
+ if self.parent:
+ return self.parent
+ else:
+ raise ValueError("Cannot End the chain-- no parents!")
+
+ def findSolid(self, searchStack=True, searchParents=True):
+ """
+ Finds the first solid object in the chain, searching from the current node
+ backwards through parents until one is found.
+
+ :param searchStack: should objects on the stack be searched first.
+ :param searchParents: should parents be searched?
+ :raises: ValueError if no solid is found in the current object or its parents,
+ and errorOnEmpty is True
+
+ This function is very important for chains that are modifying a single parent object,
+ most often a solid.
+
+ Most of the time, a chain defines or selects a solid, and then modifies it using workplanes
+ or other operations.
+
+ Plugin Developers should make use of this method to find the solid that should be modified,
+ if the plugin implements a unary operation, or if the operation will automatically merge its
+ results with an object already on the stack.
+ """
+ #notfound = ValueError("Cannot find a Valid Solid to Operate on!")
+
+ if searchStack:
+ for s in self.objects:
+ if isinstance(s, Solid):
+ return s
+ elif isinstance(s, Compound):
+ return s.Solids()
+
+ if searchParents and self.parent is not None:
+ return self.parent.findSolid(searchStack=True, searchParents=searchParents)
+
+ return None
+
+ def _selectObjects(self, objType, selector=None):
+ """
+ Filters objects of the selected type with the specified selector,and returns results
+
+ :param objType: the type of object we are searching for
+ :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
+ :return: a CQ object with the selected objects on the stack.
+
+ **Implementation Note**: This is the base implementation of the vertices,edges,faces,
+ solids,shells, and other similar selector methods. It is a useful extension point for
+ plugin developers to make other selector methods.
+ """
+ # A single list of all faces from all objects on the stack
+ toReturn = self._collectProperty(objType)
+
+ if selector is not None:
+ if isinstance(selector, str) or isinstance(selector, str):
+ selectorObj = selectors.StringSyntaxSelector(selector)
+ else:
+ selectorObj = selector
+ toReturn = selectorObj.filter(toReturn)
+
+ return self.newObject(toReturn)
+
+ def vertices(self, selector=None):
+ """
+ Select the vertices of objects on the stack, optionally filtering the selection. If there
+ are multiple objects on the stack, the vertices of all objects are collected and a list of
+ all the distinct vertices is returned.
+
+ :param selector:
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the
+ current stack, after being filtered by the selector, if provided
+
+ If there are no vertices for any objects on the current stack, an empty CQ object
+ is returned
+
+ The typical use is to select the vertices of a single object on the stack. For example::
+
+ Workplane().box(1,1,1).faces("+Z").vertices().size()
+
+ returns 4, because the topmost face of cube will contain four vertices. While this::
+
+ Workplane().box(1,1,1).faces().vertices().size()
+
+ returns 8, because a cube has a total of 8 vertices
+
+ **Note** Circles are peculiar, they have a single vertex at the center!
+
+ :py:class:`StringSyntaxSelector`
+
+ """
+ return self._selectObjects('Vertices', selector)
+
+ def faces(self, selector=None):
+ """
+ Select the faces of objects on the stack, optionally filtering the selection. If there are
+ multiple objects on the stack, the faces of all objects are collected and a list of all the
+ distinct faces is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no vertices for any objects on the current stack, an empty CQ object
+ is returned.
+
+ The typical use is to select the faces of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").size()
+
+ returns 1, because a cube has one face with a normal in the +Z direction. Similarly::
+
+ CQ(aCube).faces().size()
+
+ returns 6, because a cube has a total of 6 faces, And::
+
+ CQ(aCube).faces("|Z").size()
+
+ returns 2, because a cube has 2 faces having normals parallel to the z direction
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Faces', selector)
+
+ def edges(self, selector=None):
+ """
+ Select the edges of objects on the stack, optionally filtering the selection. If there are
+ multiple objects on the stack, the edges of all objects are collected and a list of all the
+ distinct edges is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no edges for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the edges of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").edges().size()
+
+ returns 4, because a cube has one face with a normal in the +Z direction. Similarly::
+
+ CQ(aCube).edges().size()
+
+ returns 12, because a cube has a total of 12 edges, And::
+
+ CQ(aCube).edges("|Z").size()
+
+ returns 4, because a cube has 4 edges parallel to the z direction
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Edges', selector)
+
+ def wires(self, selector=None):
+ """
+ Select the wires of objects on the stack, optionally filtering the selection. If there are
+ multiple objects on the stack, the wires of all objects are collected and a list of all the
+ distinct wires is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no wires for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the wires of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").wires().size()
+
+ returns 1, because a face typically only has one outer wire
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Wires', selector)
+
+ def solids(self, selector=None):
+ """
+ Select the solids of objects on the stack, optionally filtering the selection. If there are
+ multiple objects on the stack, the solids of all objects are collected and a list of all the
+ distinct solids is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no solids for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the a single object on the stack. For example::
+
+ CQ(aCube).solids().size()
+
+ returns 1, because a cube consists of one solid.
+
+ It is possible for single CQ object ( or even a single CAD primitive ) to contain
+ multiple solids.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Solids', selector)
+
+ def shells(self, selector=None):
+ """
+ Select the shells of objects on the stack, optionally filtering the selection. If there are
+ multiple objects on the stack, the shells of all objects are collected and a list of all the
+ distinct shells is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no shells for any objects on the current stack, an empty CQ object is returned
+
+ Most solids will have a single shell, which represents the outer surface. A shell will
+ typically be composed of multiple faces.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Shells', selector)
+
+ def compounds(self, selector=None):
+ """
+ Select compounds on the stack, optionally filtering the selection. If there are multiple
+ objects on the stack, they are collected and a list of all the distinct compounds
+ is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ A compound contains multiple CAD primitives that resulted from a single operation, such as
+ a union, cut, split, or fillet. Compounds can contain multiple edges, wires, or solids.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Compounds', selector)
+
+ def toSvg(self, opts=None):
+ """
+ Returns svg text that represents the first item on the stack.
+
+ for testing purposes.
+
+ :param opts: svg formatting options
+ :type opts: dictionary, width and height
+ :return: a string that contains SVG that represents this item.
+ """
+ return exporters.getSVG(self.val().wrapped, opts)
+
+ def exportSvg(self, fileName):
+ """
+ Exports the first item on the stack as an SVG file
+
+ For testing purposes mainly.
+
+ :param fileName: the filename to export
+ :type fileName: String, absolute path to the file
+ """
+ exporters.exportSVG(self, fileName)
+
+ def rotateAboutCenter(self, axisEndPoint, angleDegrees):
+ """
+ Rotates all items on the stack by the specified angle, about the specified axis
+
+ The center of rotation is a vector starting at the center of the object on the stack,
+ and ended at the specified point.
+
+ :param axisEndPoint: the second point of axis of rotation
+ :type axisEndPoint: a three-tuple in global coordinates
+ :param angleDegrees: the rotation angle, in degrees
+ :type angleDegrees: float
+ :returns: a CQ object, with all items rotated.
+
+ WARNING: This version returns the same cq object instead of a new one-- the
+ old object is not accessible.
+
+ Future Enhancements:
+ * A version of this method that returns a transformed copy, rather than modifying
+ the originals
+ * This method doesnt expose a very good interface, because the axis of rotation
+ could be inconsistent between multiple objects. This is because the beginning
+ of the axis is variable, while the end is fixed. This is fine when operating on
+ one object, but is not cool for multiple.
+ """
+
+ #center point is the first point in the vector
+ endVec = Vector(axisEndPoint)
+
+ def _rot(obj):
+ startPt = obj.Center()
+ endPt = startPt + endVec
+ return obj.rotate(startPt, endPt, angleDegrees)
+
+ return self.each(_rot, False)
+
+ def rotate(self, axisStartPoint, axisEndPoint, angleDegrees):
+ """
+ Returns a copy of all of the items on the stack rotated through and angle around the axis
+ of rotation.
+
+ :param axisStartPoint: The first point of the axis of rotation
+ :type axisStartPoint: a 3-tuple of floats
+ :type axisEndPoint: The second point of the axis of rotation
+ :type axisEndPoint: a 3-tuple of floats
+ :param angleDegrees: the rotation angle, in degrees
+ :type angleDegrees: float
+ :returns: a CQ object
+ """
+ return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees)
+ for o in self.objects])
+
+ def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
+ """
+ Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring
+
+ :param mirrorPlane: the plane to mirror about
+ :type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes
+ :param basePointVector: the base point to mirror about
+ :type basePointVector: tuple
+ """
+ newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)])
+ return newS.first()
+
+
+ def translate(self, vec):
+ """
+ Returns a copy of all of the items on the stack moved by the specified translation vector.
+
+ :param tupleDistance: distance to move, in global coordinates
+ :type tupleDistance: a 3-tuple of float
+ :returns: a CQ object
+ """
+ return self.newObject([o.translate(vec) for o in self.objects])
+
+
+ def shell(self, thickness):
+ """
+ Remove the selected faces to create a shell of the specified thickness.
+
+ To shell, first create a solid, and *in the same chain* select the faces you wish to remove.
+
+ :param thickness: a positive float, representing the thickness of the desired shell.
+ Negative values shell inwards, positive values shell outwards.
+ :raises: ValueError if the current stack contains objects that are not faces of a solid
+ further up in the chain.
+ :returns: a CQ object with the resulting shelled solid selected.
+
+ This example will create a hollowed out unit cube, where the top most face is open,
+ and all other walls are 0.2 units thick::
+
+ Workplane().box(1,1,1).faces("+Z").shell(0.2)
+
+ Shelling is one of the cases where you may need to use the add method to select several
+ faces. For example, this example creates a 3-walled corner, by removing three faces
+ of a cube::
+
+ s = Workplane().box(1,1,1)
+ s1 = s.faces("+Z")
+ s1.add(s.faces("+Y")).add(s.faces("+X"))
+ self.saveModel(s1.shell(0.2))
+
+ This fairly yucky syntax for selecting multiple faces is planned for improvement
+
+ **Note**: When sharp edges are shelled inwards, they remain sharp corners, but **outward**
+ shells are automatically filleted, because an outward offset from a corner generates
+ a radius.
+
+
+ Future Enhancements:
+ Better selectors to make it easier to select multiple faces
+ """
+ solidRef = self.findSolid()
+
+ for f in self.objects:
+ if type(f) != Face:
+ raise ValueError("Shelling requires that faces be selected")
+
+ s = solidRef.shell(self.objects, thickness)
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def fillet(self, radius):
+ """
+ Fillets a solid on the selected edges.
+
+ The edges on the stack are filleted. The solid to which the edges belong must be in the
+ parent chain of the selected edges.
+
+ :param radius: the radius of the fillet, must be > zero
+ :type radius: positive float
+ :raises: ValueError if at least one edge is not selected
+ :raises: ValueError if the solid containing the edge is not in the chain
+ :returns: cq object with the resulting solid selected.
+
+ This example will create a unit cube, with the top edges filleted::
+
+ s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1)
+ """
+ # TODO: we will need much better edge selectors for this to work
+ # TODO: ensure that edges selected actually belong to the solid in the chain, otherwise,
+ # TODO: we segfault
+
+ solid = self.findSolid()
+
+ edgeList = self.edges().vals()
+ if len(edgeList) < 1:
+ raise ValueError("Fillets requires that edges be selected")
+
+ s = solid.fillet(radius, edgeList)
+ solid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def chamfer(self, length, length2=None):
+ """
+ Chamfers a solid on the selected edges.
+
+ The edges on the stack are chamfered. The solid to which the
+ edges belong must be in the parent chain of the selected
+ edges.
+
+ Optional parameter `length2` can be supplied with a different
+ value than `length` for a chamfer that is shorter on one side
+ longer on the other side.
+
+ :param length: the length of the fillet, must be greater than zero
+ :param length2: optional parameter for asymmetrical chamfer
+ :type length: positive float
+ :type length2: positive float
+ :raises: ValueError if at least one edge is not selected
+ :raises: ValueError if the solid containing the edge is not in the chain
+ :returns: cq object with the resulting solid selected.
+
+ This example will create a unit cube, with the top edges chamfered::
+
+ s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.1)
+
+ This example will create chamfers longer on the sides::
+
+ s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.2, 0.1)
+ """
+ solid = self.findSolid()
+
+ edgeList = self.edges().vals()
+ if len(edgeList) < 1:
+ raise ValueError("Chamfer requires that edges be selected")
+
+ s = solid.chamfer(length, length2, edgeList)
+
+ solid.wrapped = s.wrapped
+ return self.newObject([s])
+
+
+class Workplane(CQ):
+ """
+ Defines a coordinate system in space, in which 2-d coordinates can be used.
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial
+ stack value.
+ :raises: ValueError if the provided plane is not a plane, a valid named workplane
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane,
+ and the *current point* is on the origin.
+
+ .. note::
+ You can also create workplanes on the surface of existing faces using
+ :py:meth:`CQ.workplane`
+ """
+
+ FOR_CONSTRUCTION = 'ForConstruction'
+
+ def __init__(self, inPlane, origin=(0, 0, 0), obj=None):
+ """
+ make a workplane from a particular plane
+
+ :param inPlane: the plane in which the workplane will be done
+ :type inPlane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial
+ stack value.
+ :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and
+ the *current point* is on the origin.
+ """
+
+ if inPlane.__class__.__name__ == 'Plane':
+ tmpPlane = inPlane
+ elif isinstance(inPlane, str) or isinstance(inPlane, str):
+ tmpPlane = Plane.named(inPlane, origin)
+ else:
+ tmpPlane = None
+
+ if tmpPlane is None:
+ raise ValueError(
+ 'Provided value {} is not a valid work plane'.format(inPlane))
+
+ self.obj = obj
+ self.plane = tmpPlane
+ self.firstPoint = None
+ # Changed so that workplane has the center as the first item on the stack
+ self.objects = [self.plane.origin]
+ self.parent = None
+ self.ctx = CQContext()
+
+ 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: 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.origin = self.plane.toWorldCoords(offset)
+ ns = self.newObject([p.origin])
+ ns.plane = p
+
+ return ns
+
+ def newObject(self, objlist):
+ """
+ Create a new workplane object from this one.
+
+ Overrides CQ.newObject, and should be used by extensions, plugins, and
+ subclasses to create new objects.
+
+ :param objlist: new objects to put on the stack
+ :type objlist: a list of CAD primitives
+ :return: a new Workplane object with the current workplane as a parent.
+ """
+
+ #copy the current state to the new object
+ ns = Workplane("XY")
+ ns.plane = self.plane
+ ns.parent = self
+ ns.objects = list(objlist)
+ ns.ctx = self.ctx
+ return ns
+
+ def _findFromPoint(self, useLocalCoords=False):
+ """
+ Finds the start point for an operation when an existing point
+ is implied. Examples include 2d operations such as lineTo,
+ which allows specifying the end point, and implicitly use the
+ end of the previous line as the starting point
+
+ :return: a Vector representing the point to use, or none if
+ such a point is not available.
+
+ :param useLocalCoords: selects whether the point is returned
+ in local coordinates or global coordinates.
+
+ The algorithm is this:
+ * If an Edge is on the stack, its end point is used.yp
+ * if a vector is on the stack, it is used
+
+ WARNING: only the last object on the stack is used.
+
+ NOTE:
+ """
+ obj = self.objects[-1]
+
+ if isinstance(obj, Edge):
+ p = obj.endPoint()
+ elif isinstance(obj, Vector):
+ p = obj
+ else:
+ raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
+
+ if useLocalCoords:
+ return self.plane.toLocalCoords(p)
+ else:
+ return p
+
+ def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True):
+ """
+ Creates an array of points and pushes them onto the stack.
+ If you want to position the array at another point, create another workplane
+ that is shifted to the position you would like to use as a reference
+
+ :param xSpacing: spacing between points in the x direction ( must be > 0)
+ :param ySpacing: spacing between points in the y direction ( must be > 0)
+ :param xCount: number of points ( > 0 )
+ :param yCount: number of points ( > 0 )
+ :param center: if true, the array will be centered at the center of the workplane. if
+ false, the lower left corner will be at the center of the work plane
+ """
+
+ if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1:
+ raise ValueError("Spacing and count must be > 0 ")
+
+ lpoints = [] # coordinates relative to bottom left point
+ for x in range(xCount):
+ for y in range(yCount):
+ lpoints.append((xSpacing * x, ySpacing * y))
+
+ #shift points down and left relative to origin if requested
+ if center:
+ xc = xSpacing*(xCount-1) * 0.5
+ yc = ySpacing*(yCount-1) * 0.5
+ cpoints = []
+ for p in lpoints:
+ cpoints.append((p[0] - xc, p[1] - yc))
+ lpoints = list(cpoints)
+
+ return self.pushPoints(lpoints)
+
+ def pushPoints(self, pntList):
+ """
+ Pushes a list of points onto the stack as vertices.
+ The points are in the 2-d coordinate space of the workplane face
+
+ :param pntList: a list of points to push onto the stack
+ :type pntList: list of 2-tuples, in *local* coordinates
+ :return: a new workplane with the desired points on the stack.
+
+ A common use is to provide a list of points for a subsequent operation, such as creating
+ circles or holes. This example creates a cube, and then drills three holes through it,
+ based on three points::
+
+ s = Workplane().box(1,1,1).faces(">Z").workplane().\
+ pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)])
+ body = s.circle(0.05).cutThruAll()
+
+ Here the circle function operates on all three points, and is then extruded to create three
+ holes. See :py:meth:`circle` for how it works.
+ """
+ vecs = []
+ for pnt in pntList:
+ vec = self.plane.toWorldCoords(pnt)
+ vecs.append(vec)
+
+ return self.newObject(vecs)
+
+ def center(self, x, y):
+ """
+ Shift local coordinates to the specified location.
+
+ The location is specified in terms of local coordinates.
+
+ :param float x: the new x location
+ :param float y: the new y location
+ :returns: the workplane object, with the center adjusted.
+
+ The current point is set to the new center.
+ This method is useful to adjust the center point after it has been created automatically on
+ a face, but not where you'd like it to be.
+
+ In this example, we adjust the workplane center to be at the corner of a cube, instead of
+ the center of a face, which is the default::
+
+ #this workplane is centered at x=0.5,y=0.5, the center of the upper face
+ s = Workplane().box(1,1,1).faces(">Z").workplane()
+
+ s.center(-0.5,-0.5) # move the center to the corner
+ t = s.circle(0.25).extrude(0.2)
+ assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner
+
+ The result is a cube with a round boss on the corner
+ """
+ "Shift local coordinates to the specified location, according to current coordinates"
+ self.plane.setOrigin2d(x, y)
+ n = self.newObject([self.plane.origin])
+ return n
+
+ def lineTo(self, x, y, forConstruction=False):
+ """
+ Make a line from the current point to the provided point
+
+ :param float x: the x point, in workplane plane coordinates
+ :param float y: the y point, in workplane plane coordinates
+ :return: the Workplane object with the current point at the end of the new line
+
+ see :py:meth:`line` if you want to use relative dimensions to make a line instead.
+ """
+ startPoint = self._findFromPoint(False)
+
+ endPoint = self.plane.toWorldCoords((x, y))
+
+ p = Edge.makeLine(startPoint, endPoint)
+
+ if not forConstruction:
+ self._addPendingEdge(p)
+
+ return self.newObject([p])
+
+ # line a specified incremental amount from current point
+ def line(self, xDist, yDist, forConstruction=False):
+ """
+ Make a line from the current point to the provided point, using
+ dimensions relative to the current point
+
+ :param float xDist: x distance from current point
+ :param float yDist: y distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead.
+ """
+ p = self._findFromPoint(True) # return local coordinates
+ return self.lineTo(p.x + xDist, yDist + p.y, forConstruction)
+
+ def vLine(self, distance, forConstruction=False):
+ """
+ Make a vertical line from the current point the provided distance
+
+ :param float distance: (y) distance from current point
+ :return: the workplane object with the current point at the end of the new line
+ """
+ return self.line(0, distance, forConstruction)
+
+ def hLine(self, distance, forConstruction=False):
+ """
+ Make a horizontal line from the current point the provided distance
+
+ :param float distance: (x) distance from current point
+ :return: the Workplane object with the current point at the end of the new line
+ """
+ return self.line(distance, 0, forConstruction)
+
+ def vLineTo(self, yCoord, forConstruction=False):
+ """
+ Make a vertical line from the current point to the provided y coordinate.
+
+ Useful if it is more convenient to specify the end location rather than distance,
+ as in :py:meth:`vLine`
+
+ :param float yCoord: y coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(p.x, yCoord, forConstruction)
+
+ def hLineTo(self, xCoord, forConstruction=False):
+ """
+ Make a horizontal line from the current point to the provided x coordinate.
+
+ Useful if it is more convenient to specify the end location rather than distance,
+ as in :py:meth:`hLine`
+
+ :param float xCoord: x coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(xCoord, p.y, forConstruction)
+
+ #absolute move in current plane, not drawing
+ def moveTo(self, x=0, y=0):
+ """
+ Move to the specified point, without drawing.
+
+ :param x: desired x location, in local coordinates
+ :type x: float, or none for zero
+ :param y: desired y location, in local coordinates
+ :type y: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`move` to do the same thing but using relative dimensions
+ """
+ newCenter = Vector(x, y, 0)
+ return self.newObject([self.plane.toWorldCoords(newCenter)])
+
+ #relative move in current plane, not drawing
+ def move(self, xDist=0, yDist=0):
+ """
+ Move the specified distance from the current point, without drawing.
+
+ :param xDist: desired x distance, in local coordinates
+ :type xDist: float, or none for zero
+ :param yDist: desired y distance, in local coordinates
+ :type yDist: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`moveTo` to do the same thing but using absolute coordinates
+ """
+ p = self._findFromPoint(True)
+ newCenter = p + Vector(xDist, yDist, 0)
+ return self.newObject([self.plane.toWorldCoords(newCenter)])
+
+ def spline(self, listOfXYTuple, forConstruction=False):
+ """
+ Create a spline interpolated through the provided points.
+
+ :param listOfXYTuple: points to interpolate through
+ :type listOfXYTuple: list of 2-tuple
+ :return: a Workplane object with the current point at the end of the spline
+
+ The spline will begin at the current point, and
+ end with the last point in the XY tuple list
+
+ This example creates a block with a spline for one side::
+
+ s = Workplane(Plane.XY())
+ sPnts = [
+ (2.75,1.5),
+ (2.5,1.75),
+ (2.0,1.5),
+ (1.5,1.0),
+ (1.0,1.25),
+ (0.5,1.0),
+ (0,1.0)
+ ]
+ r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
+ r = r.extrude(0.5)
+
+ *WARNING* It is fairly easy to create a list of points
+ that cannot be correctly interpreted as a spline.
+
+ Future Enhancements:
+ * provide access to control points
+ """
+ gstartPoint = self._findFromPoint(False)
+ gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1])
+
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple]
+ allPoints = [gstartPoint] + vecs
+
+ e = Edge.makeSpline(allPoints)
+
+ if not forConstruction:
+ self._addPendingEdge(e)
+
+ return self.newObject([e])
+
+ def threePointArc(self, point1, point2, forConstruction=False):
+ """
+ Draw an arc from the current point, through point1, and ending at point2
+
+ :param point1: point to draw through
+ :type point1: 2-tuple, in workplane coordinates
+ :param point2: end point for the arc
+ :type point2: 2-tuple, in workplane coordinates
+ :return: a workplane with the current point at the end of the arc
+
+ Future Enhancements:
+ provide a version that allows an arc using relative measures
+ provide a centerpoint arc
+ provide tangent arcs
+ """
+
+ gstartPoint = self._findFromPoint(False)
+ gpoint1 = self.plane.toWorldCoords(point1)
+ gpoint2 = self.plane.toWorldCoords(point2)
+
+ arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2)
+
+ if not forConstruction:
+ self._addPendingEdge(arc)
+
+ return self.newObject([arc])
+
+ def rotateAndCopy(self, matrix):
+ """
+ Makes a copy of all edges on the stack, rotates them according to the
+ provided matrix, and then attempts to consolidate them into a single wire.
+
+ :param matrix: a 4xr transformation matrix, in global coordinates
+ :type matrix: a FreeCAD Base.Matrix object
+ :return: a CadQuery object with consolidated wires, and any originals on the stack.
+
+ The most common use case is to create a set of open edges, and then mirror them
+ around either the X or Y axis to complete a closed shape.
+
+ see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes
+ see :py:meth:`mirrorX` and for an example
+
+ Future Enhancements:
+ faster implementation: this one transforms 3 times to accomplish the result
+ """
+
+ #convert edges to a wire, if there are pending edges
+ n = self.wire(forConstruction=False)
+
+ #attempt to consolidate wires together.
+ consolidated = n.consolidateWires()
+
+ 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()
+
+ return c
+
+ def mirrorY(self):
+ """
+ Mirror entities around the y axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier. This line of code::
+
+ s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25)
+
+ Produces a flat, heart shaped object
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+ """
+ tm = Matrix()
+ tm.rotateY(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def mirrorX(self):
+ """
+ Mirror entities around the x axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier.
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+ """
+ tm = Matrix()
+ tm.rotateX(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def _addPendingEdge(self, edge):
+ """
+ Queues an edge for later combination into a wire.
+
+ :param edge:
+ :return:
+ """
+ self.ctx.pendingEdges.append(edge)
+
+ if self.ctx.firstPoint is None:
+ self.ctx.firstPoint = self.plane.toLocalCoords(edge.startPoint())
+
+ def _addPendingWire(self, wire):
+ """
+ Queue a Wire for later extrusion
+
+ Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids.
+
+ but users do not normally care about these distinctions. Users 'think' in terms
+ of edges, and solids.
+
+ CadQuery tracks edges as they are drawn, and automatically combines them into wires
+ when the user does an operation that needs it.
+
+ Similarly, cadQuery tracks pending wires, and automatically combines them into faces
+ when necessary to make a solid.
+ """
+ self.ctx.pendingWires.append(wire)
+
+ def consolidateWires(self):
+ """
+ Attempt to consolidate wires on the stack into a single.
+ If possible, a new object with the results are returned.
+ if not possible, the wires remain separated
+
+ FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly sometimes
+ Additionally, it has a bug where a profile composed of two wires ( rather than one )
+ also does not work properly. Together these are a real problem.
+ """
+ wires = self.wires().vals()
+ if len(wires) < 2:
+ return self
+
+ #TODO: this makes the assumption that either all wires could be combined, or none.
+ #in reality trying each combination of wires is probably not reasonable anyway
+ w = Wire.combine(wires)
+
+ #ok this is a little tricky. if we consolidate wires, we have to actually
+ #modify the pendingWires collection to remove the original ones, and replace them
+ #with the consolidate done
+ #since we are already assuming that all wires could be consolidated, its easy, we just
+ #clear the pending wire list
+ r = self.newObject([w])
+ r.ctx.pendingWires = []
+ r._addPendingWire(w)
+ return r
+
+ def wire(self, forConstruction=False):
+ """
+ Returns a CQ object with all pending edges connected into a wire.
+
+ All edges on the stack that can be combined will be combined into a single wire object,
+ and other objects will remain on the stack unmodified
+
+ :param forConstruction: whether the wire should be used to make a solid, or if it is just
+ for reference
+ :type forConstruction: boolean. true if the object is only for reference
+
+ This method is primarily of use to plugin developers making utilities for 2-d construction.
+ This method should be called when a user operation implies that 2-d construction is
+ finished, and we are ready to begin working in 3d
+
+ SEE '2-d construction concepts' for a more detailed explanation of how CadQuery handles
+ edges, wires, etc
+
+ Any non edges will still remain.
+ """
+
+ edges = self.ctx.pendingEdges
+
+ #do not consolidate if there are no free edges
+ if len(edges) == 0:
+ return self
+
+ self.ctx.pendingEdges = []
+
+ others = []
+ for e in self.objects:
+ if type(e) != Edge:
+ others.append(e)
+
+
+ w = Wire.assembleEdges(edges)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject(others + [w])
+
+ def each(self, callBackFunction, useLocalCoordinates=False):
+ """
+ Runs the provided function on each value in the stack, and collects the return values into
+ a new CQ object.
+
+ Special note: a newly created workplane always has its center point as its only stack item
+
+ :param callBackFunction: the function to call for each item on the current stack.
+ :param useLocalCoordinates: should values be converted from local coordinates first?
+ :type useLocalCoordinates: boolean
+
+ The callback function must accept one argument, which is the item on the stack, and return
+ one object, which is collected. If the function returns None, nothing is added to the stack.
+ The object passed into the callBackFunction is potentially transformed to local coordinates,
+ if useLocalCoordinates is true
+
+ useLocalCoordinates is very useful for plugin developers.
+
+ If false, the callback function is assumed to be working in global coordinates. Objects
+ created are added as-is, and objects passed into the function are sent in using global
+ coordinates
+
+ If true, the calling function is assumed to be working in local coordinates. Objects are
+ transformed to local coordinates before they are passed into the callback method, and result
+ objects are transformed to global coordinates after they are returned.
+
+ This allows plugin developers to create objects in local coordinates, without worrying
+ about the fact that the working plane is different than the global coordinate system.
+
+
+ TODO: wrapper object for Wire will clean up forConstruction flag everywhere
+ """
+ results = []
+ for obj in self.objects:
+
+ if useLocalCoordinates:
+ #TODO: this needs to work for all types of objects, not just vectors!
+ r = callBackFunction(self.plane.toLocalCoords(obj))
+ r = r.transformShape(self.plane.rG)
+ else:
+ r = callBackFunction(obj)
+
+ if type(r) == Wire:
+ if not r.forConstruction:
+ self._addPendingWire(r)
+
+ results.append(r)
+
+ return self.newObject(results)
+
+ def eachpoint(self, callbackFunction, useLocalCoordinates=False):
+ """
+ Same as each(), except each item on the stack is converted into a point before it
+ is passed into the callback function.
+
+ :return: CadQuery object which contains a list of vectors (points ) on its stack.
+
+ :param useLocalCoordinates: should points be in local or global coordinates
+ :type useLocalCoordinates: boolean
+
+ The resulting object has a point on the stack for each object on the original stack.
+ Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
+ to a point by using their center of mass.
+
+ If the stack has zero length, a single point is returned, which is the center of the current
+ workplane/coordinate system
+ """
+ #convert stack to a list of points
+ pnts = []
+ if len(self.objects) == 0:
+ #nothing on the stack. here, we'll assume we should operate with the
+ #origin as the context point
+ pnts.append(self.plane.origin)
+ else:
+
+ for v in self.objects:
+ pnts.append(v.Center())
+
+ return self.newObject(pnts).each(callbackFunction, useLocalCoordinates)
+
+ def rect(self, xLen, yLen, centered=True, forConstruction=False):
+ """
+ Make a rectangle for each item on the stack.
+
+ :param xLen: length in xDirection ( in workplane coordinates )
+ :type xLen: float > 0
+ :param yLen: length in yDirection ( in workplane coordinates )
+ :type yLen: float > 0
+ :param boolean centered: true if the rect is centered on the reference point, false if the
+ lower-left is on the reference point
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part
+ geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole
+ pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+ """
+ def makeRectangleWire(pnt):
+ # Here pnt is in local coordinates due to useLocalCoords=True
+ # (xc,yc,zc) = pnt.toTuple()
+ if centered:
+ p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0))
+ p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0, 0))
+ p3 = pnt.add(Vector(xLen/2.0, yLen/2.0, 0))
+ p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0))
+ else:
+ p1 = pnt
+ p2 = pnt.add(Vector(xLen, 0, 0))
+ p3 = pnt.add(Vector(xLen, yLen, 0))
+ p4 = pnt.add(Vector(0, yLen, 0))
+
+ w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction)
+ return w
+ #return Part.makePolygon([p1,p2,p3,p4,p1])
+
+ return self.eachpoint(makeRectangleWire, True)
+
+ #circle from current point
+ def circle(self, radius, forConstruction=False):
+ """
+ Make a circle for each item on the stack.
+
+ :param radius: radius of the circle
+ :type radius: float > 0
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating
+ part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a
+ hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin. Another common case is
+ to use successive circle() calls to create concentric circles. This works because the
+ center of a circle is its reference point::
+
+ s = Workplane().circle(2.0).circle(1.0)
+
+ Creates two concentric circles, which when extruded will form a ring.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeCircleWire(obj):
+ cir = Wire.makeCircle(radius, obj, Vector(0, 0, 1))
+ cir.forConstruction = forConstruction
+ return cir
+
+ return self.eachpoint(makeCircleWire, useLocalCoordinates=True)
+
+ def polygon(self, nSides, diameter, forConstruction=False):
+ """
+ Creates a polygon inscribed in a circle of the specified diameter for each point on
+ the stack
+
+ The first vertex is always oriented in the x direction.
+
+ :param nSides: number of sides, must be > 3
+ :param diameter: the size of the circle the polygon is inscribed into
+ :return: a polygon wire
+ """
+ def _makePolygon(center):
+ #pnt is a vector in local coordinates
+ angle = 2.0 * math.pi / nSides
+ pnts = []
+ for i in range(nSides+1):
+ pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)),
+ (diameter / 2.0 * math.sin(angle*i)), 0))
+ return Wire.makePolygon(pnts, forConstruction)
+
+ return self.eachpoint(_makePolygon, True)
+
+ def polyline(self, listOfXYTuple, forConstruction=False):
+ """
+ Create a polyline from a list of points
+
+ :param listOfXYTuple: a list of points in Workplane coordinates
+ :type listOfXYTuple: list of 2-tuples
+ :param forConstruction: whether or not the edges are used for reference
+ :type forConstruction: true if the edges are for reference, false if they are for creating geometry
+ part geometry
+ :return: a new CQ object with a list of edges on the stack
+
+ *NOTE* most commonly, the resulting wire should be closed.
+ """
+
+ # Our list of new edges that will go into a new CQ object
+ edges = []
+
+ # The very first startPoint comes from our original object, but not after that
+ startPoint = self._findFromPoint(False)
+
+ # Draw a line for each set of points, starting from the from-point of the original CQ object
+ for curTuple in listOfXYTuple:
+ endPoint = self.plane.toWorldCoords(curTuple)
+
+ edges.append(Edge.makeLine(startPoint, endPoint))
+
+ # We need to move the start point for the next line that we draw or we get stuck at the same startPoint
+ startPoint = endPoint
+
+ if not forConstruction:
+ self._addPendingEdge(edges[-1])
+
+ return self.newObject(edges)
+
+ def close(self):
+ """
+ End 2-d construction, and attempt to build a closed wire.
+
+ :return: a CQ object with a completed wire on the stack, if possible.
+
+ After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary
+ to convert the edges produced by these into one or more wires.
+
+ When a set of edges is closed, cadQuery assumes it is safe to build the group of edges
+ into a wire. This example builds a simple triangular prism::
+
+ s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2)
+ """
+ self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
+
+ # Need to reset the first point after closing a wire
+ self.ctx.firstPoint=None
+
+ return self.wire()
+
+ def largestDimension(self):
+ """
+ Finds the largest dimension in the stack.
+ Used internally to create thru features, this is how you can compute
+ how long or wide a feature must be to make sure to cut through all of the material
+ :return: A value representing the largest dimension of the first solid on the stack
+ """
+ #TODO: this implementation is naive and returns the dims of the first solid... most of
+ #TODO: the time this works. but a stronger implementation would be to search all solids.
+ s = self.findSolid()
+ if s:
+ return s.BoundingBox().DiagonalLength * 5.0
+ else:
+ return -1
+
+ def cutEach(self, fcn, useLocalCoords=False, clean=True):
+ """
+ Evaluates the provided function at each point on the stack (ie, eachpoint)
+ and then cuts the result from the context solid.
+ :param fcn: a function suitable for use in the eachpoint method: ie, that accepts a vector
+ :param useLocalCoords: same as for :py:meth:`eachpoint`
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :return: a CQ object that contains the resulting solid
+ :raises: an error if there is not a context solid to cut from
+ """
+ ctxSolid = self.findSolid()
+ if ctxSolid is None:
+ raise ValueError("Must have a solid in the chain to cut from!")
+
+ #will contain all of the counterbores as a single compound
+ results = self.eachpoint(fcn, useLocalCoords).vals()
+ s = ctxSolid
+ for cb in results:
+ s = s.cut(cb)
+
+ if clean: s = s.clean()
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ #but parameter list is different so a simple function pointer wont work
+ def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True):
+ """
+ Makes a counterbored hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param cboreDiameter: the diameter of the cbore
+ :type cboreDiameter: float > 0 and > diameter
+ :param cboreDepth: depth of the counterbore
+ :type cboreDepth: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ The surface of the hole is at the current workplane plane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\
+ .rect(1.5,3.5,forConstruction=True)\
+ .vertices().cboreHole(0.125, 0.25,0.125,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite
+ time consuming to create, but are quite easily defined by users.
+
+ see :py:meth:`cskHole` to make countersinks instead of counterbores
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCbore(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0, 0, -1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates!
+
+ #add the counter bore
+ cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir)
+ r = hole.fuse(cbore)
+ return r
+
+ return self.cutEach(_makeCbore, True, clean)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True):
+ """
+ Makes a countersunk hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param cskDiameter: the diameter of the countersink
+ :type cskDiameter: float > 0 and > diameter
+ :param cskAngle: angle of the countersink, in degrees ( 82 is common )
+ :type cskAngle: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\
+ .rect(1.5,3.5,forConstruction=True)\
+ .vertices().cskHole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite
+ time consuming to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` to make counterbores instead of countersinks
+ """
+
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCsk(center):
+ #center is in local coordinates
+
+ boreDir = Vector(0, 0, -1)
+
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords!
+ r = cskDiameter / 2.0
+ h = r / math.tan(math.radians(cskAngle / 2.0))
+ csk = Solid.makeCone(r, 0.0, h, center, boreDir)
+ r = hole.fuse(csk)
+ return r
+
+ return self.cutEach(_makeCsk, True, clean)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def hole(self, diameter, depth=None, clean=True):
+ """
+ Makes a hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\
+ .rect(1.5,3.5,forConstruction=True)\
+ .vertices().hole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite
+ time consuming to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeHole(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0, 0, -1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates!
+ return hole
+
+ return self.cutEach(_makeHole, True, clean)
+
+ #TODO: duplicated code with _extrude and extrude
+ def twistExtrude(self, distance, angleDegrees, combine=True, clean=True):
+ """
+ Extrudes a wire in the direction normal to the plane, but also twists by the specified
+ angle over the length of the extrusion
+
+ The center point of the rotation will be the center of the workplane
+
+ See extrude for more details, since this method is the same except for the the addition
+ of the angle. In fact, if angle=0, the result is the same as a linear extrude.
+
+ **NOTE** This method can create complex calculations, so be careful using it with
+ complex geometries
+
+ :param distance: the distance to extrude normal to the workplane
+ :param angle: angline ( in degrees) to rotate through the extrusion
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :return: a CQ object with the resulting solid selected.
+ """
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
+
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+ #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
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there
+ # are multiple sets
+ r = None
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin,
+ eDir, angleDegrees)
+ if r is None:
+ r = thisObj
+ else:
+ r = r.fuse(thisObj)
+
+ if combine:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def extrude(self, distance, combine=True, clean=True, both=False):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic solid.
+
+ :param distance: the distance to extrude, normal to the workplane plane
+ :type distance: float, negative means opposite the normal direction
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :param boolean both: extrude in both directions symmetrically
+ :return: a CQ object with the resulting solid selected.
+
+ extrude always *adds* material to a part.
+
+ The returned object is always a CQ object, and depends on wither combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+
+ FutureEnhancement:
+ Support for non-prismatic extrusion ( IE, sweeping along a profile, not just
+ perpendicular to the plane extrude to surface. this is quite tricky since the surface
+ selected may not be planar
+ """
+ r = self._extrude(distance,both=both) # returns a Solid (or a compound if there were multiple)
+
+ if combine:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True):
+ """
+ Use all un-revolved wires in the parent chain to create a solid.
+
+ :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
+ :param combine: True to combine the resulting solid with parent solids if found.
+ :type combine: boolean, combine with parent solid
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :return: a CQ object with the resulting solid selected.
+
+ The returned object is always a CQ object, and depends on wither combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+ """
+ #Make sure we account for users specifying angles larger than 360 degrees
+ angleDegrees %= 360.0
+
+ #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve
+ angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees
+
+ # The default start point of the vector defining the axis of rotation will be the origin
+ # of the workplane
+ if axisStart is None:
+ axisStart = self.plane.toWorldCoords((0, 0)).toTuple()
+ else:
+ axisStart = self.plane.toWorldCoords(axisStart).toTuple()
+
+ # The default end point of the vector defining the axis of rotation should be along the
+ # normal from the plane
+ if axisEnd is None:
+ # Make sure we match the user's assumed axis of rotation if they specified an start
+ # but not an end
+ if axisStart[1] != 0:
+ axisEnd = self.plane.toWorldCoords((0, axisStart[1])).toTuple()
+ else:
+ axisEnd = self.plane.toWorldCoords((0, 1)).toTuple()
+ else:
+ axisEnd = self.plane.toWorldCoords(axisEnd).toTuple()
+
+ # returns a Solid (or a compound if there were multiple)
+ r = self._revolve(angleDegrees, axisStart, axisEnd)
+ if combine:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def sweep(self, path, makeSolid=True, isFrenet=False, combine=True, clean=True):
+ """
+ Use all un-extruded wires in the parent chain to create a swept solid.
+
+ :param path: A wire along which the pending wires will be swept
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :return: a CQ object with the resulting solid selected.
+ """
+
+ r = self._sweep(path.wire(), makeSolid, isFrenet) # returns a Solid (or a compound if there were multiple)
+ if combine:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def _combineWithBase(self, obj):
+ """
+ Combines the provided object with the base solid, if one can be found.
+ :param obj:
+ :return: a new object that represents the result of combining the base object with obj,
+ or obj if one could not be found
+ """
+ baseSolid = self.findSolid(searchParents=True)
+ r = obj
+ if baseSolid is not None:
+ r = baseSolid.fuse(obj)
+ baseSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def combine(self, clean=True):
+ """
+ Attempts to combine all of the items on the stack into a single item.
+ WARNING: all of the items must be of the same type!
+
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there are no items on the stack, or if they cannot be combined
+ :return: a CQ object with the resulting object selected
+ """
+ items = list(self.objects)
+ s = items.pop(0)
+ for ss in items:
+ s = s.fuse(ss)
+
+ if clean: s = s.clean()
+
+ return self.newObject([s])
+
+ def union(self, toUnion=None, combine=True, clean=True):
+ """
+ Unions all of the items on the stack of toUnion with the current solid.
+ If there is no current solid, the items in toUnion are unioned together.
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toUnion:
+ :type toUnion: a solid object, or a CQ object having a solid,
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to add to in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ #first collect all of the items together
+ if type(toUnion) == CQ or type(toUnion) == Workplane:
+ solids = toUnion.solids().vals()
+ if len(solids) < 1:
+ raise ValueError("CQ object must have at least one solid on the stack to union!")
+ newS = solids.pop(0)
+ for s in solids:
+ newS = newS.fuse(s)
+ elif type(toUnion) == Solid:
+ newS = toUnion
+ else:
+ raise ValueError("Cannot union type '{}'".format(type(toUnion)))
+
+ #now combine with existing solid, if there is one
+ # look for parents to cut from
+ solidRef = self.findSolid(searchStack=True, searchParents=True)
+ if combine and solidRef is not None:
+ r = solidRef.fuse(newS)
+ solidRef.wrapped = newS.wrapped
+ else:
+ r = newS
+
+ if clean: r = r.clean()
+
+ return self.newObject([r])
+
+ def cut(self, toCut, combine=True, clean=True):
+ """
+ Cuts the provided solid from the current solid, IE, perform a solid subtraction
+
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toCut: object to cut
+ :type toCut: a solid object, or a CQ object having a solid,
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ # look for parents to cut from
+ solidRef = self.findSolid(searchStack=True, searchParents=True)
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to cut from")
+ solidToCut = None
+ if type(toCut) == CQ or type(toCut) == Workplane:
+ solidToCut = toCut.val()
+ elif type(toCut) == Solid:
+ solidToCut = toCut
+ else:
+ raise ValueError("Cannot cut type '{}'".format(type(toCut)))
+
+ newS = solidRef.cut(solidToCut)
+
+ if clean: newS = newS.clean()
+
+ if combine:
+ solidRef.wrapped = newS.wrapped
+
+ return self.newObject([newS])
+
+ def intersect(self, toIntersect, combine=True, clean=True):
+ """
+ Intersects the provided solid from the current solid.
+
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toIntersect: object to intersect
+ :type toIntersect: a solid object, or a CQ object having a solid,
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to intersect with in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ # look for parents to intersect with
+ solidRef = self.findSolid(searchStack=True, searchParents=True)
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to intersect with")
+ solidToIntersect = None
+
+ if isinstance(toIntersect, CQ):
+ solidToIntersect = toIntersect.val()
+ elif isinstance(toIntersect, Solid):
+ solidToIntersect = toIntersect
+ else:
+ raise ValueError("Cannot intersect type '{}'".format(type(toIntersect)))
+
+ newS = solidRef.intersect(solidToIntersect)
+
+ if clean: newS = newS.clean()
+
+ if combine:
+ solidRef.wrapped = newS.wrapped
+
+ return self.newObject([newS])
+
+
+ def cutBlind(self, distanceToCut, clean=True):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material
+ from. cutBlind always removes material from a part.
+
+ :param distanceToCut: distance to extrude before cutting
+ :type distanceToCut: float, >0 means in the positive direction of the workplane normal,
+ <0 means in the negative direction
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutThruAll` to cut material from the entire part
+
+ Future Enhancements:
+ Cut Up to Surface
+ """
+ #first, make the object
+ toCut = self._extrude(distanceToCut)
+
+ #now find a solid in the chain
+
+ solidRef = self.findSolid()
+
+ s = solidRef.cut(toCut)
+
+ if clean: s = s.clean()
+
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def cutThruAll(self, positive=False, clean=True):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material
+ from. cutThruAll always removes material from a part.
+
+ :param boolean positive: True to cut in the positive direction, false to cut in the
+ negative direction
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutBlind` to cut material to a limited depth
+ """
+ maxDim = self.largestDimension()
+ if not positive:
+ maxDim *= (-1.0)
+
+ return self.cutBlind(maxDim, clean)
+
+ def loft(self, filled=True, ruled=False, combine=True):
+ """
+ Make a lofted solid, through the set of wires.
+ :return: a CQ object containing the created loft
+ """
+ wiresToLoft = self.ctx.pendingWires
+ self.ctx.pendingWires = []
+
+ r = Solid.makeLoft(wiresToLoft, ruled)
+
+ if combine:
+ parentSolid = self.findSolid(searchStack=False, searchParents=True)
+ if parentSolid is not None:
+ r = parentSolid.fuse(r)
+ parentSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def _extrude(self, distance, both=False):
+ """
+ Make a prismatic solid from the existing set of pending wires.
+
+ :param distance: distance to extrude
+ :param boolean both: extrude in both directions symmetrically
+ :return: a FreeCAD solid, suitable for boolean operations.
+
+ This method is a utility method, primarily for plugin and internal use.
+ It is the basis for cutBlind,extrude,cutThruAll, and all similar methods.
+
+ Future Enhancements:
+ extrude along a profile (sweep)
+ """
+
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ s = time.time()
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
+ #print "sorted wires in %d sec" % ( time.time() - s )
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+
+ #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
+
+ # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
+ # multiple sets
+
+ # IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse
+ # each item to another and save the result-- instead, you want to combine all of the new
+ # items into a compound, and fuse them together!!!
+ # r = None
+ # for ws in wireSets:
+ # thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ # if r is None:
+ # r = thisObj
+ # else:
+ # s = time.time()
+ # r = r.fuse(thisObj)
+ # print "Fused in %0.3f sec" % ( time.time() - s )
+ # return r
+
+ toFuse = []
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ toFuse.append(thisObj)
+
+ if both:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.))
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+ def _revolve(self, angleDegrees, axisStart, axisEnd):
+ """
+ Make a solid from the existing set of pending 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 FreeCAD solid, suitable for boolean operations.
+
+ This method is a utility method, primarily for plugin and internal use.
+ """
+ #We have to gather the wires to be revolved
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
+
+ #Mark that all of the wires have been used to create a revolution
+ self.ctx.pendingWires = []
+
+ #Revolve the wires, make a compound out of them and then fuse them
+ toFuse = []
+ for ws in wireSets:
+ thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+ def _sweep(self, path, makeSolid=True, isFrenet=False):
+ """
+ Makes a swept solid from an existing set of pending wires.
+
+ :param path: A wire along which the pending wires will be swept
+ :return:a FreeCAD solid, suitable for boolean operations
+ """
+
+ # group wires together into faces based on which ones are inside the others
+ # result is a list of lists
+ s = time.time()
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
+ # print "sorted wires in %d sec" % ( time.time() - s )
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ toFuse = []
+ for ws in wireSets:
+ thisObj = Solid.sweep(ws[0], ws[1:], path.val(), makeSolid, isFrenet)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+ def box(self, length, width, height, centered=(True, True, True), combine=True, clean=True):
+ """
+ Return a 3d box with specified dimensions for each object on the stack.
+
+ :param length: box size in X direction
+ :type length: float > 0
+ :param width: box size in Y direction
+ :type width: float > 0
+ :param height: box size in Z direction
+ :type height: float > 0
+ :param centered: should the box be centered, or should reference point be at the lower
+ bound of the range?
+ :param combine: should the results be combined with other solids on the stack
+ (and each other)?
+ :type combine: true to combine shapes, false otherwise.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ Centered is a tuple that describes whether the box should be centered on the x,y, and
+ z axes. If true, the box is centered on the respective axis relative to the workplane
+ origin, if false, the workplane center will represent the lower bound of the resulting box
+
+ one box is created for each item on the current stack. If no items are on the stack, one box
+ using the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ if a solid was found in the chain, the result is that solid with all boxes produced
+ fused onto it otherwise, the result is the combination of all the produced boxes
+
+ if combine is false, the result will be a list of the boxes produced
+
+ Most often boxes form the basis for a part::
+
+ #make a single box with lower left corner at origin
+ s = Workplane().box(1,2,3,centered=(False,False,False)
+
+ But sometimes it is useful to create an array of them:
+
+ #create 4 small square bumps on a larger base plate:
+ s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
+ .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
+
+ """
+
+ def _makebox(pnt):
+
+ #(xp,yp,zp) = self.plane.toLocalCoords(pnt)
+ (xp, yp, zp) = pnt.toTuple()
+ if centered[0]:
+ xp -= (length / 2.0)
+ if centered[1]:
+ yp -= (width / 2.0)
+ if centered[2]:
+ zp -= (height / 2.0)
+
+ return Solid.makeBox(length, width, height, Vector(xp, yp, zp))
+
+ boxes = self.eachpoint(_makebox, True)
+
+ #if combination is not desired, just return the created boxes
+ if not combine:
+ return boxes
+ else:
+ #combine everything
+ return self.union(boxes, clean=clean)
+
+ def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360,
+ centered=(True, True, True), combine=True, clean=True):
+ """
+ Returns a 3D sphere with the specified radius for each point on the stack
+
+ :param radius: The radius of the sphere
+ :type radius: float > 0
+ :param direct: The direction axis for the creation of the sphere
+ :type direct: A three-tuple
+ :param angle1: The first angle to sweep the sphere arc through
+ :type angle1: float > 0
+ :param angle2: The second angle to sweep the sphere arc through
+ :type angle2: float > 0
+ :param angle3: The third angle to sweep the sphere arc through
+ :type angle3: float > 0
+ :param centered: A three-tuple of booleans that determines whether the sphere is centered
+ on each axis origin
+ :param combine: Whether the results should be combined with other solids on the stack
+ (and each other)
+ :type combine: true to combine shapes, false otherwise
+ :return: A sphere object for each point on the stack
+
+ Centered is a tuple that describes whether the sphere should be centered on the x,y, and
+ z axes. If true, the sphere is centered on the respective axis relative to the workplane
+ origin, if false, the workplane center will represent the lower bound of the resulting
+ sphere.
+
+ One sphere is created for each item on the current stack. If no items are on the stack, one
+ box using the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ If a solid was found in the chain, the result is that solid with all spheres produced
+ fused onto it otherwise, the result is the combination of all the produced boxes
+
+ If combine is false, the result will be a list of the spheres produced
+ """
+
+ # Convert the direction tuple to a vector, if needed
+ if isinstance(direct, tuple):
+ direct = Vector(direct)
+
+ def _makesphere(pnt):
+ """
+ Inner function that is used to create a sphere for each point/object on the workplane
+ :param pnt: The center point for the sphere
+ :return: A CQ Solid object representing a sphere
+ """
+ (xp, yp, zp) = pnt.toTuple()
+
+ if not centered[0]:
+ xp += radius
+
+ if not centered[1]:
+ yp += radius
+
+ if not centered[2]:
+ zp += radius
+
+ return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3)
+
+ # We want a sphere for each point on the workplane
+ spheres = self.eachpoint(_makesphere, True)
+
+ # If we don't need to combine everything, just return the created spheres
+ if not combine:
+ return spheres
+ else:
+ return self.union(spheres, clean=clean)
+
+ def clean(self):
+ """
+ Cleans the current solid by removing unwanted edges from the
+ faces.
+
+ Normally you don't have to call this function. It is
+ automatically called after each related operation. You can
+ disable this behavior with `clean=False` parameter if method
+ has any. In some cases this can improve performance
+ drastically but is generally dis-advised since it may break
+ some operations such as fillet.
+
+ Note that in some cases where lots of solid operations are
+ chained, `clean()` may actually improve performance since
+ the shape is 'simplified' at each step and thus next operation
+ is easier.
+
+ Also note that, due to limitation of the underlying engine,
+ `clean` may fail to produce a clean output in some cases such as
+ spherical faces.
+ """
+ try:
+ cleanObjects = [obj.clean() for obj in self.objects]
+ except AttributeError:
+ raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType())
+ return self.newObject(cleanObjects)
diff --git a/Libs/cadquery/cadquery/cq_directive.py b/Libs/cadquery/cadquery/cq_directive.py
new file mode 100644
index 0000000..11de3b5
--- /dev/null
+++ b/Libs/cadquery/cadquery/cq_directive.py
@@ -0,0 +1,85 @@
+"""
+A special directive for including a cq object.
+
+"""
+
+import traceback
+from cadquery import *
+from cadquery import cqgi
+import io
+from docutils.parsers.rst import directives
+
+template = """
+
+.. raw:: html
+
+
+ %(out_svg)s
+
+
+
+
+"""
+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
+ out_svg = "Your Script Did not assign call build_output() function!"
+
+ try:
+ _s = io.StringIO()
+ result = cqgi.parse(plot_code).build()
+
+ if result.success:
+ exporters.exportShape(result.first_result.shape, "SVG", _s)
+ out_svg = _s.getvalue()
+ else:
+ raise result.exception
+
+ except Exception:
+ traceback.print_exc()
+ out_svg = traceback.format_exc()
+
+ # now out
+ # Now start generating the lines of output
+ lines = []
+
+ # get rid of new lines
+ out_svg = out_svg.replace('\n', '')
+
+ txt_align = "left"
+ if "align" in options:
+ txt_align = 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)
diff --git a/Libs/cadquery/cadquery/cqgi.py b/Libs/cadquery/cadquery/cqgi.py
new file mode 100644
index 0000000..955b02d
--- /dev/null
+++ b/Libs/cadquery/cadquery/cqgi.py
@@ -0,0 +1,482 @@
+"""
+The CadQuery Gateway Interface.
+Provides classes and tools for executing CadQuery scripts
+"""
+import ast
+import traceback
+import time
+import cadquery
+
+CQSCRIPT = ""
+
+def parse(script_source):
+ """
+ Parses the script as a model, and returns a model.
+
+ If you would prefer to access the underlying model without building it,
+ for example, to inspect its available parameters, construct a CQModel object.
+
+ :param script_source: the script to run. Must be a valid cadquery script
+ :return: a CQModel object that defines the script and allows execution
+
+ """
+ model = CQModel(script_source)
+ return model
+
+
+class CQModel(object):
+ """
+ Represents a Cadquery Script.
+
+ After construction, the metadata property contains
+ a ScriptMetaData object, which describes the model in more detail,
+ and can be used to retrive the parameters defined by the model.
+
+ the build method can be used to generate a 3d model
+ """
+ def __init__(self, script_source):
+ """
+ Create an object by parsing the supplied python script.
+ :param script_source: a python script to parse
+ """
+ self.metadata = ScriptMetadata()
+ self.ast_tree = ast.parse(script_source, CQSCRIPT)
+ self.script_source = script_source
+ self._find_vars()
+
+ # TODO: pick up other scirpt metadata:
+ # describe
+ # pick up validation methods
+ self._find_descriptions()
+
+ def _find_vars(self):
+ """
+ Parse the script, and populate variables that appear to be
+ overridable.
+ """
+ #assumption here: we assume that variable declarations
+ #are only at the top level of the script. IE, we'll ignore any
+ #variable definitions at lower levels of the script
+
+ #we dont want to use the visit interface because here we excplicitly
+ #want to walk only the top level of the tree.
+ assignment_finder = ConstantAssignmentFinder(self.metadata)
+
+ for node in self.ast_tree.body:
+ if isinstance(node, ast.Assign):
+ assignment_finder.visit_Assign(node)
+
+ def _find_descriptions(self):
+ description_finder = ParameterDescriptionFinder(self.metadata)
+ description_finder.visit(self.ast_tree)
+
+ def validate(self, params):
+ """
+ Determine if the supplied parameters are valid.
+ NOT IMPLEMENTED YET-- raises NotImplementedError
+ :param params: a dictionary of parameters
+
+ """
+ raise NotImplementedError("not yet implemented")
+
+ def build(self, build_parameters=None, build_options=None):
+ """
+ Executes the script, using the optional parameters to override those in the model
+ :param build_parameters: a dictionary of variables. The variables must be
+ assignable to the underlying variable type. These variables override default values in the script
+ :param build_options: build options for how to build the model. Build options include things like
+ timeouts, tesselation tolerances, etc
+ :raises: Nothing. If there is an exception, it will be on the exception property of the result.
+ This is the interface so that we can return other information on the result, such as the build time
+ :return: a BuildResult object, which includes the status of the result, and either
+ a resulting shape or an exception
+ """
+ if not build_parameters:
+ build_parameters = {}
+
+ start = time.clock()
+ result = BuildResult()
+
+ try:
+ self.set_param_values(build_parameters)
+ collector = ScriptCallback()
+ env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
+ .add_entry("show_object", collector.show_object) \
+ .add_entry("debug", collector.debug) \
+ .add_entry("describe_parameter",collector.describe_parameter) \
+ .build()
+
+ c = compile(self.ast_tree, CQSCRIPT, 'exec')
+ exec (c, env)
+ result.set_debug(collector.debugObjects )
+ result.set_success_result(collector.outputObjects)
+
+ except Exception as ex:
+ #print "Error Executing Script:"
+ result.set_failure_result(ex)
+ #traceback.print_exc()
+ #print "Full Text of Script:"
+ #print self.script_source
+
+ end = time.clock()
+ result.buildTime = end - start
+ return result
+
+ def set_param_values(self, params):
+ model_parameters = self.metadata.parameters
+
+ for k, v in params.items():
+ if k not in model_parameters:
+ raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
+
+ p = model_parameters[k]
+ p.set_value(v)
+
+
+class ShapeResult(object):
+ """
+ An object created by a build, including the user parameters provided
+ """
+ def __init__(self):
+ self.shape = None
+ self.options = None
+
+class BuildResult(object):
+ """
+ The result of executing a CadQuery script.
+ The success property contains whether the exeuction was successful.
+
+ If successful, the results property contains a list of all results,
+ and the first_result property contains the first result.
+
+ If unsuccessful, the exception property contains a reference to
+ the stack trace that occurred.
+ """
+ def __init__(self):
+ self.buildTime = None
+ self.results = [] #list of ShapeResult
+ self.debugObjects = [] #list of ShapeResult
+ self.first_result = None
+ self.success = False
+ self.exception = None
+
+ def set_failure_result(self, ex):
+ self.exception = ex
+ self.success = False
+
+ def set_debug(self, debugObjects):
+ self.debugObjects = debugObjects
+
+ def set_success_result(self, results):
+ self.results = results
+ if len(self.results) > 0:
+ self.first_result = self.results[0]
+ self.success = True
+
+
+class ScriptMetadata(object):
+ """
+ Defines the metadata for a parsed CQ Script.
+ the parameters property is a dict of InputParameter objects.
+ """
+ def __init__(self):
+ self.parameters = {}
+
+ def add_script_parameter(self, p):
+ self.parameters[p.name] = p
+
+ def add_parameter_description(self,name,description):
+ #print 'Adding Parameter name=%s, desc=%s' % ( name, description )
+ p = self.parameters[name]
+ p.desc = description
+
+
+class ParameterType(object):
+ pass
+
+
+class NumberParameterType(ParameterType):
+ pass
+
+
+class StringParameterType(ParameterType):
+ pass
+
+
+class BooleanParameterType(ParameterType):
+ pass
+
+
+class InputParameter:
+ """
+ Defines a parameter that can be supplied when the model is executed.
+
+ Name, varType, and default_value are always available, because they are computed
+ from a variable assignment line of code:
+
+ The others are only available if the script has used define_parameter() to
+ provide additional metadata
+
+ """
+ def __init__(self):
+
+ #: the default value for the variable.
+ self.default_value = None
+
+ #: the name of the parameter.
+ self.name = None
+
+ #: type of the variable: BooleanParameter, StringParameter, NumericParameter
+ self.varType = None
+
+ #: help text describing the variable. Only available if the script used describe_parameter()
+ self.desc = None
+
+ #: valid values for the variable. Only available if the script used describe_parameter()
+ self.valid_values = []
+
+ self.ast_node = None
+
+ @staticmethod
+ def create(ast_node, var_name, var_type, default_value, valid_values=None, desc=None):
+
+ if valid_values is None:
+ valid_values = []
+
+ p = InputParameter()
+ p.ast_node = ast_node
+ p.default_value = default_value
+ p.name = var_name
+ p.desc = desc
+ p.varType = var_type
+ p.valid_values = valid_values
+ return p
+
+ def set_value(self, new_value):
+
+ if len(self.valid_values) > 0 and new_value not in self.valid_values:
+ raise InvalidParameterError(
+ "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
+ .format(str(new_value), self.name, str(self.valid_values)))
+
+ if self.varType == NumberParameterType:
+ try:
+ # Sometimes a value must stay as an int for the script to work properly
+ if isinstance(new_value, int):
+ f = int(new_value)
+ else:
+ f = float(new_value)
+
+ self.ast_node.n = f
+ except ValueError:
+ raise InvalidParameterError(
+ "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
+ .format(str(new_value), self.name))
+
+ elif self.varType == StringParameterType:
+ self.ast_node.s = str(new_value)
+ elif self.varType == BooleanParameterType:
+ if new_value:
+ self.ast_node.id = 'True'
+ else:
+ self.ast_node.id = 'False'
+ else:
+ raise ValueError("Unknown Type of var: ", str(self.varType))
+
+ def __str__(self):
+ return "InputParameter: {name=%s, type=%s, defaultValue=%s" % (
+ self.name, str(self.varType), str(self.default_value))
+
+
+class ScriptCallback(object):
+ """
+ Allows a script to communicate with the container
+ the show_object() method is exposed to CQ scripts, to allow them
+ to return objects to the execution environment
+ """
+ def __init__(self):
+ self.outputObjects = []
+ self.debugObjects = []
+
+ def show_object(self, shape,options={}):
+ """
+ return an object to the executing environment, with options
+ :param shape: a cadquery object
+ :param options: a dictionary of options that will be made available to the executing envrionment
+ """
+ o = ShapeResult()
+ o.options=options
+ o.shape = shape
+ self.outputObjects.append(o)
+
+ def debug(self,obj,args={}):
+ """
+ Debug print/output an object, with optional arguments.
+ """
+ s = ShapeResult()
+ s.shape = obj
+ s.options = args
+ self.debugObjects.append(s)
+
+ def describe_parameter(self,var_data ):
+ """
+ Do Nothing-- we parsed the ast ahead of exection to get what we need.
+ """
+ pass
+
+ def add_error(self, param, field_list):
+ """
+ Not implemented yet: allows scripts to indicate that there are problems with inputs
+ """
+ pass
+
+ def has_results(self):
+ return len(self.outputObjects) > 0
+
+
+
+class InvalidParameterError(Exception):
+ """
+ Raised when an attempt is made to provide a new parameter value
+ that cannot be assigned to the model
+ """
+ pass
+
+
+class NoOutputError(Exception):
+ """
+ Raised when the script does not execute the show_object() method to
+ return a solid
+ """
+ pass
+
+
+class ScriptExecutionError(Exception):
+ """
+ Represents a script syntax error.
+ Useful for helping clients pinpoint issues with the script
+ interactively
+ """
+
+ def __init__(self, line=None, message=None):
+ if line is None:
+ self.line = 0
+ else:
+ self.line = line
+
+ if message is None:
+ self.message = "Unknown Script Error"
+ else:
+ self.message = message
+
+ def full_message(self):
+ return self.__repr__()
+
+ def __str__(self):
+ return self.__repr__()
+
+ def __repr__(self):
+ return "ScriptError [Line %s]: %s" % (self.line, self.message)
+
+
+class EnvironmentBuilder(object):
+ """
+ Builds an execution environment for a cadquery script.
+ The environment includes the builtins, as well as
+ the other methods the script will need.
+ """
+ def __init__(self):
+ self.env = {}
+
+ def with_real_builtins(self):
+ return self.with_builtins(__builtins__)
+
+ def with_builtins(self, env_dict):
+ self.env['__builtins__'] = env_dict
+ return self
+
+ def with_cadquery_objects(self):
+ self.env['cadquery'] = cadquery
+ self.env['cq'] = cadquery
+ return self
+
+ def add_entry(self, name, value):
+ self.env[name] = value
+ return self
+
+ def build(self):
+ return self.env
+
+class ParameterDescriptionFinder(ast.NodeTransformer):
+ """
+ Visits a parse tree, looking for function calls to describe_parameter(var, description )
+ """
+ def __init__(self, cq_model):
+ self.cqModel = cq_model
+
+ def visit_Call(self,node):
+ """
+ Called when we see a function call. Is it describe_parameter?
+ """
+ try:
+ if node.func.id == 'describe_parameter':
+ #looks like we have a call to our function.
+ #first parameter is the variable,
+ #second is the description
+ varname = node.args[0].id
+ desc = node.args[1].s
+ self.cqModel.add_parameter_description(varname,desc)
+
+ except:
+ #print "Unable to handle function call"
+ pass
+ return node
+
+class ConstantAssignmentFinder(ast.NodeTransformer):
+ """
+ Visits a parse tree, and adds script parameters to the cqModel
+ """
+
+ def __init__(self, cq_model):
+ self.cqModel = cq_model
+
+ def handle_assignment(self, var_name, value_node):
+ try:
+
+ if type(value_node) == ast.Num:
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
+ elif type(value_node) == ast.Str:
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
+ elif type(value_node == ast.Name):
+ if value_node.id == 'True':
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, BooleanParameterType, True))
+ elif value_node.id == 'False':
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, BooleanParameterType, True))
+ except:
+ print("Unable to handle assignment for variable '%s'" % var_name)
+ pass
+
+ def visit_Assign(self, node):
+
+ try:
+ left_side = node.targets[0]
+
+ #do not handle attribute assignments
+ if isinstance(left_side,ast.Attribute):
+ return
+
+ if type(node.value) in [ast.Num, ast.Str, ast.Name]:
+ self.handle_assignment(left_side.id, node.value)
+ elif type(node.value) == ast.Tuple:
+ # we have a multi-value assignment
+ for n, v in zip(left_side.elts, node.value.elts):
+ self.handle_assignment(n.id, v)
+ except:
+ traceback.print_exc()
+ print("Unable to handle assignment for node '%s'" % ast.dump(left_side))
+
+ return node
diff --git a/Libs/cadquery/cadquery/freecad_impl/README.txt b/Libs/cadquery/cadquery/freecad_impl/README.txt
new file mode 100644
index 0000000..34ea788
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/README.txt
@@ -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
\ No newline at end of file
diff --git a/Libs/cadquery/cadquery/freecad_impl/__init__.py b/Libs/cadquery/cadquery/freecad_impl/__init__.py
new file mode 100644
index 0000000..2d2cbe8
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/__init__.py
@@ -0,0 +1,144 @@
+"""
+ Copyright (C) 2011-2015 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
+"""
+import os
+import sys
+
+
+#FreeCAD has crudified the stdout stream with a bunch of STEP output
+#garbage
+#this cleans it up
+#so it is possible to use stdout for object output
+class suppress_stdout_stderr(object):
+ '''
+ A context manager for doing a "deep suppression" of stdout and stderr in
+ Python, i.e. will suppress all print, even if the print originates in a
+ compiled C/Fortran sub-function.
+ This will not suppress raised exceptions, since exceptions are printed
+ to stderr just before a script exits, and after the context manager has
+ exited (at least, I think that is why it lets exceptions through).
+
+ '''
+ def __init__(self):
+ # Open a pair of null files
+ self.null_fds = [os.open(os.devnull,os.O_RDWR) for x in range(2)]
+ # Save the actual stdout (1) and stderr (2) file descriptors.
+ self.save_fds = [os.dup(1), os.dup(2)]
+
+ def __enter__(self):
+ # Assign the null pointers to stdout and stderr.
+ os.dup2(self.null_fds[0],1)
+ os.dup2(self.null_fds[1],2)
+
+ def __exit__(self, *_):
+ # Re-assign the real stdout/stderr back to (1) and (2)
+ os.dup2(self.save_fds[0],1)
+ os.dup2(self.save_fds[1],2)
+ # Close all file descriptors
+ for fd in self.null_fds + self.save_fds:
+ os.close(fd)
+
+def _fc_path():
+ """Find FreeCAD"""
+ # Look for FREECAD_LIB env variable
+ _PATH = os.environ.get('FREECAD_LIB', '')
+ if _PATH and 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",
+ "/opt/freecad/lib/",
+ "/usr/bin/freecad/lib",
+ "/usr/lib/freecad-daily/lib",
+ "/usr/lib/freecad",
+ "/usr/lib64/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
+
+ elif sys.platform.startswith('darwin'):
+ # Assume we're dealing with a Mac
+ for _PATH in [
+ "/Applications/FreeCAD.app/Contents/lib",
+ os.path.join(os.path.expanduser("~"),
+ "Library/Application Support/FreeCAD/lib"),
+ ]:
+ if os.path.exists(_PATH):
+ return _PATH
+
+ raise ImportError('cadquery was unable to determine freecad library path')
+
+
+# Make sure that the correct FreeCAD path shows up in Python's system path
+with suppress_stdout_stderr():
+ try:
+ import FreeCAD
+ except ImportError:
+ path = _fc_path()
+ sys.path.insert(0, path)
+ import FreeCAD
+
+# logging
+from . import console_logging
diff --git a/Libs/cadquery/cadquery/freecad_impl/console_logging.py b/Libs/cadquery/cadquery/freecad_impl/console_logging.py
new file mode 100644
index 0000000..ada75e0
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/console_logging.py
@@ -0,0 +1,111 @@
+import sys
+import logging
+
+import FreeCAD
+
+# Logging Handler
+# Retain a reference to the logging handler so it may be removed on requeset.
+# Also to prevent 2 handlers being added
+_logging_handler = None
+
+# FreeCAD Logging Handler
+class FreeCADConsoleHandler(logging.Handler):
+ """logging.Handler class to output to FreeCAD's console"""
+
+ def __init__(self, *args, **kwargs):
+ super(FreeCADConsoleHandler, self).__init__(*args, **kwargs)
+
+ # Test for expected print functions
+ # (just check they exist, if they don't an exception will be raised)
+ FreeCAD.Console.PrintMessage
+ FreeCAD.Console.PrintWarning
+ FreeCAD.Console.PrintError
+
+ def emit(self, record):
+ log_text = self.format(record) + "\n"
+ if record.levelno >= logging.ERROR:
+ FreeCAD.Console.PrintError(log_text)
+ elif record.levelno >= logging.WARNING:
+ FreeCAD.Console.PrintWarning(log_text)
+ else:
+ FreeCAD.Console.PrintMessage(log_text)
+
+
+def enable(level=None, format="%(message)s"):
+ """
+ Enable python builtin logging, and output it somewhere you can see.
+ - FreeCAD Console, or
+ - STDOUT (if output to console fails, for whatever reason)
+
+ Any script can log to FreeCAD console with:
+
+ >>> import cadquery
+ >>> cadquery.freecad_impl.console_logging.enable()
+ >>> import logging
+ >>> log = logging.getLogger(__name__)
+ >>> log.debug("detailed info, not normally displayed")
+ >>> log.info("some information")
+ some information
+ >>> log.warning("some warning text") # orange text
+ some warning text
+ >>> log.error("an error message") # red text
+ an error message
+
+ logging only needs to be enabled once, somewhere in your codebase.
+ debug logging level can be set with:
+
+ >>> import cadquery
+ >>> import logging
+ >>> cadquery.freecad_impl.console_logging.enable(logging.DEBUG)
+ >>> log = logging.getLogger(__name__)
+ >>> log.debug("debug logs will now be displayed")
+ debug logs will now be displayed
+
+ :param level: logging level to display, one of logging.(DEBUG|INFO|WARNING|ERROR)
+ :param format: logging format to display (search for "python logging format" for details)
+ :return: the logging Handler instance in effect
+ """
+ global _logging_handler
+
+ # Set overall logging level (done even if handler has already been assigned)
+ root_logger = logging.getLogger()
+ if level is not None:
+ root_logger.setLevel(level)
+ elif _logging_handler is None:
+ # level is not specified, and ho handler has been added yet.
+ # assumption: user is enabling logging for the first time with no parameters.
+ # let's make it simple for them and default the level to logging.INFO
+ # (logging default level is logging.WARNING)
+ root_logger.setLevel(logging.INFO)
+
+ if _logging_handler is None:
+ # Determine which Handler class to use
+ try:
+ _logging_handler = FreeCADConsoleHandler()
+ except Exception as e:
+ raise
+ # Fall back to STDOUT output (better than nothing)
+ _logging_handler = logging.StreamHandler(sys.stdout)
+
+ # Configure and assign handler to root logger
+ _logging_handler.setLevel(logging.DEBUG)
+ root_logger.addHandler(_logging_handler)
+
+ # Set formatting (can be used to re-define logging format)
+ formatter = logging.Formatter(format)
+ _logging_handler.setFormatter(formatter)
+
+ return _logging_handler
+
+
+def disable():
+ """
+ Disables logging to FreeCAD console (or STDOUT).
+ Note, logging may be enabled by another imported module, so this isn't a
+ guarentee; this function undoes logging_enable(), nothing more.
+ """
+ global _logging_handler
+ if _logging_handler:
+ root_logger = logging.getLogger()
+ root_logger.handlers.remove(_logging_handler)
+ _logging_handler = None
diff --git a/Libs/cadquery/cadquery/freecad_impl/exporters.py b/Libs/cadquery/cadquery/freecad_impl/exporters.py
new file mode 100644
index 0000000..59d5176
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/exporters.py
@@ -0,0 +1,396 @@
+import cadquery
+
+import FreeCAD
+import Drawing
+
+import tempfile, os, io
+
+#weird syntax i know
+from ..freecad_impl import suppress_stdout_stderr
+
+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 = io.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.
+ #FreeCAD junks up stdout with a bunch of messages, so this context
+ #manager supresses that stuff in the case we're trying to write to stdout
+ os.close(h)
+ with suppress_stdout_stderr():
+ 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="unicode",xml_declaration=True)
+
+"""
+ 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 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 = "%s" % 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 = """
+
+"""
+
+PATHTEMPLATE="\t\t\t\n"
diff --git a/Libs/cadquery/cadquery/freecad_impl/geom.py b/Libs/cadquery/cadquery/freecad_impl/geom.py
new file mode 100644
index 0000000..b619857
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/geom.py
@@ -0,0 +1,666 @@
+"""
+ Copyright (C) 2011-2015 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
+"""
+
+import math
+import cadquery
+import FreeCAD
+import Part as FreeCADPart
+
+
+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:
+ * nothing (in which case the null vector is return)
+ * a FreeCAD vector
+ * a vector ( in which case it is copied )
+ * a 3-tuple
+ * three float values, x, y, and z
+ """
+ def __init__(self, *args):
+ if len(args) == 3:
+ fV = FreeCAD.Base.Vector(args[0], args[1], args[2])
+ elif len(args) == 1:
+ if isinstance(args[0], Vector):
+ fV = args[0].wrapped
+ elif isinstance(args[0], tuple):
+ fV = FreeCAD.Base.Vector(args[0][0], args[0][1], args[0][2])
+ elif isinstance(args[0], FreeCAD.Base.Vector):
+ fV = args[0]
+ else:
+ fV = args[0]
+ elif len(args) == 0:
+ fV = FreeCAD.Base.Vector(0, 0, 0)
+ else:
+ raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
+
+ self._wrapped = fV
+
+ @property
+ def x(self):
+ return self.wrapped.x
+
+ @x.setter
+ def x(self, value):
+ self.wrapped.x = value
+
+ @property
+ def y(self):
+ return self.wrapped.y
+
+ @y.setter
+ def y(self, value):
+ self.wrapped.y = value
+
+ @property
+ def z(self):
+ return self.wrapped.z
+
+ @z.setter
+ def z(self, value):
+ self.wrapped.z = value
+
+ @property
+ def Length(self):
+ return self.wrapped.Length
+
+ @property
+ def wrapped(self):
+ return self._wrapped
+
+ 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 Vector(self.wrapped.sub(v.wrapped))
+
+ def add(self, v):
+ return Vector(self.wrapped.add(v.wrapped))
+
+ def multiply(self, scale):
+ """Return a copy multiplied by the provided scalar"""
+ tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
+ return Vector(tmp_fc_vector.multiply(scale))
+
+ def normalized(self):
+ """Return a normalized version of this vector"""
+ tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
+ tmp_fc_vector.normalize()
+ return Vector(tmp_fc_vector)
+
+ def Center(self):
+ """Return the vector itself
+
+ 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 __add__(self, v):
+ return self.add(v)
+
+ def __sub__(self, v):
+ return self.sub(v)
+
+ def __repr__(self):
+ return self.wrapped.__repr__()
+
+ def __str__(self):
+ return self.wrapped.__str__()
+
+ def __ne__(self, other):
+ if isinstance(other, Vector):
+ return self.wrapped.__ne__(other.wrapped)
+ return False
+
+ def __eq__(self, other):
+ if isinstance(other, Vector):
+ return self.wrapped.__eq__(other.wrapped)
+ return False
+
+
+class Matrix:
+ """A 3d , 4x4 transformation matrix.
+
+ Used to move geometry in space.
+ """
+ def __init__(self, matrix=None):
+ if matrix is 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(object):
+ """A 2D coordinate system in space
+
+ A 2D coordinate system in space, with the x-y axes on the 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|ZX|XZ|YX|ZY|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
+ ZX +z +x +y
+ XZ +x +z -y
+ YX +y +x -z
+ ZY +z +y -x
+ 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(origin, (1, 0, 0), (0, 0, 1)),
+ 'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)),
+ 'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)),
+ 'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)),
+ 'YX': Plane(origin, (0, 1, 0), (0, 0, -1)),
+ 'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)),
+ 'front': Plane(origin, (1, 0, 0), (0, 0, 1)),
+ 'back': Plane(origin, (-1, 0, 0), (0, 0, -1)),
+ 'left': Plane(origin, (0, 0, 1), (-1, 0, 0)),
+ 'right': Plane(origin, (0, 0, -1), (1, 0, 0)),
+ 'top': Plane(origin, (1, 0, 0), (0, 1, 0)),
+ 'bottom': Plane(origin, (1, 0, 0), (0, -1, 0))
+ }
+
+ try:
+ return namedPlanes[stdName]
+ except KeyError:
+ raise ValueError('Supported names are {}'.format(
+ list(namedPlanes.keys())))
+
+ @classmethod
+ def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('XY', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
+ plane = Plane.named('YZ', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('ZX', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('XZ', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
+ plane = Plane.named('YX', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('ZY', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('front', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)):
+ plane = Plane.named('back', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('left', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)):
+ plane = Plane.named('right', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('top', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('bottom', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ 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.
+ """
+ normal = Vector(normal)
+ if (normal.Length == 0.0):
+ raise ValueError('normal should be non null')
+ self.zDir = normal.normalized()
+ xDir = Vector(xDir)
+ if (xDir.Length == 0.0):
+ raise ValueError('xDir should be non null')
+ self._setPlaneDir(xDir)
+
+ self.invZDir = self.zDir.multiply(-1.0)
+
+ self.origin = origin
+
+ @property
+ def origin(self):
+ return self._origin
+
+ @origin.setter
+ def origin(self, value):
+ self._origin = Vector(value)
+ self._calcTransforms()
+
+ def setOrigin2d(self, x, y):
+ """
+ Set a new origin in the plane itself
+
+ Set a new origin in the plane itself. 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. Both operations were relative to local coordinates of the
+ plane.
+ """
+ self.origin = self.toWorldCoords((x, y))
+
+ def isWireInside(self, baseWire, testWire):
+ """Determine if testWire is inside baseWire
+
+ 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
+ return bb == BoundBox.findOutsideBox2D(bb, tb)
+
+ 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, 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, cadquery.Shape):
+ return obj.transformShape(self.rG)
+ else:
+ raise ValueError(
+ "Don't know how to convert type {} to local coordinates".format(
+ 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 isinstance(tuplePoint, Vector):
+ v = tuplePoint
+ elif len(tuplePoint) == 2:
+ v = Vector(tuplePoint[0], tuplePoint[1], 0)
+ else:
+ v = Vector(tuplePoint)
+ 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
+
+ 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.
+ """
+ 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))
+
+ return Plane(self.origin, newXdir, newZdir)
+
+ def rotateShapes(self, listOfShapes, rotationMatrix):
+ """Rotate the listOfShapes by the supplied rotationMatrix
+
+ @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 coordinates.
+
+ resultWires = []
+ for w in listOfShapes:
+ mirrored = w.transformGeometry(rotationMatrix.wrapped)
+
+ # If the first vertex of the second wire is not coincident with the
+ # first or last vertices of the first wire we have to fix the wire
+ # so that it will mirror correctly.
+ if ((mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and
+ mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and
+ mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or
+ (mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and
+ mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and
+ mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z)):
+
+ resultWires.append(mirrored)
+ else:
+ # Make sure that our mirrored edges meet up and are ordered
+ # properly.
+ aEdges = w.wrapped.Edges
+ aEdges.extend(mirrored.wrapped.Edges)
+ comp = FreeCADPart.Compound(aEdges)
+ mirroredWire = comp.connectEdgesToWires(False).Wires[0]
+
+ resultWires.append(cadquery.Shape.cast(mirroredWire))
+
+ return resultWires
+
+ def _setPlaneDir(self, xDir):
+ """Set the vectors parallel to the plane, i.e. xDir and yDir"""
+ if (self.zDir.dot(xDir) > 1e-5):
+ raise ValueError('xDir must be parralel to the plane')
+ xDir = Vector(xDir)
+ self.xDir = xDir.normalized()
+ self.yDir = self.zDir.cross(self.xDir).normalized()
+
+ def _calcTransforms(self):
+ """Computes transformation matrices to convert between coordinates
+
+ Computes transformation matrices to convert between 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 translation and the rotation.
+ # the double-inverting is strange, and I don't 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 = self.origin.x
+ invR.A24 = self.origin.y
+ invR.A34 = self.origin.z
+
+ self.rG = invR
+ self.fG = invR.inverse()
+
+ def computeTransform(self, tMatrix):
+ """Computes the 2-d projection of the supplied matrix"""
+
+ return Matrix(self.fG.multiply(tMatrix.wrapped).multiply(self.rG))
+
+
+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 isinstance(obj, tuple):
+ tmp.add(obj[0], obj[1], obj[2])
+ elif isinstance(obj, Vector):
+ tmp.add(obj.fV)
+ elif isinstance(obj, BoundBox):
+ tmp.add(obj.wrapped)
+
+ return BoundBox(tmp)
+
+ @classmethod
+ def findOutsideBox2D(cls, b1, b2):
+ """Compares bounding boxes
+
+ 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
+ doesn't work correctly plus, there was all kinds of rounding error in
+ the built-in implementation i do not understand.
+ """
+ fc_bb1 = b1.wrapped
+ fc_bb2 = b2.wrapped
+ if (fc_bb1.XMin < fc_bb2.XMin and
+ fc_bb1.XMax > fc_bb2.XMax and
+ fc_bb1.YMin < fc_bb2.YMin and
+ fc_bb1.YMax > fc_bb2.YMax):
+ return b1
+
+ if (fc_bb2.XMin < fc_bb1.XMin and
+ fc_bb2.XMax > fc_bb1.XMax and
+ fc_bb2.YMin < fc_bb1.YMin and
+ fc_bb2.YMax > fc_bb1.YMax):
+ return b2
+
+ return None
+
+ def isInside(self, anotherBox):
+ """Is the provided bounding box inside this one?"""
+ return self.wrapped.isInside(anotherBox.wrapped)
diff --git a/Libs/cadquery/cadquery/freecad_impl/importers.py b/Libs/cadquery/cadquery/freecad_impl/importers.py
new file mode 100644
index 0000000..b9f3fce
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/importers.py
@@ -0,0 +1,71 @@
+
+import cadquery
+from .shapes import Shape
+
+import FreeCAD
+import Part
+import sys
+import os
+import urllib as urlreader
+import tempfile
+
+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.Workplane 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:
+ #print fileName
+ rshape = Part.read(fileName)
+
+ #Make sure that we extract all the solids
+ solids = []
+ for solid in rshape.Solids:
+ solids.append(Shape.cast(solid))
+
+ return cadquery.Workplane("XY").newObject(solids)
+ except:
+ raise ValueError("STEP File Could not be loaded")
+
+#Loads a STEP file from an URL into a CQ.Workplane object
+def importStepFromURL(url):
+ #Now read and return the shape
+ try:
+ webFile = urlreader.urlopen(url)
+ tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
+ tempFile.write(webFile.read())
+ webFile.close()
+ tempFile.close()
+
+ rshape = Part.read(tempFile.name)
+
+ #Make sure that we extract all the solids
+ solids = []
+ for solid in rshape.Solids:
+ solids.append(Shape.cast(solid))
+
+ return cadquery.Workplane("XY").newObject(solids)
+ except:
+ raise ValueError("STEP File from the URL: " + url + " Could not be loaded")
diff --git a/Libs/cadquery/cadquery/freecad_impl/shapes.py b/Libs/cadquery/cadquery/freecad_impl/shapes.py
new file mode 100644
index 0000000..6badc47
--- /dev/null
+++ b/Libs/cadquery/cadquery/freecad_impl/shapes.py
@@ -0,0 +1,1049 @@
+"""
+ Copyright (C) 2011-2015 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
+
+ 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 implementations
+
+ 2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
+ its docs are terrible. This allows us to provide 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 'forConstruction' 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 user friendly: we like to change those when necessary. 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
+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
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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, tolerance=0.1):
+ self.wrapped.tessellate(tolerance)
+ return BoundBox(self.wrapped.BoundBox)
+
+ def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
+ if mirrorPlane == "XY" or mirrorPlane== "YX":
+ mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 0, 1)
+ elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
+ mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 1, 0)
+ elif mirrorPlane == "YZ" or mirrorPlane == "ZY":
+ mirrorPlaneNormalVector = FreeCAD.Base.Vector(1, 0, 0)
+
+ if type(basePointVector) == tuple:
+ basePointVector = Vector(basePointVector)
+
+ return Shape.cast(self.wrapped.mirror(basePointVector.wrapped, mirrorPlaneNormalVector))
+
+ def Center(self):
+ # A Part.Shape object doesn't have the CenterOfMass function, but it's wrapped Solid(s) does
+ if isinstance(self.wrapped, FreeCADPart.Shape):
+ # If there are no Solids, we're probably dealing with a Face or something similar
+ if len(self.Solids()) == 0:
+ return Vector(self.wrapped.CenterOfMass)
+ elif len(self.Solids()) == 1:
+ return Vector(self.Solids()[0].wrapped.CenterOfMass)
+ elif len(self.Solids()) > 1:
+ return self.CombinedCenter(self.Solids())
+ elif isinstance(self.wrapped, FreeCADPart.Solid):
+ return Vector(self.wrapped.CenterOfMass)
+ else:
+ raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped)))
+
+ def CenterOfBoundBox(self, tolerance = 0.1):
+ self.wrapped.tessellate(tolerance)
+ if isinstance(self.wrapped, FreeCADPart.Shape):
+ # If there are no Solids, we're probably dealing with a Face or something similar
+ if len(self.Solids()) == 0:
+ return Vector(self.wrapped.BoundBox.Center)
+ elif len(self.Solids()) == 1:
+ return Vector(self.Solids()[0].wrapped.BoundBox.Center)
+ elif len(self.Solids()) > 1:
+ return self.CombinedCenterOfBoundBox(self.Solids())
+ elif isinstance(self.wrapped, FreeCADPart.Solid):
+ return Vector(self.wrapped.BoundBox.Center)
+ else:
+ raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(type(self.Solids()[0].wrapped)))
+
+ @staticmethod
+ def CombinedCenter(objects):
+ """
+ Calculates the center of mass of multiple objects.
+
+ :param objects: a list of objects with mass
+ """
+ total_mass = sum(Shape.computeMass(o) for o in objects)
+ weighted_centers = [o.wrapped.CenterOfMass.multiply(Shape.computeMass(o)) for o in objects]
+
+ sum_wc = weighted_centers[0]
+ for wc in weighted_centers[1:] :
+ sum_wc = sum_wc.add(wc)
+
+ return Vector(sum_wc.multiply(1./total_mass))
+
+ @staticmethod
+ def computeMass(object):
+ """
+ Calculates the 'mass' of an object. in FreeCAD < 15, all objects had a mass.
+ in FreeCAD >=15, faces no longer have mass, but instead have area.
+ """
+ if object.wrapped.ShapeType == 'Face':
+ return object.wrapped.Area
+ else:
+ return object.wrapped.Mass
+
+ @staticmethod
+ def CombinedCenterOfBoundBox(objects, tolerance = 0.1):
+ """
+ Calculates the center of BoundBox of multiple objects.
+
+ :param objects: a list of objects with mass 1
+ """
+ total_mass = len(objects)
+
+ weighted_centers = []
+ for o in objects:
+ o.wrapped.tessellate(tolerance)
+ weighted_centers.append(o.wrapped.BoundBox.Center.multiply(1.0))
+
+ sum_wc = weighted_centers[0]
+ for wc in weighted_centers[1:] :
+ sum_wc = sum_wc.add(wc)
+
+ return Vector(sum_wc.multiply(1./total_mass))
+
+ 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):
+ """
+ A Single Point in Space
+ """
+
+ 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
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ 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):
+ """
+ A trimmed curve that represents the border of a face
+ """
+
+ def __init__(self, obj):
+ """
+ An Edge
+ """
+ self.wrapped = obj
+ # self.startPoint = None
+ # self.endPoint = None
+
+ self.edgetypes = {
+ FreeCADPart.ArcOfCircle: 'ARC',
+ FreeCADPart.Circle: 'CIRCLE'
+ }
+
+ if hasattr(FreeCADPart,"LineSegment"):
+ #FreeCAD <= 0.16
+ self.edgetypes[FreeCADPart.LineSegment] = 'LINE'
+ else:
+ #FreeCAD >= 0.17
+ self.edgetypes[FreeCADPart.Line] = 'LINE'
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ def geomType(self):
+ t = type(self.wrapped.Curve)
+ if t in self.edgetypes:
+ 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 .valueAt(.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):
+ center = Vector(pnt)
+ normal = Vector(dir)
+ return Edge(FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped, 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):
+ """
+ A series of connected, ordered Edges, that typically bounds a Face
+ """
+
+ def __init__(self, obj):
+ """
+ A Wire
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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=0, lefthand=False, heightstyle=False):
+ """
+ Make a helix along +z axis
+ :param pitch: displacement of 1 turn measured along surface.
+ :param height: full length of helix surface (measured sraight along surface's face)
+ :param radius: starting radius of helix
+ :param angle: if > 0, conical surface is used instead of a cylindrical. (angle < 0 not supported)
+ :param lefthand: if True, helix direction is reversed
+ :param heightstyle: if True, pitch and height are measured parallel to z-axis
+ """
+ # FreeCAD doc: https://www.freecadweb.org/wiki/Part_API (search for makeHelix)
+ return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle, lefthand, heightstyle))
+
+ def clean(self):
+ """This method is not implemented yet."""
+ return self
+
+class Face(Shape):
+ """
+ a bounded surface that represents part of the boundary of a solid
+ """
+ def __init__(self, obj):
+
+ self.wrapped = obj
+
+ self.facetypes = {
+ # TODO: bezier,bspline etc
+ FreeCADPart.Plane: 'PLANE',
+ FreeCADPart.Sphere: 'SPHERE',
+ FreeCADPart.Cone: 'CONE'
+ }
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ def geomType(self):
+ t = type(self.wrapped.Surface)
+ if t in self.facetypes:
+ 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=(0, 0, 0), dir=(0, 0, 1)):
+ basePnt = Vector(basePnt)
+ dir = Vector(dir)
+ return Face(FreeCADPart.makePlane(length, width, basePnt.wrapped, dir.wrapped))
+
+ @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):
+ """
+ the outer boundary of a surface
+ """
+ def __init__(self, wrapped):
+ """
+ A Shell
+ """
+ self.wrapped = wrapped
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @classmethod
+ def makeShell(cls, listOfFaces):
+ return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
+
+
+class Solid(Shape):
+ """
+ a single solid
+ """
+ def __init__(self, obj):
+ """
+ A Solid
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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 in pnt with the dimensions (length,width,height)
+ By 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):
+ """
+ Make a cone with given radii and height
+ By 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, ruled=False):
+ """
+ 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, ruled))
+
+ @classmethod
+ def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
+ """
+ Make a wedge located in pnt
+ By default pnt=Vector(0,0,0) and dir=Vector(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, dir=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
+ """
+ Make a sphere with a given radius
+ By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360
+ """
+ return Shape.cast(FreeCADPart.makeSphere(radius, pnt.wrapped, dir.wrapped, 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 are:
+ (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
+ (4) 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)
+
+ @classmethod
+ def sweep(cls, outerWire, innerWires, path, makeSolid=True, isFrenet=False):
+ """
+ Attempt to sweep the list of wires into a prismatic solid along the provided path
+
+ :param outerWire: the outermost wire
+ :param innerWires: a list of inner wires
+ :param path: The wire to sweep the face resulting from the wires over
+ :return: a Solid object
+ """
+
+ # 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)
+ wire = FreeCADPart.Wire([path.wrapped])
+ result = wire.makePipeShell(freeCADWires, makeSolid, isFrenet)
+
+ 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 clean(self):
+ """Clean faces by removing splitter edges."""
+ r = self.wrapped.removeSplitter()
+ # removeSplitter() returns a generic Shape type, cast to actual type of object
+ r = FreeCADPart.cast_to_shape(r)
+ return Shape.cast(r)
+
+ 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 chamfer(self, length, length2, edgeList):
+ """
+ Chamfers the specified edges of this solid.
+ :param length: length > 0, the length (length) of the chamfer
+ :param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required.
+ :param edgeList: a list of Edge objects, which must belong to this solid
+ :return: Chamfered solid
+ """
+ nativeEdges = [e.wrapped for e in edgeList]
+ # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API
+ if length2:
+ return Shape.cast(self.wrapped.makeChamfer(length, length2, nativeEdges))
+ else:
+ return Shape.cast(self.wrapped.makeChamfer(length, 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):
+ """
+ a collection of disconnected solids
+ """
+
+ def __init__(self, obj):
+ """
+ An Edge
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ def Center(self):
+ return self.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)
+
+ def clean(self):
+ """This method is not implemented yet."""
+ return self
diff --git a/Libs/cadquery/cadquery/plugins/__init__.py b/Libs/cadquery/cadquery/plugins/__init__.py
new file mode 100644
index 0000000..3697b9f
--- /dev/null
+++ b/Libs/cadquery/cadquery/plugins/__init__.py
@@ -0,0 +1,18 @@
+"""
+ CadQuery
+ Copyright (C) 2015 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
+"""
diff --git a/Libs/cadquery/cadquery/selectors.py b/Libs/cadquery/cadquery/selectors.py
new file mode 100644
index 0000000..12ee24e
--- /dev/null
+++ b/Libs/cadquery/cadquery/selectors.py
@@ -0,0 +1,664 @@
+"""
+ Copyright (C) 2011-2015 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
+"""
+
+import re
+import math
+from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
+from collections import defaultdict
+from pyparsing import Literal,Word,nums,Optional,Combine,oneOf,upcaseTokens,\
+ CaselessLiteral,Group,infixNotation,opAssoc,Forward,\
+ ZeroOrMore,Keyword
+from functools import reduce
+
+
+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
+
+ def __and__(self, other):
+ return AndSelector(self, other)
+
+ def __add__(self, other):
+ return SumSelector(self, other)
+
+ def __sub__(self, other):
+ return SubtractSelector(self, other)
+
+ def __neg__(self):
+ return InverseSelector(self)
+
+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(Vector(*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 BoxSelector(Selector):
+ """
+ Selects objects inside the 3D box defined by 2 points.
+
+ If `boundingbox` is True only the objects that have their bounding
+ box inside the given box is selected. Otherwise only center point
+ of the object is tested.
+
+ Applicability: all types of shapes
+
+ Example::
+
+ CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
+ """
+ def __init__(self, point0, point1, boundingbox=False):
+ self.p0 = Vector(*point0)
+ self.p1 = Vector(*point1)
+ self.test_boundingbox = boundingbox
+
+ def filter(self, objectList):
+
+ result = []
+ x0, y0, z0 = self.p0.toTuple()
+ x1, y1, z1 = self.p1.toTuple()
+
+ def isInsideBox(p):
+ # using XOR for checking if x/y/z is in between regardless
+ # of order of x/y/z0 and x/y/z1
+ return ((p.x < x0) ^ (p.x < x1)) and \
+ ((p.y < y0) ^ (p.y < y1)) and \
+ ((p.z < z0) ^ (p.z < z1))
+
+ for o in objectList:
+ if self.test_boundingbox:
+ bb = o.BoundingBox()
+ if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \
+ isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)):
+ result.append(o)
+ else:
+ if isInsideBox(o.Center()):
+ result.append(o)
+
+ return result
+
+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" )
+
+ """
+ def __init__(self, vector, directionMax=True, tolerance=0.0001):
+ self.vector = vector
+ self.max = max
+ self.directionMax = directionMax
+ self.TOLERANCE = tolerance
+ def filter(self,objectList):
+
+ def distance(tShape):
+ return tShape.Center().dot(self.vector)
+
+ # import OrderedDict
+ from collections import OrderedDict
+ #make and distance to object dict
+ objectDict = {distance(el) : el for el in objectList}
+ #transform it into an ordered dict
+ objectDict = OrderedDict(sorted(list(objectDict.items()),
+ key=lambda x: x[0]))
+
+ # find out the max/min distance
+ if self.directionMax:
+ d = list(objectDict.keys())[-1]
+ else:
+ d = list(objectDict.keys())[0]
+
+ # return all objects at the max/min distance (within a tolerance)
+ return [o for o in objectList if abs(d - distance(o)) < self.TOLERANCE]
+
+class DirectionNthSelector(ParallelDirSelector):
+ """
+ Selects nth object parallel (or normal) to the specified direction
+ Used for faces and edges
+
+ Applicability:
+ Linear Edges
+ Planar Faces
+ """
+ def __init__(self, vector, n, directionMax=True, tolerance=0.0001):
+ self.direction = vector
+ self.max = max
+ self.directionMax = directionMax
+ self.TOLERANCE = tolerance
+ self.N = n
+
+ def filter(self,objectList):
+ #select first the objects that are normal/parallel to a given dir
+ objectList = super(DirectionNthSelector,self).filter(objectList)
+
+ def distance(tShape):
+ return tShape.Center().dot(self.direction)
+
+ #calculate how many digits of precision do we need
+ digits = int(1/self.TOLERANCE)
+
+ #make a distance to object dict
+ #this is one to many mapping so I am using a default dict with list
+ objectDict = defaultdict(list)
+ for el in objectList:
+ objectDict[round(distance(el),digits)].append(el)
+
+ # choose the Nth unique rounded distance
+ nth_distance = sorted(list(objectDict.keys()),
+ reverse=not self.directionMax)[self.N]
+
+ # map back to original objects and return
+ return objectDict[nth_distance]
+
+class BinarySelector(Selector):
+ """
+ Base class for selectors that operates with two other
+ selectors. Subclass must implement the :filterResults(): method.
+ """
+ def __init__(self, left, right):
+ self.left = left
+ self.right = right
+
+ def filter(self, objectList):
+ return self.filterResults(self.left.filter(objectList),
+ self.right.filter(objectList))
+
+ def filterResults(self, r_left, r_right):
+ raise NotImplementedError
+
+class AndSelector(BinarySelector):
+ """
+ Intersection selector. Returns objects that is selected by both selectors.
+ """
+ def filterResults(self, r_left, r_right):
+ # return intersection of lists
+ return list(set(r_left) & set(r_right))
+
+class SumSelector(BinarySelector):
+ """
+ Union selector. Returns the sum of two selectors results.
+ """
+ def filterResults(self, r_left, r_right):
+ # return the union (no duplicates) of lists
+ return list(set(r_left + r_right))
+
+class SubtractSelector(BinarySelector):
+ """
+ Difference selector. Substract results of a selector from another
+ selectors results.
+ """
+ def filterResults(self, r_left, r_right):
+ return list(set(r_left) - set(r_right))
+
+class InverseSelector(Selector):
+ """
+ Inverts the selection of given selector. In other words, selects
+ all objects that is not selected by given selector.
+ """
+ def __init__(self, selector):
+ self.selector = selector
+
+ def filter(self, objectList):
+ # note that Selector() selects everything
+ return SubtractSelector(Selector(), self.selector).filter(objectList)
+
+
+def _makeGrammar():
+ """
+ Define the simple string selector grammar using PyParsing
+ """
+
+ #float definition
+ point = Literal('.')
+ plusmin = Literal('+') | Literal('-')
+ number = Word(nums)
+ integer = Combine(Optional(plusmin) + number)
+ floatn = Combine(integer + Optional(point + Optional(number)))
+
+ #vector definition
+ lbracket = Literal('(')
+ rbracket = Literal(')')
+ comma = Literal(',')
+ vector = Combine(lbracket + floatn('x') + comma + \
+ floatn('y') + comma + floatn('z') + rbracket)
+
+ #direction definition
+ simple_dir = oneOf(['X','Y','Z','XY','XZ','YZ'])
+ direction = simple_dir('simple_dir') | vector('vector_dir')
+
+ #CQ type definition
+ cqtype = oneOf(['Plane','Cylinder','Sphere','Cone','Line','Circle','Arc'],
+ caseless=True)
+ cqtype = cqtype.setParseAction(upcaseTokens)
+
+ #type operator
+ type_op = Literal('%')
+
+ #direction operator
+ direction_op = oneOf(['>','<'])
+
+ #index definition
+ ix_number = Group(Optional('-')+Word(nums))
+ lsqbracket = Literal('[').suppress()
+ rsqbracket = Literal(']').suppress()
+
+ index = lsqbracket + ix_number('index') + rsqbracket
+
+ #other operators
+ other_op = oneOf(['|','#','+','-'])
+
+ #named view
+ named_view = oneOf(['front','back','left','right','top','bottom'])
+
+ return direction('only_dir') | \
+ (type_op('type_op') + cqtype('cq_type')) | \
+ (direction_op('dir_op') + direction('dir') + Optional(index)) | \
+ (other_op('other_op') + direction('dir')) | \
+ named_view('named_view')
+
+_grammar = _makeGrammar() #make a grammar instance
+
+class _SimpleStringSyntaxSelector(Selector):
+ """
+ This is a private class that converts a parseResults object into a simple
+ selector object
+ """
+ def __init__(self,parseResults):
+
+ #define all token to object mappings
+ self.axes = {
+ 'X': Vector(1,0,0),
+ 'Y': Vector(0,1,0),
+ 'Z': Vector(0,0,1),
+ 'XY': Vector(1,1,0),
+ 'YZ': Vector(0,1,1),
+ 'XZ': Vector(1,0,1)
+ }
+
+ self.namedViews = {
+ 'front' : (Vector(0,0,1),True),
+ 'back' : (Vector(0,0,1),False),
+ 'left' : (Vector(1,0,0),False),
+ 'right' : (Vector(1,0,0),True),
+ 'top' : (Vector(0,1,0),True),
+ 'bottom': (Vector(0,1,0),False)
+ }
+
+ self.operatorMinMax = {
+ '>' : True,
+ '<' : False,
+ '+' : True,
+ '-' : False
+ }
+
+ self.operator = {
+ '+' : DirectionSelector,
+ '-' : DirectionSelector,
+ '#' : PerpendicularDirSelector,
+ '|' : ParallelDirSelector}
+
+ self.parseResults = parseResults
+ self.mySelector = self._chooseSelector(parseResults)
+
+ def _chooseSelector(self,pr):
+ """
+ Sets up the underlying filters accordingly
+ """
+ if 'only_dir' in pr:
+ vec = self._getVector(pr)
+ return DirectionSelector(vec)
+
+ elif 'type_op' in pr:
+ return TypeSelector(pr.cq_type)
+
+ elif 'dir_op' in pr:
+ vec = self._getVector(pr)
+ minmax = self.operatorMinMax[pr.dir_op]
+
+ if 'index' in pr:
+ return DirectionNthSelector(vec,int(''.join(pr.index.asList())),minmax)
+ else:
+ return DirectionMinMaxSelector(vec,minmax)
+
+ elif 'other_op' in pr:
+ vec = self._getVector(pr)
+ return self.operator[pr.other_op](vec)
+
+ else:
+ args = self.namedViews[pr.named_view]
+ return DirectionMinMaxSelector(*args)
+
+ def _getVector(self,pr):
+ """
+ Translate parsed vector string into a CQ Vector
+ """
+ if 'vector_dir' in pr:
+ vec = pr.vector_dir
+ return Vector(float(vec.x),float(vec.y),float(vec.z))
+ else:
+ return self.axes[pr.simple_dir]
+
+ def filter(self,objectList):
+ """
+ selects minimum, maximum, positive or negative values relative to a direction
+ [+\|-\|<\|>\|] \
+ """
+ return self.mySelector.filter(objectList)
+
+def _makeExpressionGrammar(atom):
+ """
+ Define the complex string selector grammar using PyParsing (which supports
+ logical operations and nesting)
+ """
+
+ #define operators
+ and_op = Literal('and')
+ or_op = Literal('or')
+ delta_op = oneOf(['exc','except'])
+ not_op = Literal('not')
+
+ def atom_callback(res):
+ return _SimpleStringSyntaxSelector(res)
+
+ atom.setParseAction(atom_callback) #construct a simple selector from every matched
+
+ #define callback functions for all operations
+ def and_callback(res):
+ items = res.asList()[0][::2] #take every secend items, i.e. all operands
+ return reduce(AndSelector,items)
+
+ def or_callback(res):
+ items = res.asList()[0][::2] #take every secend items, i.e. all operands
+ return reduce(SumSelector,items)
+
+ def exc_callback(res):
+ items = res.asList()[0][::2] #take every secend items, i.e. all operands
+ return reduce(SubtractSelector,items)
+
+ def not_callback(res):
+ right = res.asList()[0][1] #take second item, i.e. the operand
+ return InverseSelector(right)
+
+ #construct the final grammar and set all the callbacks
+ expr = infixNotation(atom,
+ [(and_op,2,opAssoc.LEFT,and_callback),
+ (or_op,2,opAssoc.LEFT,or_callback),
+ (delta_op,2,opAssoc.LEFT,exc_callback),
+ (not_op,1,opAssoc.RIGHT,not_callback)])
+
+ return expr
+
+_expression_grammar = _makeExpressionGrammar(_grammar)
+
+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`` or ``(x,y,z)`` which defines an arbitrary direction
+
+ It is possible to combine simple selectors together using logical operations.
+ The following operations are suuported
+
+ :and:
+ Logical AND, e.g. >X and >Y
+ :or:
+ Logical OR, e.g. |X or |Y
+ :not:
+ Logical NOT, e.g. not #XY
+ :exc(ept):
+ Set difference (equivalent to AND NOT): |X exc >Z
+
+ Finally, it is also possible to use even more complex expressions with nesting
+ and arbitrary number of terms, e.g.
+
+ (not >X[0] and #XY) or >XY[0]
+
+ Selectors are a complex topic: see :ref:`selector_reference` for more information
+ """
+ def __init__(self,selectorString):
+ """
+ Feed the input string through the parser and construct an relevant complex selector object
+ """
+ self.selectorString = selectorString
+ parse_result = _expression_grammar.parseString(selectorString,
+ parseAll=True)
+ self.mySelector = parse_result.asList()[0]
+
+ def filter(self,objectList):
+ """
+ Filter give object list through th already constructed complex selector object
+ """
+ return self.mySelector.filter(objectList)
\ No newline at end of file
diff --git a/Libs/cadquery/changes.md b/Libs/cadquery/changes.md
new file mode 100644
index 0000000..50f998d
--- /dev/null
+++ b/Libs/cadquery/changes.md
@@ -0,0 +1,105 @@
+Changes
+=======
+
+
+v0.1
+-----
+ * Initial Version
+
+v0.1.6
+-----
+ * Added STEP import and supporting tests
+
+v0.1.7
+-----
+ * Added revolve operation and supporting tests
+ * Fixed minor documentation errors
+
+v0.1.8
+-----
+ * Added toFreecad() function as a convenience for val().wrapped
+ * Converted all examples to use toFreecad()
+ * Updated all version numbers that were missed before
+ * Fixed import issues in Windows caused by fc_import
+ * Added/fixed Mac OS support
+ * Improved STEP import
+ * Fixed bug in rotateAboutCenter that negated its effect on solids
+ * Added Travis config (thanks @krasin)
+ * Removed redundant workplane.py file left over from the PParts.com migration
+ * Fixed toWorldCoordinates bug in moveTo (thanks @xix-xeaon)
+ * Added new tests for 2D drawing functions
+ * Integrated Coveralls.io, with a badge in README.md
+ * Integrated version badge in README.md
+
+v0.2.0
+-----
+ * Fixed versioning to match the semantic versioning scheme
+ * Added license badge in changes.md
+ * Fixed Solid.makeSphere implementation
+ * Added CQ.sphere operation that mirrors CQ.box
+ * Updated copyright dates
+ * Cleaned up spelling and misc errors in docstrings
+ * Fixed FreeCAD import error on Arch Linux (thanks @moeb)
+ * Made FreeCAD import report import error instead of silently failing (thanks @moeb)
+ * Added ruled option for the loft operation (thanks @hyOzd)
+ * Fixed close() not working in planes other than XY (thanks @hyOzd)
+ * Added box selector with bounding box option (thanks @hyOzd)
+ * CQ.translate and CQ.rotate documentation fixes (thanks @hyOzd)
+ * Fixed centering of a sphere
+ * Increased test coverage
+ * Added a clean function to keep some operations from failing on solids that need simplified (thanks @hyOzd)
+ * Added a mention of the new Google Group to the readme
+
+v0.3.0
+-----
+ * Fixed a bug where clean() could not be called on appropriate objects other than solids (thanks @hyOzd) #108
+ * Implemented new selectors that allow existing selectors to be combined with arithmetic/boolean operations (thanks @hyOzd) #110
+ * Fixed a bug where only 1 random edge was returned with multiple min/max selector matches (thanks @hyOzd) #111
+ * Implemented the creation of a workplane from multiple co-planar faces (thanks @hyOzd) #113
+ * Fixed the operation of Center() when called on a compound with multiple solids
+ * Add the named planes ZX YX ZY to define different normals (thanks @galou) #115
+ * Code cleanup in accordance with PEP 8 (thanks @galou)
+ * Fixed a bug with the close function not resetting the first point of the context correctly (thanks @huskier)
+ * Fixed the findSolid function so that it handles compounds #107
+ * Changed the polyline function so that it adds edges to the stack instead of a wire #102
+ * Add the ability to find the center of the bounding box, rather than the center of mass (thanks @huskier) #122
+ * Changed normalize function to normalized to match OCC/PythonOCC nomenclature #124
+ * Added a label attribute to all freecad_impl.shapes so that they can have IDs attached to them #124
+
+v0.4.0
+------
+ * Added Documentation, which is available on dcowden.github.io/cadquery
+ * Added CQGI, an adapter API that standardizes use of cadquery from within structured execution environments
+ * Added ability to import STEP files from a web URL (thanks @huskier ) #128
+
+v0.4.1
+------
+ * Minor CQGI updates
+
+v0.5.0-stable
+------
+ * Configuring Travis to push to PyPI on version releases.
+
+v0.5.1
+------
+ * Mirroring fixes (thanks @huskier)
+ * Added a mirroring example (thanks @huskier)
+
+v0.5.2
+------
+ * Added the sweep operation #33
+
+v1.0.0
+------
+ * Added an option to do symmetric extrusion about the workplane (thanks @adam-urbanczyk)
+ * Extended selector syntax to include Nth selector and re-implemented selectors using pyparsing (thanks @adam-urbanczyk)
+ * Added logical operations to string selectors (thanks @adam-urbanczyk)
+ * Cleanup of README.md and changes.md (thanks @baoboa)
+ * Fixed bugs with toVector and Face 'Not Defined' errors (thanks @huskier)
+ * Refactor of the initialization code for PEP8 compliance and Python 3 compatibility (thanks @Peque)
+ * Making sure that the new pyparsing library dependency is handled properly (thanks @Peque)
+
+v1.1.0 (Unreleased)
+------
+ * Fixes and addition of graphical examples for selectors (thanks @adam-urbanczyk)
+ * Added intersect operation (thanks @fragmuffin)
diff --git a/Libs/cadquery/conda-py3-freecad.yml b/Libs/cadquery/conda-py3-freecad.yml
new file mode 100644
index 0000000..9c377e0
--- /dev/null
+++ b/Libs/cadquery/conda-py3-freecad.yml
@@ -0,0 +1,72 @@
+name: cq-freecad
+channels: !!python/tuple
+- !!python/unicode
+ 'defaults'
+dependencies:
+- certifi=2016.2.28=py36_0
+- conda-forge::boost=1.64.0=py36_4
+- conda-forge::boost-cpp=1.64.0=1
+- conda-forge::bzip2=1.0.6=1
+- conda-forge::curl=7.54.1=0
+- conda-forge::cycler=0.10.0=py36_0
+- conda-forge::fontconfig=2.12.1=4
+- conda-forge::freeimage=3.17.0=0
+- conda-forge::freetype=2.7=1
+- conda-forge::future=0.16.0=py36_0
+- conda-forge::git=2.14.1=1
+- conda-forge::hdf5=1.8.18=0
+- conda-forge::icu=58.1=1
+- conda-forge::jsoncpp=0.10.6=1
+- conda-forge::krb5=1.14.2=0
+- conda-forge::libssh2=1.8.0=1
+- conda-forge::libtiff=4.0.6=7
+- conda-forge::libxslt=1.1.29=5
+- conda-forge::matplotlib=1.5.3=np113py36_8
+- conda-forge::occt=7.1.0=occt7.1.0_1
+- conda-forge::pyparsing=2.2.0=py36_0
+- conda-forge::pyqt=4.11.4=py36_2
+- conda-forge::pyside=1.2.4=py36_8
+- conda-forge::python-dateutil=2.6.1=py36_0
+- conda-forge::pytz=2017.2=py36_0
+- conda-forge::sip=4.18=py36_1
+- conda-forge::six=1.11.0=py36_1
+- conda-forge::tbb=2018_20170919=0
+- conda-forge::tornado=4.5.2=py36_0
+- conda-forge::vtk=7.1.1=py36_202
+- conda-forge::xerces-c=3.1.4=3
+- dbus=1.10.20=0
+- expat=2.1.0=0
+- freecad::coin3d=4.0.0=5
+- freecad::freecad=0.17=py36_occt7.1.0_5
+- freecad::libmed=3.1.0=2
+- freecad::netgen=6.2=py36_occt7.1.0_3
+- freecad::pivy=0.6.2=py36_dev_4
+- freecad::simage=1.7.0=0
+- freecad::soqt=1.5.0=0
+- glib=2.50.2=1
+- gst-plugins-base=1.8.0=0
+- gstreamer=1.8.0=0
+- jpeg=9b=0
+- libffi=3.2.1=1
+- libgcc=5.2.0=0
+- libgfortran=3.0.0=1
+- libiconv=1.14=0
+- libpng=1.6.30=1
+- libxcb=1.12=1
+- libxml2=2.9.4=0
+- mkl=2017.0.3=0
+- mock=2.0.0=py36_0
+- numpy=1.13.1=py36_0
+- openssl=1.0.2l=0
+- pbr=1.10.0=py36_0
+- pcre=8.39=1
+- pip=9.0.1=py36_1
+- python=3.6.2=0
+- qt=4.8.7=3
+- readline=6.2=2
+- setuptools=36.4.0=py36_1
+- sqlite=3.13.0=0
+- tk=8.5.18=0
+- wheel=0.29.0=py36_0
+- xz=5.2.3=0
+- zlib=1.2.11=0
diff --git a/Libs/cadquery/cq_cmd.py b/Libs/cadquery/cq_cmd.py
new file mode 100644
index 0000000..6b73eb1
--- /dev/null
+++ b/Libs/cadquery/cq_cmd.py
@@ -0,0 +1,196 @@
+#
+# cadquery command line interface
+# usage: cq_cmd [-h] [--param-file inputfile ] [--format STEP|STL ] [--output filename] filename
+# if input file contains multiple inputs, multiple outputs are created
+# if no input filename is provided, stdin is read instead
+# if no output filename is provided, output goes to stdout
+# default output format is STEP
+#
+
+import sys,os
+
+
+
+
+
+
+
+from cadquery import cqgi,exporters
+import argparse
+import os.path
+import json
+import tempfile
+
+class FilepathShapeWriter(object):
+ #a shape writer that writes a new file in a directory for each object
+ def __init__(self,file_pattern, shape_format):
+ self.shape_format=shape_format
+ self.file_pattern=file_pattern
+ self.counter = 1
+
+ def _compute_file_name(self):
+ return self.file_pattern % ({ "counter": self.counter,"format": self.shape_format } )
+
+ def write_shapes(self,shape_list):
+ for result in shape_list:
+ shape = result.shape
+ file_name = self._compute_file_name()
+ info("Writing %s Output to '%s'" % (self.shape_format, file_name))
+ s = open(file_name,'w')
+ exporters.exportShape(shape,self.shape_format,s)
+ s.flush()
+ s.close()
+
+class StdoutShapeWriter(object):
+ #has extra code to prevent freecad crap from junking up stdout
+ def __init__(self,shape_format ):
+ self.shape_format = shape_format
+
+ def write_shapes(self,shape_list):
+ #f = open('/tmp/cqtemp','w')
+ #with suppress_stdout_stderr():
+ exporters.exportShape(shape_list[0].shape,self.shape_format,sys.stdout)
+ #f.flush()
+ #f.close()
+ #f = open('/tmp/cqtemp')
+ #sys.stdout.write(f.read())
+
+
+def create_shape_writer(out_spec,shape_format):
+ if out_spec == 'stdout':
+ return StdoutShapeWriter(shape_format)
+ else:
+ return FilepathShapeWriter(out_spec,shape_format)
+
+class ErrorCodes(object):
+ SCRIPT_ERROR=2
+ UNKNOWN_ERROR=3
+ INVALID_OUTPUT_DESTINATION=4
+ INVALID_OUTPUT_FORMAT=5
+ INVALID_INPUT=6
+ MULTIPLE_RESULTS=7
+
+def eprint(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+def error(msg,exit_code=1):
+ eprint("ERROR %d: %s" %( exit_code, msg))
+ sys.exit(exit_code)
+
+def warning(msg):
+ eprint("WARNING: %s" % msg)
+
+def info(msg):
+ eprint("INFO: %s" % msg)
+
+def read_file_as_string(file_name):
+ if os.path.isfile(file_name):
+ f = open(file_name)
+ s = f.read()
+ f.close()
+ return s
+ else:
+ return None
+
+class ParameterHandler(object):
+ def __init__(self):
+ self.params = {}
+
+ def apply_file(self,file_spec):
+ if file_spec is not None:
+ p = read_file_as_string(file_spec)
+ if p is not None:
+ d = json.loads(p)
+ self.params.update(d)
+
+ def apply_string(self,param_string):
+ if param_string is not None:
+ r = json.loads(param_string)
+ self.params.update(r)
+
+ def get(self):
+ return self.params
+
+def read_input_script(file_name):
+ if file_name != "stdin":
+ s = read_file_as_string(file_name)
+ if s is None:
+ error("%s does not appear to be a readable file." % file_name,ErrorCodes.INVALID_INPUT)
+ else:
+ return s
+ else:
+ s = sys.stdin.read()
+ return s
+
+def check_input_format(input_format):
+ valid_types = [exporters.ExportTypes.TJS,exporters.ExportTypes.AMF,
+ exporters.ExportTypes.STEP,exporters.ExportTypes.STL,exporters.ExportTypes.TJS ]
+ if input_format not in valid_types:
+ error("Invalid Input format '%s'. Valid values: %s" % ( input_format, str(valid_types)) ,
+ ErrorCodes.INVALID_OUTPUT_FORMAT)
+
+
+def describe_parameters(user_params, script_params):
+ if len(script_params)> 0:
+ parameter_names = ",".join(list(script_params.keys()))
+ info("This script provides parameters %s, which can be customized at build time." % parameter_names)
+ else:
+ info("This script provides no customizable build parameters.")
+ if len(user_params) > 0:
+ info("User Supplied Parameter Values ( Override Model Defaults):")
+ for k,v in user_params.items():
+ info("\tParameter: %s=%s" % (k,v))
+ else:
+ info("The script will run with default variable values")
+ info("use --param_file to provide a json file that contains values to override the defaults")
+
+def run(args):
+
+ info("Reading from file '%s'" % args.in_spec)
+ input_script = read_input_script(args.in_spec)
+ script_name = 'stdin' if args.in_spec is None else args.in_spec
+ cq_model = cqgi.parse(input_script)
+ info("Parsed Script '%s'." % script_name)
+
+ param_handler = ParameterHandler()
+ param_handler.apply_file(args.param_file)
+ param_handler.apply_string(args.params)
+ user_params = param_handler.get()
+ describe_parameters(user_params,cq_model.metadata.parameters)
+
+ check_input_format(args.format)
+
+ build_result = cq_model.build(build_parameters=user_params)
+
+ info("Output Format is '%s'. Use --output-format to change it." % args.format)
+ info("Output Path is '%s'. Use --out_spec to change it." % args.out_spec)
+
+ if build_result.success:
+ result_list = build_result.results
+ info("Script Generated %d result Objects" % len(result_list))
+ shape_writer = create_shape_writer(args.out_spec,args.format)
+ shape_writer.write_shapes(result_list)
+ else:
+ error("Script Error: '%s'" % build_result.exception,ErrorCodes.SCRIPT_ERROR)
+
+if __name__=='__main__':
+
+ desc="""
+CQ CMD. Runs a cadquery python file, and produces a 3d object.
+A script can be provided as a file or as standard input.
+Each object created by the script is written the supplied output directory.
+ """
+ filename_pattern_help="""
+Filename pattern to use when creating output files.
+The sequential file number and the format are available.
+Default: cqobject-%%(counter)d.%%(format)s
+Use stdout to write to stdout ( can't be used for multiple results though)
+ """
+ parser = argparse.ArgumentParser(description=desc)
+ parser.add_argument("--format", action="store",default="STEP",help="Output Object format (TJS|STEP|STL|SVG)")
+ parser.add_argument("--param_file", action="store",help="Parameter Values File, in JSON format")
+ parser.add_argument("--params",action="store", help="JSON encoded parameter values. They override values provided in param_file")
+ parser.add_argument("--in_spec", action="store", required=True, help="Input File path. Use stdin to read standard in")
+ parser.add_argument("--out_spec", action="store",default="./cqobject-%(counter)d.%(format)s",help=filename_pattern_help)
+ args = parser.parse_args()
+ run(args)
diff --git a/Libs/cadquery/cq_cmd.sh b/Libs/cadquery/cq_cmd.sh
new file mode 100644
index 0000000..db651ef
--- /dev/null
+++ b/Libs/cadquery/cq_cmd.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# NOTE
+# the -u (unbuffered flag) in the below is very important
+# without it, the FreeCAD libraries somehow manage to get some stdout
+# junking up output when stdout is used.
+# this is the script we use
+# to select between running a build server
+# and a command line job runner
+if [ -z "$1" ]; then
+ echo "************************"
+ echo "CadQuery Docker Image"
+ echo "************************"
+ echo "Usage: docker run cadquery build [options]"
+ echo "Examples:"
+ echo " Read a model from stdin, write output to stdout"
+ echo ""
+ echo " cat cadquery_script.py | sudo docker run -i dcowden/cadquery:latest --in_spec stdin --out_spec stdout > my_object.STEP"
+ echo " "
+ echo " Mount a directory, and write results into the local directory"
+ echo ""
+ echo " sudo docker run -i dcowden/cadquery:latest --in_spec my_script.py"
+ echo ""
+ exec python -u /opt/cadquery/cq_cmd.py -h
+ exit 1
+fi;
+if [ "$1" == "build" ]; then
+ exec python -u /opt/cadquery/cq_cmd.py "${@:2}"
+fi;
+if [ "$1" == "runserver" ]; then
+ echo "Future CadQuery Server"
+ exit 1
+ #exec python -u /opt/cadquery/cq_server.py "${@:2}"
+fi;
diff --git a/Libs/cadquery/doc/README b/Libs/cadquery/doc/README
new file mode 100644
index 0000000..b89ab3d
--- /dev/null
+++ b/Libs/cadquery/doc/README
@@ -0,0 +1,2 @@
+This documentation should be generated with sphinxdoc.
+see ../build-docs.sh
diff --git a/Libs/cadquery/doc/_static/ParametricPulley.PNG b/Libs/cadquery/doc/_static/ParametricPulley.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..c5e6e873038fd3beb443fa8bfd146c649578e126
GIT binary patch
literal 59186
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