From feb559bedad7268a96d834ccb258b5e639947147 Mon Sep 17 00:00:00 2001 From: Youbao Zhang Date: Wed, 30 Dec 2015 14:08:49 +0000 Subject: [PATCH 01/17] 1. Add a mirror function into CQ class(wrap) and Shape class(implement); 2. To get precise BoundBox, add tessellate(0.000001) in the BoundBox function implement...... --- cadquery/CQ.py | 13 +++++++++++++ cadquery/freecad_impl/shapes.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cadquery/CQ.py b/cadquery/CQ.py index 8d21173..7e87fb2 100644 --- a/cadquery/CQ.py +++ b/cadquery/CQ.py @@ -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. diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 27b97a6..3134333 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -182,8 +182,22 @@ class Shape(object): return self.wrapped.isValid() def BoundingBox(self): + self.wrapped.tessellate(0.000001) 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): From 3186b68b058069a7f7a667fcd11c565a3568b284 Mon Sep 17 00:00:00 2001 From: Youbao Zhang Date: Wed, 30 Dec 2015 14:08:49 +0000 Subject: [PATCH 02/17] 1. Add a mirror function into CQ class(wrap) and Shape class(implement); 2. To get precise BoundBox, add tessellate(0.000001) in the BoundBox function implement...... --- cadquery/cq.py | 13 +++++++++++++ cadquery/freecad_impl/shapes.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cadquery/cq.py b/cadquery/cq.py index 59a1180..7293813 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -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. diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 76af1c1..12d410b 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -185,8 +185,22 @@ class Shape(object): return self.wrapped.isValid() def BoundingBox(self): + self.wrapped.tessellate(0.000001) 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): From 7225d84905403b1c5926dcba02c709ae133356ac Mon Sep 17 00:00:00 2001 From: Simon Huskier Date: Wed, 20 Jan 2016 16:56:48 +0800 Subject: [PATCH 03/17] 1. Add a mirroring example into doc/example.rst; 2. Add a tolerance parameter into BoundingBox function, and default it as 0.1, otherwise, it's very slow when compile the docs with sphinx-build, don't know why; 3. Add a testBoundingBox function into TestCadQuery.py file... --- cadquery/freecad_impl/shapes.py | 18 +++++----- doc/examples.rst | 58 +++++++++++++++++++++++++++++++++ tests/TestCadQuery.py | 37 +++++++++++++++++++++ 3 files changed, 104 insertions(+), 9 deletions(-) diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 12d410b..16814ba 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -184,22 +184,22 @@ class Shape(object): def isValid(self): return self.wrapped.isValid() - def BoundingBox(self): - self.wrapped.tessellate(0.000001) + 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 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)) + 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 diff --git a/doc/examples.rst b/doc/examples.rst index 0705da5..06c79aa 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -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 ----------------------------- diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 01e7753..f8b9b2a 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -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 From 9d9b6d310e67dc50616cb81f7ede2500fd4cddcf Mon Sep 17 00:00:00 2001 From: Simon Huskier Date: Wed, 20 Jan 2016 17:01:14 +0800 Subject: [PATCH 04/17] Small format fix for function BoundingBox in shapes.py...... --- cadquery/freecad_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 16814ba..a78443c 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -185,7 +185,7 @@ class Shape(object): return self.wrapped.isValid() def BoundingBox(self, tolerance=0.1): - self.wrapped.tessellate(tolerance) + self.wrapped.tessellate(tolerance) return BoundBox(self.wrapped.BoundBox) def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): From 8c8432410bbe611e2321657a325c121683f67869 Mon Sep 17 00:00:00 2001 From: Simon Huskier Date: Wed, 20 Jan 2016 17:08:32 +0800 Subject: [PATCH 05/17] Small format fix for function mirror in Shapes.py file...... --- cadquery/freecad_impl/shapes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index a78443c..0ceae22 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -189,17 +189,17 @@ class Shape(object): 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 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)) + 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 From 3ea7829471ff09b317e9a60b9535181d208d8264 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Fri, 25 Mar 2016 16:51:58 -0400 Subject: [PATCH 06/17] ignore the 2_0 branch from travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 08bce03..30d714e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,3 +25,4 @@ after_success: branches: except: - pythonocc + - 2_0_branch From e0e14a133d21025a27e4a951f607323b8412c874 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Sun, 3 Apr 2016 20:24:13 -0400 Subject: [PATCH 07/17] fixed tests to pass with FreeCAD 0.15 --- cadquery/freecad_impl/shapes.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 0ceae22..659d4d1 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -237,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:] : @@ -246,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): """ From 65480d4bf1a7bc20ceb20b4243e7678be89e98ab Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Sun, 3 Apr 2016 21:01:36 -0400 Subject: [PATCH 08/17] added debug function to cqgi --- cadquery/cqgi.py | 27 ++++++++++++++++++++++++--- tests/TestCQGI.py | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index 92fd860..cc0d087 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -95,10 +95,13 @@ 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) \ + .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 +142,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 +151,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] @@ -270,9 +277,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,6 +288,12 @@ class ScriptCallback(object): """ self.outputObjects.append(shape) + def debug(self,obj,args={}): + """ + Debug print/output an object, with optional arguments. + """ + self.debugObjects.append(DebugObject(obj,args)) + def describe_parameter(self,var, valid_values, short_desc): """ Not yet implemented: allows a script to document @@ -297,7 +310,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): """ diff --git a/tests/TestCQGI.py b/tests/TestCQGI.py index 35d8906..ae8c49d 100644 --- a/tests/TestCQGI.py +++ b/tests/TestCQGI.py @@ -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() From 7c3bf01779efc9c6d78d678421126aed7e54f2f8 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Sun, 3 Apr 2016 21:12:54 -0400 Subject: [PATCH 09/17] added build_options to cqgi.build --- cadquery/cqgi.py | 6 ++++-- doc/cqgi.rst | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index cc0d087..37da61f 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -75,11 +75,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 diff --git a/doc/cqgi.rst b/doc/cqgi.rst index 6e47b91..c8be3e3 100644 --- a/doc/cqgi.rst +++ b/doc/cqgi.rst @@ -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, @@ -57,7 +73,8 @@ The :py:meth:`cadquery.cqgi.parse()` method returns a :py:class:`cadquery.cqgi.C 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 +84,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 ----------------------------- From b22409d88c0ed493ddb79f75f5b01f24d2a12cab Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Tue, 5 Apr 2016 21:04:09 -0400 Subject: [PATCH 10/17] added describe_parameter --- cadquery/cqgi.py | 56 +++++++++++++++++++++++++++++++++-------------- doc/cqgi.rst | 18 +++++++++++++++ runtests.py | 2 +- tests/TestCQGI.py | 24 ++++++++++++++++++++ 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index 37da61f..01701db 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -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): """ @@ -99,6 +104,7 @@ class CQModel(object): env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ .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') @@ -173,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 @@ -213,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 = [] @@ -234,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 @@ -296,10 +300,9 @@ class ScriptCallback(object): """ self.debugObjects.append(DebugObject(obj,args)) - def describe_parameter(self,var, valid_values, short_desc): + def describe_parameter(self,var_data ): """ - Not yet implemented: allows a script to document - extra metadata about the parameters + Do Nothing-- we parsed the ast ahead of exection to get what we need. """ pass @@ -394,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): """ @@ -404,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: diff --git a/doc/cqgi.rst b/doc/cqgi.rst index c8be3e3..310f7d2 100644 --- a/doc/cqgi.rst +++ b/doc/cqgi.rst @@ -70,6 +70,24 @@ 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. diff --git a/runtests.py b/runtests.py index ad516ce..ee90ce4 100644 --- a/runtests.py +++ b/runtests.py @@ -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) diff --git a/tests/TestCQGI.py b/tests/TestCQGI.py index ae8c49d..7cc967f 100644 --- a/tests/TestCQGI.py +++ b/tests/TestCQGI.py @@ -66,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( """ From 0c80eac633f6e4aef85a763bcf608cf4b636f411 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 21:15:49 -0400 Subject: [PATCH 11/17] configure travis to push to pypi --- .travis.yml | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30d714e..f1e4d27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,28 @@ ---- 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 - - 2_0_branch + - pythonocc + - 2_0_branch +deploy: + provider: pypi + user: dcowden + password: + secure: JwP4mOuypJPVW+WYgvVWLSY7rr1oEkxFhzMx3GCR53LfnpwGd9Qlm4aCDu0RbAuwbpEWmLD1S/m9/jtGr4LuTOMqqoLrRVxjNqz96hGo2he+/G/s5k1KyP8jSZK1zhJV5u69YcSnJq5/NI+ondALyYU3ZpEVFaFVcUA/PsDWzXA= From 96d15cb0ccdd0bc340b79a6704c40faebca552ab Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 21:21:58 -0400 Subject: [PATCH 12/17] add travis build number to subminor version --- setup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e68a6e6..1eedd31 100644 --- a/setup.py +++ b/setup.py @@ -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_list = ['0','4','0'] +if 'TRAVIS_BUILD_NUMBER' in os.environ.keys(): + version_list[-1] = os.environ['TRAVIS_BUILD_NUMBER'] +version = '.'.join(version_list) + setup( name='cadquery', - version='0.4.0', + version=version, url='https://github.com/dcowden/cadquery', license='Apache Public License 2.0', author='David Cowden', From 08ab9370556d1c9f39c963fbbb937548f69d03b6 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 21:25:23 -0400 Subject: [PATCH 13/17] try to fix password --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f1e4d27..b1e0e97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ deploy: provider: pypi user: dcowden password: - secure: JwP4mOuypJPVW+WYgvVWLSY7rr1oEkxFhzMx3GCR53LfnpwGd9Qlm4aCDu0RbAuwbpEWmLD1S/m9/jtGr4LuTOMqqoLrRVxjNqz96hGo2he+/G/s5k1KyP8jSZK1zhJV5u69YcSnJq5/NI+ondALyYU3ZpEVFaFVcUA/PsDWzXA= + secure: aP02wBbry1j3hYG/w++siF1lk26teuRQlPAx1c+ec8fxUw+bECa2HbPQHcIvSXB5N6nc6P3L9LjHt9ktm+Dn6FLJu3qWYNGAZx9PTn24ug0iAmB+JyNrsET3nK6WUKR1XpBqvjKgdpukd1Hknh2FSzYoyUvFWH9/CovITCFN3jo= From a06bd4176ba8904602d6de3705318992664fdfe9 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 21:48:03 -0400 Subject: [PATCH 14/17] fixed problem with installation. readme missing --- MANIFEST | 1 + MANIFEST.in | 1 + 2 files changed, 2 insertions(+) diff --git a/MANIFEST b/MANIFEST index 69b0aae..ce5c215 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,4 +1,5 @@ README.txt +README.md setup.cfg setup.py cadquery\cq.py diff --git a/MANIFEST.in b/MANIFEST.in index e69de29..bb3ec5f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md From 54d3e23c847f6e096bd1b780d46f9efbc3668dd0 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 22:21:10 -0400 Subject: [PATCH 15/17] changed to build only on tagged commits --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b1e0e97..e4e38b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,3 +26,5 @@ deploy: user: dcowden password: secure: aP02wBbry1j3hYG/w++siF1lk26teuRQlPAx1c+ec8fxUw+bECa2HbPQHcIvSXB5N6nc6P3L9LjHt9ktm+Dn6FLJu3qWYNGAZx9PTn24ug0iAmB+JyNrsET3nK6WUKR1XpBqvjKgdpukd1Hknh2FSzYoyUvFWH9/CovITCFN3jo= + on: + tags: true \ No newline at end of file From a28b1d176cdc5ee8c1558aabcaf0c3be3f28655c Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 22:23:11 -0400 Subject: [PATCH 16/17] changed to use tagged version in a build --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 1eedd31..988a00e 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,10 @@ from setuptools import setup #if we are building in travis, use the build number as the sub-minor version -version_list = ['0','4','0'] -if 'TRAVIS_BUILD_NUMBER' in os.environ.keys(): - version_list[-1] = os.environ['TRAVIS_BUILD_NUMBER'] -version = '.'.join(version_list) +version = '0.5-SNAPSHOT' +if 'TRAVIS_TAG' in os.environ.keys(): + version= os.environ['TRAVIS_TAG'] + setup( name='cadquery', From 4a105d12413b11bbd29cd50249867d86b41e3965 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Wed, 13 Apr 2016 22:28:29 -0400 Subject: [PATCH 17/17] small change to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 988a00e..9a25b9e 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ from setuptools import setup version = '0.5-SNAPSHOT' if 'TRAVIS_TAG' in os.environ.keys(): version= os.environ['TRAVIS_TAG'] - + setup( name='cadquery',