diff --git a/cadquery/cq.py b/cadquery/cq.py index 61496f2f5..818c77343 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -18,6 +18,7 @@ """ import math +from copy import copy from . import ( Vector, Plane, @@ -71,6 +72,7 @@ def __init__(self, obj): self.objects = [] self.ctx = CQContext() self.parent = None + self._tag = None if obj: # guarded because sometimes None for internal use self.objects.append(obj) @@ -94,6 +96,17 @@ def newObject(self, objlist): r.objects = list(objlist) return r + def tag(self, name): + """ + Tags the current CQ object for later reference. + + :param name: the name to tag this object with + :type name: string + :returns: self, a cq object with tag applied + """ + self._tag = name + return self + def _collectProperty(self, propName): """ Collects all of the values for propName, @@ -266,6 +279,22 @@ def val(self): """ return self.objects[0] + def _getTagged(self, name): + """ + Search the parent chain for a an object with tag == name. + + :param name: the tag to search for + :type name: string + :returns: the first CQ object in the parent chain with tag == name + :raises: ValueError if no object tagged name in the chain + """ + if self._tag == name: + return self + if self.parent is None: + raise ValueError("No CQ object named {} in chain".format(name)) + else: + return self.parent._getTagged(name) + def toOCC(self): """ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code @@ -430,6 +459,31 @@ def _computeXdir(normal): # a new workplane has the center of the workplane on the stack return s + def copyWorkplane(self, obj): + """ + Copies the workplane from obj. + + :param obj: an object to copy the workplane from + :type obj: a CQ object + :returns: a CQ object with obj's workplane + """ + out = Workplane(obj.plane) + out.parent = self + out.ctx = self.ctx + return out + + def workplaneFromTagged(self, name): + """ + Copies the workplane from a tagged parent. + + :param name: tag to search for + :type name: string + :returns: a CQ object with name's workplane + """ + tagged = self._getTagged(name) + out = self.copyWorkplane(tagged) + return out + def first(self): """ Return the first item on the stack @@ -522,20 +576,23 @@ def findFace(self, searchStack=True, searchParents=True): return self._findType(Face, searchStack, searchParents) - def _selectObjects(self, objType, selector=None): + def _selectObjects(self, objType, selector=None, tag=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) + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. """ + cq_obj = self._getTagged(tag) if tag else self # A single list of all faces from all objects on the stack - toReturn = self._collectProperty(objType) + toReturn = cq_obj._collectProperty(objType) if selector is not None: if isinstance(selector, str) or isinstance(selector, str): @@ -546,7 +603,7 @@ def _selectObjects(self, objType, selector=None): return self.newObject(toReturn) - def vertices(self, selector=None): + def vertices(self, selector=None, tag=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 @@ -554,6 +611,8 @@ def vertices(self, selector=None): :param selector: :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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 @@ -575,9 +634,9 @@ def vertices(self, selector=None): :py:class:`StringSyntaxSelector` """ - return self._selectObjects("Vertices", selector) + return self._selectObjects("Vertices", selector, tag) - def faces(self, selector=None): + def faces(self, selector=None, tag=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 @@ -585,6 +644,8 @@ def faces(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -607,9 +668,9 @@ def faces(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Faces", selector) + return self._selectObjects("Faces", selector, tag) - def edges(self, selector=None): + def edges(self, selector=None, tag=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 @@ -617,6 +678,8 @@ def edges(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -638,9 +701,9 @@ def edges(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Edges", selector) + return self._selectObjects("Edges", selector, tag) - def wires(self, selector=None): + def wires(self, selector=None, tag=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 @@ -648,6 +711,8 @@ def wires(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -661,9 +726,9 @@ def wires(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Wires", selector) + return self._selectObjects("Wires", selector, tag) - def solids(self, selector=None): + def solids(self, selector=None, tag=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 @@ -671,6 +736,8 @@ def solids(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -687,9 +754,9 @@ def solids(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Solids", selector) + return self._selectObjects("Solids", selector, tag) - def shells(self, selector=None): + def shells(self, selector=None, tag=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 @@ -697,6 +764,8 @@ def shells(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -707,9 +776,9 @@ def shells(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Shells", selector) + return self._selectObjects("Shells", selector, tag) - def compounds(self, selector=None): + def compounds(self, selector=None, tag=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 @@ -717,6 +786,8 @@ def compounds(self, selector=None): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :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. @@ -725,7 +796,7 @@ def compounds(self, selector=None): See more about selectors HERE """ - return self._selectObjects("Compounds", selector) + return self._selectObjects("Compounds", selector, tag) def toSvg(self, opts=None): """ @@ -1007,6 +1078,7 @@ def __init__(self, inPlane, origin=(0, 0, 0), obj=None): self.objects = [self.plane.origin] self.parent = None self.ctx = CQContext() + self._tag = None def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)): """ @@ -1048,7 +1120,7 @@ def newObject(self, objlist): # copy the current state to the new object ns = Workplane("XY") - ns.plane = self.plane + ns.plane = copy(self.plane) ns.parent = self ns.objects = list(objlist) ns.ctx = self.ctx @@ -1215,8 +1287,9 @@ def center(self, x, y): 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]) + new_origin = self.plane.toWorldCoords((x, y)) + n = self.newObject([new_origin]) + n.plane.setOrigin2d(x, y) return n def lineTo(self, x, y, forConstruction=False): diff --git a/doc/examples.rst b/doc/examples.rst index 0f3662e0d..a25975e4d 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -460,6 +460,32 @@ This example uses an offset workplane to make a compound object, which is perfec * :py:meth:`Workplane.box` * :py:meth:`Workplane` +Copying Workplanes +-------------------------- + +An existing CQ object can copy a workplane from another CQ object. + +.. cq_plot:: + + result = (cq.Workplane("front").circle(1).extrude(10) # make a cylinder + # We want to make a second cylinder perpendicular to the first, + # but we have no face to base the workplane off + .copyWorkplane( + # create a temporary object with the required workplane + cq.Workplane("right", origin=(-5, 0, 0)) + ).circle(1).extrude(10)) + show_object(result) + +.. topic:: API References + + .. hlist: + :columns: 2 + + * :py:meth:`CQ.copyWorkplane` **!** + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane` + Rotated Workplanes -------------------------- @@ -604,6 +630,55 @@ Here we fillet all of the edges of a simple plate. * :py:meth:`Workplane.edges` * :py:meth:`Workplane` +Tagging objects +---------------- + +The :py:meth:`CQ.tag` method can be used to tag a particular object in the chain with a string, so that it can be refered to later in the chain. + +The :py:meth:`CQ.workplaneFromTagged` method applies :py:meth:`CQ.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create and tag the base workplane + .box(10, 10, 10).faces(">Z").workplane().tag("baseplane") + # extrude a cylinder + .center(-3, 0).circle(1).extrude(3) + # to reselect the base workplane, simply + .workplaneFromTagged("baseplane") + # extrude a second cylinder + .center(3, 0).circle(1).extrude(2)) + show_object(result) + + +Tags can also be used with most selectors, including :py:meth:`CQ.vertices`, :py:meth:`CQ.faces`, :py:meth:`CQ.edges`, :py:meth:`CQ.wires`, :py:meth:`CQ.shells`, :py:meth:`CQ.solids` and :py:meth:`CQ.compounds`. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create a triangular prism and tag it + .polygon(3, 5).extrude(4).tag("prism") + # create a sphere that obscures the prism + .sphere(10) + # create features based on the prism's faces + .faces("X", tag="prism").faces(">Y").workplane().circle(1).cutThruAll()) + show_object(result) + +.. topic:: Api References + + .. hlist:: + :columns: 2 + + * :py:meth:`CQ.tag` **!** + * :py:meth:`CQ.getTagged` **!** + * :py:meth:`CQ.workplaneFromTagged` **!** + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane.cutThruAll` + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.faces` + * :py:meth:`Workplane` + A Parametric Bearing Pillow Block ------------------------------------ diff --git a/doc/primer.rst b/doc/primer.rst index f036de3f2..88d2c7f5a 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -111,12 +111,19 @@ backwards in the stack to get the face as well:: You can browse stack access methods here: :ref:`stackMethods`. +.. _chaining: + Chaining --------------------------- All CadQuery methods return another CadQuery object, so that you can chain the methods together fluently. Use the core CQ methods to get at the objects that were created. +Each time a new CadQuery object is produced during these chained calls, it has a ``parent`` attribute that points +to the CadQuery object that created it. Several CadQuery methods search this parent chain, for example when searching +for the context solid. You can also give a CadQuery object a tag, and further down your chain of CadQuery calls you +can refer back to this particular object using it's tag. + The Context Solid --------------------------- diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 46dfc4121..2057e58fa 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2742,3 +2742,89 @@ def test_assembleEdges(self): ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) + + def testTag(self): + + # test tagging + result = ( + Workplane("XY") + .pushPoints([(-2, 0), (2, 0)]) + .box(1, 1, 1, combine=False) + .tag("2 solids") + .union(Workplane("XY").box(6, 1, 1)) + ) + self.assertEqual(len(result.objects), 1) + result = result._getTagged("2 solids") + self.assertEqual(len(result.objects), 2) + + def testCopyWorkplane(self): + + obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() + obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1) + self.assertTupleAlmostEquals((0, 0, 5), obj1.val().Center().toTuple(), 9) + + def testWorkplaneFromTagged(self): + + # create a flat, wide base. Extrude one object 4 units high, another + # object ontop of it 6 units high. Go back to base plane. Extrude an + # object 11 units high. Assert that top face is 11 units high. + result = ( + Workplane("XY") + .box(10, 10, 1, centered=(True, True, False)) + .faces(">Z") + .workplane() + .tag("base") + .center(3, 0) + .rect(2, 2) + .extrude(4) + .faces(">Z") + .workplane() + .circle(1) + .extrude(6) + .workplaneFromTagged("base") + .center(-3, 0) + .circle(1) + .extrude(11) + ) + self.assertTupleAlmostEquals( + result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9 + ) + + def testTagSelectors(self): + + result0 = Workplane("XY").box(1, 1, 1).tag("box").sphere(1) + # result is currently a sphere + self.assertEqual(1, result0.faces().size()) + # a box has 8 vertices + self.assertEqual(8, result0.vertices(tag="box").size()) + # 6 faces + self.assertEqual(6, result0.faces(tag="box").size()) + # 12 edges + self.assertEqual(12, result0.edges(tag="box").size()) + # 6 wires + self.assertEqual(6, result0.wires(tag="box").size()) + + # create two solids, tag them, join to one solid + result1 = ( + Workplane("XY") + .pushPoints([(1, 0), (-1, 0)]) + .box(1, 1, 1) + .tag("boxes") + .sphere(1) + ) + self.assertEqual(1, result1.solids().size()) + self.assertEqual(2, result1.solids(tag="boxes").size()) + self.assertEqual(1, result1.shells().size()) + self.assertEqual(2, result1.shells(tag="boxes").size()) + + # create 4 individual objects, tag it, then combine to one compound + result2 = ( + Workplane("XY") + .rect(4, 4) + .vertices() + .box(1, 1, 1, combine=False) + .tag("4 objs") + ) + result2 = result2.newObject([Compound.makeCompound(result2.objects)]) + self.assertEqual(1, result2.compounds().size()) + self.assertEqual(0, result2.compounds(tag="4 objs").size()) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 049a5a4b6..4bddb8bae 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -28,7 +28,7 @@ def testWorkplaneCenter(self): self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) # move origin and confirm center moves - s.center(-2.0, -2.0) + s = s.center(-2.0, -2.0) # current point should be 0,0, but