filter/map/apply/sort/[]/invoke (#1514)
* PoC commit for discussion * Rework Remove group Rename More generic [] Add apply / change map * Iterable check fix * typing fix * Add docstrings * Add tests * Black fix * fix test * Another fix * Test fix * Add invoke * Support builtins * Add test for invoke * Better coverage * Add some docs on ad-hoc selection * Mention special methods for extending * Typo fix * Better docstring and typos Co-authored-by: Matti Eiden <snaipperi@gmail.com> * Typo fix Co-authored-by: Matti Eiden <snaipperi@gmail.com> --------- Co-authored-by: AU <adam-urbanczyk@users.noreply.github.com>
This commit is contained in:
@ -35,7 +35,7 @@ from typing import (
|
||||
Dict,
|
||||
)
|
||||
from typing_extensions import Literal
|
||||
from inspect import Parameter, Signature
|
||||
from inspect import Parameter, Signature, isbuiltin
|
||||
|
||||
|
||||
from .occ_impl.geom import Vector, Plane, Location
|
||||
@ -4430,6 +4430,82 @@ class Workplane(object):
|
||||
_selectShapes(self.objects)
|
||||
)._repr_javascript_()
|
||||
|
||||
def __getitem__(self: T, item: Union[int, Sequence[int], slice]) -> T:
|
||||
|
||||
if isinstance(item, Iterable):
|
||||
rv = self.newObject(self.objects[i] for i in item)
|
||||
elif isinstance(item, slice):
|
||||
rv = self.newObject(self.objects[item])
|
||||
else:
|
||||
rv = self.newObject([self.objects[item]])
|
||||
|
||||
return rv
|
||||
|
||||
def filter(self: T, f: Callable[[CQObject], bool]) -> T:
|
||||
"""
|
||||
Filter items using a boolean predicate.
|
||||
:param f: Callable to be used for filtering.
|
||||
:return: Workplane object with filtered items.
|
||||
"""
|
||||
|
||||
return self.newObject(filter(f, self.objects))
|
||||
|
||||
def map(self: T, f: Callable[[CQObject], CQObject]):
|
||||
"""
|
||||
Apply a callable to every item separately.
|
||||
:param f: Callable to be applied to every item separately.
|
||||
:return: Workplane object with f applied to all items.
|
||||
"""
|
||||
|
||||
return self.newObject(map(f, self.objects))
|
||||
|
||||
def apply(self: T, f: Callable[[Iterable[CQObject]], Iterable[CQObject]]):
|
||||
"""
|
||||
Apply a callable to all items at once.
|
||||
:param f: Callable to be applied.
|
||||
:return: Workplane object with f applied to all items.
|
||||
"""
|
||||
|
||||
return self.newObject(f(self.objects))
|
||||
|
||||
def sort(self: T, key: Callable[[CQObject], Any]) -> T:
|
||||
"""
|
||||
Sort items using a callable.
|
||||
:param key: Callable to be used for sorting.
|
||||
:return: Workplane object with items sorted.
|
||||
"""
|
||||
|
||||
return self.newObject(sorted(self.objects, key=key))
|
||||
|
||||
def invoke(
|
||||
self: T, f: Union[Callable[[T], T], Callable[[T], None], Callable[[], None]]
|
||||
):
|
||||
"""
|
||||
Invoke a callable mapping Workplane to Workplane or None. Supports also
|
||||
callables that take no arguments such as breakpoint. Returns self if callable
|
||||
returns None.
|
||||
:param f: Callable to be invoked.
|
||||
:return: Workplane object.
|
||||
"""
|
||||
|
||||
if isbuiltin(f):
|
||||
arity = 0 # assume 0 arity for builtins; they cannot be introspected
|
||||
else:
|
||||
arity = f.__code__.co_argcount # NB: this is not understood by mypy
|
||||
|
||||
rv = self
|
||||
|
||||
if arity == 0:
|
||||
f() # type: ignore
|
||||
elif arity == 1:
|
||||
res = f(self) # type: ignore
|
||||
if res is not None:
|
||||
rv = res
|
||||
else:
|
||||
raise ValueError("Provided function {f} accepts too many arguments")
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
# alias for backward compatibility
|
||||
CQ = Workplane
|
||||
|
||||
@ -163,7 +163,9 @@ This ultra simple plugin makes cubes of the specified size for each stack point.
|
||||
|
||||
(The cubes are off-center because the boxes have their lower left corner at the reference points.)
|
||||
|
||||
.. code-block:: python
|
||||
.. cadquery::
|
||||
|
||||
from cadquery.occ_impl.shapes import box
|
||||
|
||||
def makeCubes(self, length):
|
||||
# self refers to the CQ or Workplane object
|
||||
@ -172,7 +174,7 @@ This ultra simple plugin makes cubes of the specified size for each stack point.
|
||||
def _singleCube(loc):
|
||||
# loc is a location in local coordinates
|
||||
# since we're using eachpoint with useLocalCoordinates=True
|
||||
return cq.Solid.makeBox(length, length, length, pnt).locate(loc)
|
||||
return box(length, length, length).locate(loc)
|
||||
|
||||
# use CQ utility method to iterate over the stack, call our
|
||||
# method, and convert to/from local coordinates.
|
||||
@ -193,3 +195,42 @@ This ultra simple plugin makes cubes of the specified size for each stack point.
|
||||
.combineSolids()
|
||||
)
|
||||
|
||||
|
||||
Extending CadQuery: Special Methods
|
||||
-----------------------------------
|
||||
|
||||
The above-mentioned approach has one drawback, it requires monkey-patching or subclassing. To avoid this
|
||||
one can also use the following special methods of :py:class:`cadquery.Workplane`
|
||||
and write plugins in a more functional style.
|
||||
|
||||
* :py:meth:`cadquery.Workplane.map`
|
||||
* :py:meth:`cadquery.Workplane.apply`
|
||||
* :py:meth:`cadquery.Workplane.invoke`
|
||||
|
||||
Here is the same plugin rewritten using one of those methods.
|
||||
|
||||
.. cadquery::
|
||||
|
||||
from cadquery.occ_impl.shapes import box
|
||||
|
||||
def makeCubes(length):
|
||||
|
||||
# inner method that creates the cubes
|
||||
def callback(wp):
|
||||
|
||||
return wp.eachpoint(box(length, length, length), True)
|
||||
|
||||
return callback
|
||||
|
||||
# use the plugin
|
||||
result = (
|
||||
cq.Workplane("XY")
|
||||
.box(6.0, 8.0, 0.5)
|
||||
.faces(">Z")
|
||||
.rect(4.0, 4.0, forConstruction=True)
|
||||
.vertices()
|
||||
.invoke(makeCubes(1.0))
|
||||
.combineSolids()
|
||||
)
|
||||
|
||||
Such an approach is more friendly for auto-completion and static analysis tools.
|
||||
|
||||
@ -185,3 +185,45 @@ objects. This includes chaining and combining.
|
||||
# select top and bottom wires
|
||||
result = box.faces(">Z or <Z").wires()
|
||||
|
||||
|
||||
|
||||
|
||||
Additional special methods
|
||||
--------------------------
|
||||
|
||||
:py:class:`cadquery.Workplane` provides the following special methods that can be used
|
||||
for quick prototyping of selectors when implementing a complete selector via subclassing of
|
||||
:py:class:`cadquery.Selector` is not desirable.
|
||||
|
||||
* :py:meth:`cadquery.Workplane.filter`
|
||||
* :py:meth:`cadquery.Workplane.sort`
|
||||
* :py:meth:`cadquery.Workplane.__getitem__`
|
||||
|
||||
For example, one could use those methods for selecting objects within a certain range of volumes.
|
||||
|
||||
.. cadquery::
|
||||
|
||||
from cadquery.occ_impl.shapes import box
|
||||
|
||||
result = (
|
||||
cq.Workplane()
|
||||
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
|
||||
)
|
||||
|
||||
# select boxes with volume <= 3
|
||||
result = result.filter(lambda s: s.Volume() <= 3)
|
||||
|
||||
|
||||
The same can be achieved using sorting.
|
||||
|
||||
.. cadquery::
|
||||
|
||||
from cadquery.occ_impl.shapes import box
|
||||
|
||||
result = (
|
||||
cq.Workplane()
|
||||
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
|
||||
)
|
||||
|
||||
# select boxes with volume <= 3
|
||||
result = result.sort(lambda s: s.Volume())[:3]
|
||||
|
||||
@ -1786,7 +1786,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testBoundBoxEnlarge(self):
|
||||
"""
|
||||
Tests BoundBox.enlarge(). Confirms that the
|
||||
Tests BoundBox.enlarge(). Confirms that the
|
||||
bounding box lengths are all enlarged by the
|
||||
correct amount.
|
||||
"""
|
||||
@ -5741,6 +5741,43 @@ class TestCadQuery(BaseTest):
|
||||
res7 = list(fs.siblings(c, "Edge", 2))
|
||||
assert len(res7) == 2
|
||||
|
||||
def test_map_apply_filter_sort(self):
|
||||
|
||||
w = Workplane().box(1, 1, 1).moveTo(3, 0).box(1, 1, 3).solids()
|
||||
|
||||
assert w.filter(lambda s: s.Volume() > 2).size() == 1
|
||||
assert w.filter(lambda s: s.Volume() > 5).size() == 0
|
||||
|
||||
assert w.sort(lambda s: -s.Volume())[-1].val().Volume() == approx(1)
|
||||
|
||||
assert w.apply(lambda obj: []).size() == 0
|
||||
|
||||
assert w.map(lambda s: s.faces(">Z")).faces().size() == 2
|
||||
|
||||
def test_getitem(self):
|
||||
|
||||
w = Workplane().rarray(2, 1, 5, 1).box(1, 1, 1, combine=False)
|
||||
|
||||
assert w[0].solids().size() == 1
|
||||
assert w[-2:].solids().size() == 2
|
||||
assert w[[0, 1]].solids().size() == 2
|
||||
|
||||
def test_invoke(self):
|
||||
|
||||
w = Workplane().rarray(2, 1, 5, 1).box(1, 1, 1, combine=False)
|
||||
|
||||
# builtin
|
||||
assert w.invoke(print).size() == 5
|
||||
# arity 0
|
||||
assert w.invoke(lambda: 1).size() == 5
|
||||
# arity 1 and no return
|
||||
assert w.invoke(lambda x: None).size() == 5
|
||||
# arity 1
|
||||
assert w.invoke(lambda x: x.newObject([x.val()])).size() == 1
|
||||
# test exception with wrong arity
|
||||
with raises(ValueError):
|
||||
w.invoke(lambda x, y: 1)
|
||||
|
||||
def test_tessellate(self):
|
||||
|
||||
# happy flow
|
||||
|
||||
Reference in New Issue
Block a user