Skip to content

Commit 9ee703d

Browse files
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 <[email protected]> * Typo fix Co-authored-by: Matti Eiden <[email protected]> --------- Co-authored-by: AU <[email protected]>
1 parent d9ccd25 commit 9ee703d

File tree

4 files changed

+200
-4
lines changed

4 files changed

+200
-4
lines changed

cadquery/cq.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
Dict,
3636
)
3737
from typing_extensions import Literal
38-
from inspect import Parameter, Signature
38+
from inspect import Parameter, Signature, isbuiltin
3939

4040

4141
from .occ_impl.geom import Vector, Plane, Location
@@ -4430,6 +4430,82 @@ def _repr_javascript_(self) -> Any:
44304430
_selectShapes(self.objects)
44314431
)._repr_javascript_()
44324432

4433+
def __getitem__(self: T, item: Union[int, Sequence[int], slice]) -> T:
4434+
4435+
if isinstance(item, Iterable):
4436+
rv = self.newObject(self.objects[i] for i in item)
4437+
elif isinstance(item, slice):
4438+
rv = self.newObject(self.objects[item])
4439+
else:
4440+
rv = self.newObject([self.objects[item]])
4441+
4442+
return rv
4443+
4444+
def filter(self: T, f: Callable[[CQObject], bool]) -> T:
4445+
"""
4446+
Filter items using a boolean predicate.
4447+
:param f: Callable to be used for filtering.
4448+
:return: Workplane object with filtered items.
4449+
"""
4450+
4451+
return self.newObject(filter(f, self.objects))
4452+
4453+
def map(self: T, f: Callable[[CQObject], CQObject]):
4454+
"""
4455+
Apply a callable to every item separately.
4456+
:param f: Callable to be applied to every item separately.
4457+
:return: Workplane object with f applied to all items.
4458+
"""
4459+
4460+
return self.newObject(map(f, self.objects))
4461+
4462+
def apply(self: T, f: Callable[[Iterable[CQObject]], Iterable[CQObject]]):
4463+
"""
4464+
Apply a callable to all items at once.
4465+
:param f: Callable to be applied.
4466+
:return: Workplane object with f applied to all items.
4467+
"""
4468+
4469+
return self.newObject(f(self.objects))
4470+
4471+
def sort(self: T, key: Callable[[CQObject], Any]) -> T:
4472+
"""
4473+
Sort items using a callable.
4474+
:param key: Callable to be used for sorting.
4475+
:return: Workplane object with items sorted.
4476+
"""
4477+
4478+
return self.newObject(sorted(self.objects, key=key))
4479+
4480+
def invoke(
4481+
self: T, f: Union[Callable[[T], T], Callable[[T], None], Callable[[], None]]
4482+
):
4483+
"""
4484+
Invoke a callable mapping Workplane to Workplane or None. Supports also
4485+
callables that take no arguments such as breakpoint. Returns self if callable
4486+
returns None.
4487+
:param f: Callable to be invoked.
4488+
:return: Workplane object.
4489+
"""
4490+
4491+
if isbuiltin(f):
4492+
arity = 0 # assume 0 arity for builtins; they cannot be introspected
4493+
else:
4494+
arity = f.__code__.co_argcount # NB: this is not understood by mypy
4495+
4496+
rv = self
4497+
4498+
if arity == 0:
4499+
f() # type: ignore
4500+
elif arity == 1:
4501+
res = f(self) # type: ignore
4502+
if res is not None:
4503+
rv = res
4504+
else:
4505+
raise ValueError("Provided function {f} accepts too many arguments")
4506+
4507+
return rv
4508+
44334509

44344510
# alias for backward compatibility
44354511
CQ = Workplane

doc/extending.rst

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ This ultra simple plugin makes cubes of the specified size for each stack point.
163163

164164
(The cubes are off-center because the boxes have their lower left corner at the reference points.)
165165

166-
.. code-block:: python
166+
.. cadquery::
167+
168+
from cadquery.occ_impl.shapes import box
167169

168170
def makeCubes(self, length):
169171
# 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.
172174
def _singleCube(loc):
173175
# loc is a location in local coordinates
174176
# since we're using eachpoint with useLocalCoordinates=True
175-
return cq.Solid.makeBox(length, length, length, pnt).locate(loc)
177+
return box(length, length, length).locate(loc)
176178

177179
# use CQ utility method to iterate over the stack, call our
178180
# 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.
193195
.combineSolids()
194196
)
195197

198+
199+
Extending CadQuery: Special Methods
200+
-----------------------------------
201+
202+
The above-mentioned approach has one drawback, it requires monkey-patching or subclassing. To avoid this
203+
one can also use the following special methods of :py:class:`cadquery.Workplane`
204+
and write plugins in a more functional style.
205+
206+
* :py:meth:`cadquery.Workplane.map`
207+
* :py:meth:`cadquery.Workplane.apply`
208+
* :py:meth:`cadquery.Workplane.invoke`
209+
210+
Here is the same plugin rewritten using one of those methods.
211+
212+
.. cadquery::
213+
214+
from cadquery.occ_impl.shapes import box
215+
216+
def makeCubes(length):
217+
218+
# inner method that creates the cubes
219+
def callback(wp):
220+
221+
return wp.eachpoint(box(length, length, length), True)
222+
223+
return callback
224+
225+
# use the plugin
226+
result = (
227+
cq.Workplane("XY")
228+
.box(6.0, 8.0, 0.5)
229+
.faces(">Z")
230+
.rect(4.0, 4.0, forConstruction=True)
231+
.vertices()
232+
.invoke(makeCubes(1.0))
233+
.combineSolids()
234+
)
235+
236+
Such an approach is more friendly for auto-completion and static analysis tools.

doc/selectors.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,45 @@ objects. This includes chaining and combining.
185185
# select top and bottom wires
186186
result = box.faces(">Z or <Z").wires()
187187

188+
189+
190+
191+
Additional special methods
192+
--------------------------
193+
194+
:py:class:`cadquery.Workplane` provides the following special methods that can be used
195+
for quick prototyping of selectors when implementing a complete selector via subclassing of
196+
:py:class:`cadquery.Selector` is not desirable.
197+
198+
* :py:meth:`cadquery.Workplane.filter`
199+
* :py:meth:`cadquery.Workplane.sort`
200+
* :py:meth:`cadquery.Workplane.__getitem__`
201+
202+
For example, one could use those methods for selecting objects within a certain range of volumes.
203+
204+
.. cadquery::
205+
206+
from cadquery.occ_impl.shapes import box
207+
208+
result = (
209+
cq.Workplane()
210+
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
211+
)
212+
213+
# select boxes with volume <= 3
214+
result = result.filter(lambda s: s.Volume() <= 3)
215+
216+
217+
The same can be achieved using sorting.
218+
219+
.. cadquery::
220+
221+
from cadquery.occ_impl.shapes import box
222+
223+
result = (
224+
cq.Workplane()
225+
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
226+
)
227+
228+
# select boxes with volume <= 3
229+
result = result.sort(lambda s: s.Volume())[:3]

tests/test_cadquery.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1786,7 +1786,7 @@ def testBoundingBox(self):
17861786

17871787
def testBoundBoxEnlarge(self):
17881788
"""
1789-
Tests BoundBox.enlarge(). Confirms that the
1789+
Tests BoundBox.enlarge(). Confirms that the
17901790
bounding box lengths are all enlarged by the
17911791
correct amount.
17921792
"""
@@ -5741,6 +5741,43 @@ def test_iterators(self):
57415741
res7 = list(fs.siblings(c, "Edge", 2))
57425742
assert len(res7) == 2
57435743

5744+
def test_map_apply_filter_sort(self):
5745+
5746+
w = Workplane().box(1, 1, 1).moveTo(3, 0).box(1, 1, 3).solids()
5747+
5748+
assert w.filter(lambda s: s.Volume() > 2).size() == 1
5749+
assert w.filter(lambda s: s.Volume() > 5).size() == 0
5750+
5751+
assert w.sort(lambda s: -s.Volume())[-1].val().Volume() == approx(1)
5752+
5753+
assert w.apply(lambda obj: []).size() == 0
5754+
5755+
assert w.map(lambda s: s.faces(">Z")).faces().size() == 2
5756+
5757+
def test_getitem(self):
5758+
5759+
w = Workplane().rarray(2, 1, 5, 1).box(1, 1, 1, combine=False)
5760+
5761+
assert w[0].solids().size() == 1
5762+
assert w[-2:].solids().size() == 2
5763+
assert w[[0, 1]].solids().size() == 2
5764+
5765+
def test_invoke(self):
5766+
5767+
w = Workplane().rarray(2, 1, 5, 1).box(1, 1, 1, combine=False)
5768+
5769+
# builtin
5770+
assert w.invoke(print).size() == 5
5771+
# arity 0
5772+
assert w.invoke(lambda: 1).size() == 5
5773+
# arity 1 and no return
5774+
assert w.invoke(lambda x: None).size() == 5
5775+
# arity 1
5776+
assert w.invoke(lambda x: x.newObject([x.val()])).size() == 1
5777+
# test exception with wrong arity
5778+
with raises(ValueError):
5779+
w.invoke(lambda x, y: 1)
5780+
57445781
def test_tessellate(self):
57455782

57465783
# happy flow

0 commit comments

Comments
 (0)