Merge commit '4a105d12413b11bbd29cd50249867d86b41e3965'

This commit is contained in:
Jeremy Mack Wright 2016-04-22 21:38:48 -04:00
commit 4a21c9e665
12 changed files with 326 additions and 49 deletions

View File

@ -1,27 +1,30 @@
---
language: python
before_install:
- sudo add-apt-repository -y ppa:freecad-maintainers/freecad-daily
- sudo apt-get update -qq
- sudo add-apt-repository -y ppa:freecad-maintainers/freecad-daily
- sudo apt-get update -qq
install:
- sudo apt-get install -y freecad freecad-doc
- gcc --version
- g++ --version
- python ./setup.py install
- pip install coverage
- pip install coveralls
- pip install Sphinx==1.3.2
- pip install travis-sphinx
- sudo apt-get install -y freecad freecad-doc
- gcc --version
- g++ --version
- python ./setup.py install
- pip install coverage
- pip install coveralls
- pip install Sphinx==1.3.2
- pip install travis-sphinx
script:
- coverage run --source=cadquery ./runtests.py
- travis-sphinx --nowarn --source=doc build
- coverage run --source=cadquery ./runtests.py
- travis-sphinx --nowarn --source=doc build
after_success:
- coveralls
- travis-sphinx deploy
- coveralls
- travis-sphinx deploy
branches:
except:
- pythonocc
- pythonocc
- 2_0_branch
deploy:
provider: pypi
user: dcowden
password:
secure: aP02wBbry1j3hYG/w++siF1lk26teuRQlPAx1c+ec8fxUw+bECa2HbPQHcIvSXB5N6nc6P3L9LjHt9ktm+Dn6FLJu3qWYNGAZx9PTn24ug0iAmB+JyNrsET3nK6WUKR1XpBqvjKgdpukd1Hknh2FSzYoyUvFWH9/CovITCFN3jo=
on:
tags: true

View File

@ -1,4 +1,5 @@
README.txt
README.md
setup.cfg
setup.py
cadquery\cq.py

View File

@ -0,0 +1 @@
include README.md

View File

@ -742,6 +742,19 @@ class 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.

View File

@ -44,9 +44,11 @@ class CQModel(object):
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):
"""
@ -65,6 +67,9 @@ class CQModel(object):
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):
"""
@ -75,11 +80,13 @@ class CQModel(object):
"""
raise NotImplementedError("not yet implemented")
def build(self, build_parameters=None):
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.
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
@ -95,10 +102,14 @@ class CQModel(object):
self.set_param_values(build_parameters)
collector = ScriptCallback()
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
.add_entry("build_object", collector.build_object).build()
.add_entry("build_object", collector.build_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 )
if collector.has_results():
result.set_success_result(collector.outputObjects)
else:
@ -139,6 +150,7 @@ class BuildResult(object):
def __init__(self):
self.buildTime = None
self.results = []
self.debugObjects = []
self.first_result = None
self.success = False
self.exception = None
@ -147,6 +159,9 @@ class BuildResult(object):
self.exception = ex
self.success = False
def set_debug(self, debugObjects):
self.debugObjects = debugObjects
def set_success_result(self, results):
self.results = results
self.first_result = self.results[0]
@ -164,6 +179,11 @@ class ScriptMetadata(object):
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
@ -204,19 +224,15 @@ class InputParameter:
self.varType = None
#: help text describing the variable. Only available if the script used describe_parameter()
self.shortDesc = None
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, short_desc=None):
def create(ast_node, var_name, var_type, default_value, valid_values=None, desc=None):
if valid_values is None:
valid_values = []
@ -225,10 +241,7 @@ class InputParameter:
p.ast_node = ast_node
p.default_value = default_value
p.name = var_name
if short_desc is None:
p.shortDesc = var_name
else:
p.shortDesc = short_desc
p.desc = desc
p.varType = var_type
p.valid_values = valid_values
return p
@ -270,9 +283,9 @@ class ScriptCallback(object):
the build_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 build_object(self, shape):
"""
@ -281,10 +294,15 @@ class ScriptCallback(object):
"""
self.outputObjects.append(shape)
def describe_parameter(self,var, valid_values, short_desc):
def debug(self,obj,args={}):
"""
Not yet implemented: allows a script to document
extra metadata about the parameters
Debug print/output an object, with optional arguments.
"""
self.debugObjects.append(DebugObject(obj,args))
def describe_parameter(self,var_data ):
"""
Do Nothing-- we parsed the ast ahead of exection to get what we need.
"""
pass
@ -297,7 +315,15 @@ class ScriptCallback(object):
def has_results(self):
return len(self.outputObjects) > 0
class DebugObject(object):
"""
Represents a request to debug an object
Object is the type of object we want to debug
args are parameters for use during debuging ( for example, color, tranparency )
"""
def __init__(self,object,args):
self.args = args
self.object = object
class InvalidParameterError(Exception):
"""
@ -371,6 +397,30 @@ class EnvironmentBuilder(object):
def build(self):
return self.env
class ParameterDescriptionFinder(ast.NodeTransformer):
"""
Visits a parse tree, looking for function calls to describe_parameter(var, description )
"""
def __init__(self, cq_model):
self.cqModel = cq_model
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):
"""
@ -381,9 +431,6 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
self.cqModel = cq_model
def handle_assignment(self, var_name, value_node):
try:
if type(value_node) == ast.Num:

View File

@ -184,9 +184,23 @@ class Shape(object):
def isValid(self):
return self.wrapped.isValid()
def BoundingBox(self):
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):
@ -223,8 +237,8 @@ class Shape(object):
:param objects: a list of objects with mass
"""
total_mass = sum(o.wrapped.Mass for o in objects)
weighted_centers = [o.wrapped.CenterOfMass.multiply(o.wrapped.Mass) for o in objects]
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:] :
@ -232,6 +246,17 @@ class Shape(object):
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):
"""

View File

@ -32,10 +32,26 @@ CQGI compliant containers provide an execution environment for scripts. The envi
* the cadquery library is automatically imported as 'cq'.
* the :py:meth:`cadquery.cqgi.ScriptCallback.build_object()` method is defined that should be used to export a shape to the execution environment
* the :py:meth:`cadquery.cqgi.ScriptCallBack.debug()` method is defined, which can be used by scripts to debug model output during execution.
Scripts must call build_output at least once. Invoking build_object more than once will send multiple objects to
the container. An error will occur if the script does not return an object using the build_object() method.
This CQGI compliant script produces a cube with a circle on top, and displays a workplane as well as an intermediate circle as debug output::
base_cube = cq.Workplane('XY').rect(1.0,1.0).extrude(1.0)
top_of_cube_plane = base_cube.faces(">Z").workplane()
debug(top_of_cube_plane, { 'color': 'yellow', } )
debug(top_of_cube_plane.center, { 'color' : 'blue' } )
circle=top_of_cube_plane.circle(0.5)
debug(circle, { 'color': 'red' } )
build_object( circle.extrude(1.0) )
Note that importing cadquery is not required.
At the end of this script, one object will be displayed, in addition to a workplane, a point, and a circle
Future enhancements will include several other methods, used to provide more metadata for the execution environment:
* :py:meth:`cadquery.cqgi.ScriptCallback.add_error()`, indicates an error with an input parameter
* :py:meth:`cadquery.cqgi.ScriptCallback.describe_parameter()`, provides extra information about a parameter in the script,
@ -54,10 +70,29 @@ run code like this::
The :py:meth:`cadquery.cqgi.parse()` method returns a :py:class:`cadquery.cqgi.CQModel` object.
The `metadata`p property of the object contains a `cadquery.cqgi.ScriptMetaData` object, which can be used to discover the
user parameters available. This is useful if the execution environment would like to present a GUI to allow the user to change the
model parameters. Typically, after collecting new values, the environment will supply them in the build() method.
This code will return a dictionary of parameter values in the model text SCRIPT::
parameters = cqgi.parse(SCRIPT).metadata.parameters
The dictionary you get back is a map where key is the parameter name, and value is an InputParameter object,
which has a name, type, and default value.
The type is an object which extends ParameterType-- you can use this to determine what kind of widget to render ( checkbox for boolean, for example ).
The parameter object also has a description, valid values, minimum, and maximum values, if the user has provided them using the
describe_parameter() method.
Calling :py:meth:`cadquery.cqgi.CQModel.build()` returns a :py:class:`cadquery.cqgi.BuildResult` object,
,which includes the script execution time, and a success flag.
If the script was successful, the results property will include a list of results returned by the script.
If the script was successful, the results property will include a list of results returned by the script,
as well as any debug the script produced
If the script failed, the exception property contains the exception object.
@ -67,12 +102,16 @@ with new values::
from cadquery import cqgi
user_script = ...
build_result = cqgi.parse(user_script).build({ 'param': 2 } )
build_result = cqgi.parse(user_script).build(build_parameters={ 'param': 2 }, build_options={} )
If a parameter called 'param' is defined in the model, it will be assigned the value 2 before the script runs.
An error will occur if a value is provided that is not defined in the model, or if the value provided cannot
be assigned to a variable with the given name.
build_options is used to set server-side settings like timeouts, tesselation tolerances, and other details about
how the model should be built.
More about script variables
-----------------------------

View File

@ -312,6 +312,64 @@ introduce horizontal and vertical lines, which make for slightly easier coding.
* :py:meth:`Workplane`
* :py:meth:`Workplane.extrude`
Mirroring 3D Objects
-----------------------------
.. cq_plot::
result0 = (cadquery.Workplane("XY")
.moveTo(10,0)
.lineTo(5,0)
.threePointArc((3.9393,0.4393),(3.5,1.5))
.threePointArc((3.0607,2.5607),(2,3))
.lineTo(1.5,3)
.threePointArc((0.4393,3.4393),(0,4.5))
.lineTo(0,13.5)
.threePointArc((0.4393,14.5607),(1.5,15))
.lineTo(28,15)
.lineTo(28,13.5)
.lineTo(24,13.5)
.lineTo(24,11.5)
.lineTo(27,11.5)
.lineTo(27,10)
.lineTo(22,10)
.lineTo(22,13.2)
.lineTo(14.5,13.2)
.lineTo(14.5,10)
.lineTo(12.5,10 )
.lineTo(12.5,13.2)
.lineTo(5.5,13.2)
.lineTo(5.5,2)
.threePointArc((5.793,1.293),(6.5,1))
.lineTo(10,1)
.close())
result = result0.extrude(100)
result = result.rotate((0, 0, 0),(1, 0, 0), 90)
result = result.translate(result.val().BoundingBox().center.multiply(-1))
mirXY_neg = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, -30))
mirXY_pos = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, 30))
mirZY_neg = result.mirror(mirrorPlane="ZY", basePointVector=(-30,0,0))
mirZY_pos = result.mirror(mirrorPlane="ZY", basePointVector=(30,0,0))
result = result.union(mirXY_neg).union(mirXY_pos).union(mirZY_neg).union(mirZY_pos)
build_object(result)
.. topic:: Api References
.. hlist::
:columns: 2
* :py:meth:`Workplane.moveTo`
* :py:meth:`Workplane.lineTo`
* :py:meth:`Workplane.threePointArc`
* :py:meth:`Workplane.extrude`
* :py:meth:`Workplane.mirror`
* :py:meth:`Workplane.union`
* :py:meth:`CQ.rotate`
Creating Workplanes on Faces
-----------------------------

View File

@ -8,11 +8,11 @@ import unittest
#on py 2.7.x on win
suite = unittest.TestSuite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI))
unittest.TextTestRunner().run(suite)

View File

@ -11,12 +11,19 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from setuptools import setup
#if we are building in travis, use the build number as the sub-minor version
version = '0.5-SNAPSHOT'
if 'TRAVIS_TAG' in os.environ.keys():
version= os.environ['TRAVIS_TAG']
setup(
name='cadquery',
version='0.4.0',
version=version,
url='https://github.com/dcowden/cadquery',
license='Apache Public License 2.0',
author='David Cowden',

View File

@ -23,6 +23,18 @@ TESTSCRIPT = textwrap.dedent(
"""
)
TEST_DEBUG_SCRIPT = textwrap.dedent(
"""
height=2.0
width=3.0
(a,b) = (1.0,1.0)
foo="bar"
debug(foo, { "color": 'yellow' } )
result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) )
build_object(result)
debug(height )
"""
)
class TestCQGI(BaseTest):
def test_parser(self):
@ -31,6 +43,16 @@ class TestCQGI(BaseTest):
self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'})
def test_build_with_debug(self):
model = cqgi.CQModel(TEST_DEBUG_SCRIPT)
result = model.build()
debugItems = result.debugObjects
self.assertTrue(len(debugItems) == 2)
self.assertTrue( debugItems[0].object == "bar" )
self.assertTrue( debugItems[0].args == { "color":'yellow' } )
self.assertTrue( debugItems[1].object == 2.0 )
self.assertTrue( debugItems[1].args == {} )
def test_build_with_empty_params(self):
model = cqgi.CQModel(TESTSCRIPT)
result = model.build()
@ -44,6 +66,30 @@ class TestCQGI(BaseTest):
result = model.build({'height': 3.0})
self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0")
def test_describe_parameters(self):
script = textwrap.dedent(
"""
a = 2.0
describe_parameter(a,'FirstLetter')
"""
)
model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a']
self.assertTrue(a_param.default_value == 2.0)
self.assertTrue(a_param.desc == 'FirstLetter')
self.assertTrue(a_param.varType == cqgi.NumberParameterType )
def test_describe_parameter_invalid_doesnt_fail_script(self):
script = textwrap.dedent(
"""
a = 2.0
describe_parameter(a, 2 - 1 )
"""
)
model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a']
self.assertTrue(a_param.name == 'a' )
def test_build_with_exception(self):
badscript = textwrap.dedent(
"""

View File

@ -550,6 +550,43 @@ class TestCadQuery(BaseTest):
self.assertEqual(10,currentS.faces().size())
def testBoundingBox(self):
"""
Tests the boudingbox center of a model
"""
result0 = (Workplane("XY")
.moveTo(10,0)
.lineTo(5,0)
.threePointArc((3.9393,0.4393),(3.5,1.5))
.threePointArc((3.0607,2.5607),(2,3))
.lineTo(1.5,3)
.threePointArc((0.4393,3.4393),(0,4.5))
.lineTo(0,13.5)
.threePointArc((0.4393,14.5607),(1.5,15))
.lineTo(28,15)
.lineTo(28,13.5)
.lineTo(24,13.5)
.lineTo(24,11.5)
.lineTo(27,11.5)
.lineTo(27,10)
.lineTo(22,10)
.lineTo(22,13.2)
.lineTo(14.5,13.2)
.lineTo(14.5,10)
.lineTo(12.5,10 )
.lineTo(12.5,13.2)
.lineTo(5.5,13.2)
.lineTo(5.5,2)
.threePointArc((5.793,1.293),(6.5,1))
.lineTo(10,1)
.close())
result = result0.extrude(100)
bb_center = result.val().BoundingBox().center
self.saveModel(result)
self.assertAlmostEqual(14.0, bb_center.x, 3)
self.assertAlmostEqual(7.5, bb_center.y, 3)
self.assertAlmostEqual(50.0, bb_center.z, 3)
def testCutThroughAll(self):
"""
Tests a model that uses more than one workplane