Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

filter/map/apply/sort/[]/invoke #1514

Merged
merged 20 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -4430,6 +4430,82 @@ def _repr_javascript_(self) -> Any:
_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
45 changes: 43 additions & 2 deletions doc/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
42 changes: 42 additions & 0 deletions doc/selectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]
39 changes: 38 additions & 1 deletion tests/test_cadquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -1786,7 +1786,7 @@ def testBoundingBox(self):

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.
"""
Expand Down Expand Up @@ -5741,6 +5741,43 @@ def test_iterators(self):
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
Expand Down