diff --git a/cadquery/cq.py b/cadquery/cq.py index b853fa72..76a0f98a 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -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 diff --git a/doc/extending.rst b/doc/extending.rst index 4de6d0b0..a0fb0cd1 100644 --- a/doc/extending.rst +++ b/doc/extending.rst @@ -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. diff --git a/doc/selectors.rst b/doc/selectors.rst index 34bdf4ba..9670526b 100644 --- a/doc/selectors.rst +++ b/doc/selectors.rst @@ -185,3 +185,45 @@ objects. This includes chaining and combining. # select top and bottom wires result = box.faces(">Z or 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