Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
77 changes: 76 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,81 @@ 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, Workplane to None or
without any parameters. In the two latter cases self is returned.
: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 arguemnts")

return rv
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not requesting this to be changed, but I think something like this would be a bit more mundane...?

Suggested change
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 arguemnts")
return rv
rv = self
try:
res = f(self) # type: ignore
except TypeError:
res = f() # type: ignore
if res is not None:
rv = res
return rv

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, but I find mine more clear. Also it has slightly different semantics, f could throw for other reason than wrong number of parameters.



# 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 if 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