Proposal: Automatic rotational symmetry (orientation) of objects created/cut via eachpoint()
- 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:

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.