CadQuery / CadQuery/cadquery

Proposal: Automatic rotational symmetry (orientation) of objects created/cut via eachpoint()

Open
#886 3 comments 0 reactions 0 assignees View on GitHub
question
Dominant language
Python
Stars
5.8k
Forks
541
Avg merge
3d 2h
Merged PRs (30d)
5

Description

I could be mistaken, but I didn't see any out-of-box flag for automatically orienting objects within eachpoint(), so I had to implement the functionality on top. If I've missed something, and this functionality is already available natively, my apologies for presuming otherwise...

Here's an example of what I mean:
![Screenshot from 2021-09-15 11-28-55](https://user-images.githubusercontent.com/1193219/133491599-75b66798-c7cd-456e-b4fc-1ed3dee22ee4.png)

Notice that the snapfits are all *oriented* towards the center. I've managed this with a plugin that wraps around the call to eachpoint(), and adjusts the orientation of the item being inserted/cut based on the angle of the point in the XY plane, wrt the origin, via the lambda function. It presently only works for the XY plane (i.e, symmetry around the Z axis), but it might be generalizable (as discussed below) to other axes. The reason I am posting this as an issue is because it ('face_origin: Tuple[bool, bool, bool]') might seem like a useful parameter that could exist directly on the eachpoint() method.

Meanwhile, here's the code that I've been using, that does what I've proposed above. (It actually needs this for both cutEach() and eachpoint() invocations, but that's an orthogonal matter). Note the automatic rotation that is being invoked during the lambda operations in each call to cutEach() and eachpoint(), which would not be needed if this was a default feature exposed by eachpoint().
```
def cut_and_insert(
self: cq.Workplane,
to_cut: cq.Solid = None,
to_insert: cq.Solid = None,
face_origin: Tuple[bool, bool, bool] = (False, False, True), # one bool for each axis to orient towards
clean: bool = True,
return_parts: bool = False):
"""
Aggregates functionality of cutEach() and eachpoint(). Also performs automatic rotation of the object for
each point processed, to maintain symmetry along the rotational axes.

:param self: workplane (as plugin)
:param to_cut: solid to cut, if any (default: None)
:param to_insert: solid to insert, if any (default: None)
:param face_origin: booleans for rotational symmetry (, , ). Automatically orients the objects to face the origin of the current workplane, along the axes selected. (default: (False, False, True))
:param clean: whether to clean after cutting (default: True)
:param return_parts: whether to return the intermediate objects - post_cut, additions, and final (default: False)
:return:
"""

assert to_cut is not None or to_insert is not None, "One of to_cut or to_insert is required"

def move_and_orient_copy(solid: cq.Solid, pose: cq.Location, face_origin: Tuple[bool, bool, bool]) -> cq.Solid:
solid = solid.moved(pose)
position, orientation = pose.toTuple()
solid = orient_to_origin(solid, cq.Vector(position), face_origin)
return solid

post_cut = self.cutEach(
lambda pose: move_and_orient_copy( <--------------------------- HERE --------
to_cut,
pose,
face_origin,
),
useLocalCoords=True,
clean=clean
) if to_cut is not None else self

additions = self.eachpoint(
lambda pose: move_and_orient_copy( <--------------------------- AND HERE --------
to_insert,
pose,
face_origin,
),
useLocalCoordinates=True
) if to_insert is not None else []

finished = post_cut.union(additions)

if return_parts:
return post_cut, additions, finished
else:
return finished
```

And the actual orientation is done by:
```
def orient_to_origin(to_orient: cq.Shape, position: cq.Vector, face_origin: Tuple[bool, bool, bool]) -> cq.Shape:
"""
Helper method that takes a shape and target position, and rotates the shape so that when it is placed at the given
position, it will be oriented toward the workplane's center.
:param to_orient: shape to be oriented
:param position: position that the shape is being considered for placement at
:param face_origin: a tuple of booleans, one for each axis of orientation. (Only rotation around the Z axis is presently supported).
:return: a copy of the shape, with required orientation
"""
oriented = to_orient

assert not face_origin[0] and not face_origin[1], "Right now symmetry is only supported along the Z-axis"
if face_origin[2]:
dx = position.x
dy = position.y
if dx == 0.0:
required_rotation_radians = math.pi / 2
elif dy == 0.0:
required_rotation_radians = 0.0
else:
required_rotation_radians = math.atan2(dy, dx)
oriented = (
to_orient
.rotate(
startVector=position,
endVector=position + cq.Vector(0, 0, 1), # Orient to Z-axis
angleDegrees=math.degrees(required_rotation_radians))
)
return oriented
```

If this feature were to be available directly on the eachpoint() API, I'd imagine it would look like the following:

eachpoint():
```
def eachpoint(
self: T,
callback: Callable[[Location], Shape],
useLocalCoordinates: bool = False,
face_origin: Tuple[bool, bool, bool], <-------------------- New parameter (or perhaps "rotational_symmetry")
) -> T:
```

This could then be exposed via other APIs that utilize eachpoint(). For example:
```
def cutEach(
self: T,
fcn: Callable[[Location], Shape],
useLocalCoords: bool = False,
clean: bool = True,
face_origin: Tuple[bool, bool, bool], <-------------------- New parameter
) -> T:
```

Thoughts? If this feature doesn't presently exist, and if there is consensus about its utility, I'd be happy to (when time permits) port/implement the relevant code and submit a PR.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.