Skip to content

Commit 214b14d

Browse files
committed
Collected code and examples from #713
- Changed first callback of apply_to_each_face(..) so that it not only determines x axis direction but also creates face workplane so that it can be extended in the future - Registered apply_to_each_face as a Workplane method Code not fully cleaned up. No type hints yet. And no comments. Did not test installer
1 parent 8154a1f commit 214b14d

File tree

7 files changed

+365
-0
lines changed

7 files changed

+365
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Apply TO Each Face plugin
2+
3+
4+
This plugin simplifies using
5+
`Workplane.each(..)` on faces.
6+
To use each you have to select workplane coordinate
7+
system for each face before building your geometry.
8+
`Workplane.apply_to_each_face()` function provided by this plugin
9+
separates tasks of choosing face coordinate system and
10+
actually building new geometry and provides a few built
11+
in ways of choosing coordinate system that are good
12+
enough in many cases.
13+
14+
15+
## Installation
16+
17+
```
18+
pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=apply_to_each_face&subdirectory=plugins/apply_to_each_face"
19+
```
20+
You can also clone the repository of the plugin and run in the repository the following command :
21+
```
22+
python setup.py install
23+
```
24+
25+
## Dependencies
26+
27+
This plugin has no dependencies other than the cadquery library.
28+
29+
## Usage
30+
31+
To use this plugin after it has been installed, just import it and use `apply_to_each_face(..)` function. `apply_to_each_face` has two arguments
32+
both of which are callbacks
33+
34+
1. `f_workplane_selector(face)` callback accepts a face and returns
35+
cadquery Workplane instance that is passed to the next callback.
36+
Two variants of this callback are provided both of which
37+
choose workplane center of origin at face center (`Face.Center()`),
38+
face normal at face center as Z axis. They have different
39+
methods of X axis selector
40+
1. `XAxisInPlane` (recommended in most cases) - a callable that chooses
41+
x axis that is simultaneously perpendicular to face normal at center
42+
and belongs to one of user-provided planes (specified by their normal vectors). Plane normal vectors are checked in the order they are provided and the first one that is not collinear with face normal is used.
43+
The plugin provides the following plane lists
44+
- `WORLD_AXIS_PLANES_XY_ZX_YZ`
45+
- `WORLD_AXIS_PLANES_XY_YZ_ZX`
46+
- `WORLD_AXIS_PLANES_YZ_XY_ZX`
47+
- `WORLD_AXIS_PLANES_YZ_ZX_XY`
48+
- `WORLD_AXIS_PLANES_ZX_XY_YZ`
49+
- `WORLD_AXIS_PLANES_ZX_YZ_XY`
50+
2. `XAxisClosestTo` - a callable that chooses x axis perpendicular
51+
to face normal at face center as close as possible to one
52+
of user-specified unit vectors (usually world coordinate
53+
system axis unit vectors).
54+
Vectors are checked in the order they are provided and the first one
55+
that is not collinear with face normal is used.
56+
The plugin provides the following vector lists
57+
- `WORLD_AXIS_UNIT_VECTORS_XYZ`
58+
- `WORLD_AXIS_UNIT_VECTORS_XZY`
59+
- `WORLD_AXIS_UNIT_VECTORS_YXZ`
60+
- `WORLD_AXIS_UNIT_VECTORS_YZX`
61+
- `WORLD_AXIS_UNIT_VECTORS_ZXY`
62+
- `WORLD_AXIS_UNIT_VECTORS_ZYX`
63+
64+
2. `f_draw(wp_face, face)` bui
65+
66+
User-provided vectors for both `XAxisInPlane` and `XAxisClosestTo` do not have to be linearly independent but their span (linear hull) should be all 3D vector space for these face coordinate system selectors to work on arbitrary faces. In some cases this requirement can be relaxed.
67+
68+
69+
## Example 1
70+
```python
71+
import cadquery as cq
72+
from apply_to_each_face import \
73+
XAxisInPlane,
74+
WORLD_AXIS_PLANES_XY_ZX_YZ
75+
76+
77+
def main_body():
78+
return cq.Workplane("XY")\
79+
.box(10.0,10.0,10.0,centered=(True,True,True))\
80+
.union(
81+
cq.Workplane("XY")
82+
.move(15,0)
83+
.box(10.0,10.0,10.0,centered=(True,True,True)))
84+
85+
result =\
86+
main_body().union(
87+
main_body()\
88+
.faces()\
89+
.apply_to_each_face(
90+
XAxisInPlane(WORLD_AXIS_PLANES_XY_ZX_YZ),
91+
lambda wp, face:\
92+
wp.rect(2,4).extrude(1)))
93+
```
94+
![example 1](ex1.png "Example 1")
95+
96+
97+
## Example 2
98+
```python
99+
import cadquery as cq
100+
from apply_to_each_face import \
101+
XAxisInPlane,
102+
WORLD_AXIS_PLANES_XY_ZX_YZ
103+
104+
def main_body():
105+
return cq.Workplane("XY")\
106+
.polygon(5, 10.0)\
107+
.extrude(5)
108+
109+
result =\
110+
main_body().union(
111+
main_body()\
112+
.faces("#Z")\
113+
.apply_to_each_face(
114+
XAxisInPlane(WORLD_AXIS_PLANES_XY_YZ_ZX),
115+
lambda wp, face:\
116+
wp.rect(1,2).extrude(3)))
117+
```
118+
![example 2](ex2.png "Example 2")
119+
120+
## Example 3
121+
```python
122+
def main_body():
123+
return cq.Workplane("XY")\
124+
.polygon(6, 10.0)\
125+
.extrude(3, taper=45)
126+
127+
result =\
128+
main_body().faces("<Z").shell(-0.5).cut(
129+
main_body()\
130+
.faces("not <Z")\
131+
.apply_to_each_face(
132+
XAxisInPlane(WORLD_AXIS_PLANES_XY_ZX_YZ),
133+
lambda wp, face:\
134+
wp.add(face)\
135+
.wires()\
136+
.toPending()\
137+
.offset2D(-0.8)\
138+
.extrude(-2)))
139+
```
140+
![example 3](ex3.png "Example 3")
141+

plugins/apply_to_each_face/__init__.py

Whitespace-only changes.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import cadquery as cq
2+
3+
def apply_to_each_face(wp, f_workplane_selector, f_draw):
4+
def each_callback(face):
5+
wp_face = f_workplane_selector(face)
6+
7+
return f_draw(wp_face, face).vals()[0]
8+
9+
return wp.each(each_callback)
10+
11+
12+
v_x_unit = cq.Vector(1,0,0)
13+
v_y_unit = cq.Vector(0,1,0)
14+
v_z_unit = cq.Vector(0,0,1)
15+
16+
# ---- VECTORS ----
17+
18+
WORLD_AXIS_UNIT_VECTORS_XYZ = \
19+
[v_x_unit,
20+
v_y_unit,
21+
v_z_unit]
22+
23+
WORLD_AXIS_UNIT_VECTORS_XZY = \
24+
[v_x_unit,
25+
v_z_unit,
26+
v_y_unit]
27+
28+
WORLD_AXIS_UNIT_VECTORS_YXZ = \
29+
[v_y_unit,
30+
v_x_unit,
31+
v_z_unit]
32+
33+
WORLD_AXIS_UNIT_VECTORS_YZX = \
34+
[v_y_unit,
35+
v_z_unit,
36+
v_x_unit]
37+
38+
WORLD_AXIS_UNIT_VECTORS_ZXY = \
39+
[v_z_unit,
40+
v_x_unit,
41+
v_y_unit]
42+
43+
WORLD_AXIS_UNIT_VECTORS_ZYX = \
44+
[v_z_unit,
45+
v_y_unit,
46+
v_x_unit]
47+
48+
# ---- PLANES ----
49+
50+
WORLD_XY_NORMAL = v_z_unit
51+
WORLD_ZX_NORMAL = v_y_unit
52+
WORLD_YZ_NORMAL = v_x_unit
53+
54+
WORLD_AXIS_PLANES_XY_ZX_YZ = \
55+
[WORLD_XY_NORMAL,
56+
WORLD_ZX_NORMAL,
57+
WORLD_YZ_NORMAL]
58+
59+
WORLD_AXIS_PLANES_XY_YZ_ZX = \
60+
[WORLD_XY_NORMAL,
61+
WORLD_YZ_NORMAL,
62+
WORLD_ZX_NORMAL]
63+
64+
WORLD_AXIS_PLANES_YZ_XY_ZX = \
65+
[WORLD_YZ_NORMAL,
66+
WORLD_XY_NORMAL,
67+
WORLD_ZX_NORMAL]
68+
69+
WORLD_AXIS_PLANES_YZ_ZX_XY = \
70+
[WORLD_YZ_NORMAL,
71+
WORLD_ZX_NORMAL,
72+
WORLD_XY_NORMAL]
73+
74+
WORLD_AXIS_PLANES_ZX_XY_YZ = \
75+
[WORLD_ZX_NORMAL,
76+
WORLD_XY_NORMAL,
77+
WORLD_YZ_NORMAL]
78+
79+
WORLD_AXIS_PLANES_ZX_YZ_XY = \
80+
[WORLD_ZX_NORMAL,
81+
WORLD_YZ_NORMAL,
82+
WORLD_XY_NORMAL]
83+
84+
def _create_workplane(v_center, v_xaxis, v_zaxis):
85+
return cq.Workplane(
86+
cq.Plane(
87+
v_center,
88+
v_xaxis,
89+
v_zaxis),
90+
origin=v_center)
91+
92+
class XAxisInPlane:
93+
def __init__(self, plane_normals, tolerance=1e-3):
94+
self.__plane_normals = [x.normalized() for x in plane_normals]
95+
self.__tolerance = tolerance
96+
97+
def __call__(self, face):
98+
v_zaxis = face.normalAt()
99+
100+
selected_plane_normal = None
101+
for plane_normal in self.__plane_normals:
102+
plane_normal_projection = plane_normal.dot(v_zaxis)
103+
if (1-abs(plane_normal_projection)) > self.__tolerance:
104+
selected_plane_normal = plane_normal
105+
break
106+
if selected_plane_normal is None:
107+
raise ValueError(
108+
"All plane normals are too close to face normal %s" % v_zaxis)
109+
v_xaxis = selected_plane_normal.cross(v_zaxis)
110+
111+
return _create_workplane(
112+
face.Center(),
113+
v_xaxis,
114+
v_zaxis)
115+
116+
class XAxisClosestTo:
117+
def __init__(self,
118+
candidate_vectors,
119+
tolerance = 1e-3):
120+
121+
self.__tolerance = tolerance
122+
self.__weighted_candidate_vectors = \
123+
[(i, x.normalized()) \
124+
for i, x in enumerate(candidate_vectors)]
125+
126+
def __get_best_candidate(
127+
self,
128+
objectlist,
129+
key_selector,
130+
cluster_sort_key):
131+
# idea borrowed from
132+
# https://github.com/CadQuery/cadquery/blob/a71a93ea274089ddbd48dbbd84d84710fc82a432/cadquery/selectors.py#L343
133+
key_and_obj = []
134+
for obj in objectlist:
135+
key_and_obj.append((key_selector(obj), obj))
136+
key_and_obj.sort(key=lambda x: x[0])
137+
138+
first_cluster = []
139+
start = key_and_obj[0][0]
140+
for key, obj in key_and_obj:
141+
if abs(key - start) <= self.__tolerance:
142+
first_cluster.append(obj)
143+
else:
144+
break
145+
first_cluster.sort(key=cluster_sort_key)
146+
147+
return first_cluster[0]
148+
149+
def __call__(self, face):
150+
v_zaxis = face.normalAt()
151+
152+
best_xax_candidate = \
153+
self.__get_best_candidate(
154+
self.__weighted_candidate_vectors,
155+
lambda x: abs(x[1].dot(v_zaxis)),
156+
lambda x: x[0])[1]
157+
158+
v_xaxis = (best_xax_candidate \
159+
- v_zaxis.multiply(best_xax_candidate.dot(v_zaxis)))\
160+
.normalized()
161+
162+
return _create_workplane(
163+
face.Center(),
164+
v_xaxis,
165+
v_zaxis)
166+
167+
168+
cq.Workplane.apply_to_each_face = apply_to_each_face

plugins/apply_to_each_face/ex1.png

14.1 KB
Loading

plugins/apply_to_each_face/ex2.png

12.3 KB
Loading

plugins/apply_to_each_face/ex3.png

23.5 KB
Loading
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from setuptools import setup, find_packages
2+
3+
version = "1.0.0"
4+
plugin_name = "apply_to_each_face" # The name of your plugin
5+
description = "Building the same geometry on each selected face"
6+
long_description = "This plugin "+\
7+
"simplifies using each(..) on faces. To use each "+\
8+
"you have to select workplane coordinate system for each"+\
9+
"face before building your geometry. "+\
10+
"apply_to_each_face() function provided by this plugin "+\
11+
"separates tasks of choosing face coordinate system and "+\
12+
"actually building new geometry and provides a few built in "+\
13+
"ways of choosing coordinate system that are good enough in "+\
14+
"many cases."
15+
author = "Fedor Kotov"
16+
author_email = "fedorkotov@gmail.com"
17+
packages = [] # List of packages that will be installed with this plugin
18+
py_modules = ["apply_to_each_face"] # Put the name of your plugin's .py file here
19+
install_requires = (
20+
[]
21+
) # Any dependencies that pip also needs to install to make this plugin work
22+
23+
24+
setup(
25+
name=plugin_name,
26+
version=version,
27+
url="https://github.com/CadQuery/cadquery-plugins",
28+
license="Apache Public License 2.0",
29+
author=author,
30+
author_email=author_email,
31+
description=description,
32+
long_description=long_description,
33+
packages=packages,
34+
py_modules=py_modules,
35+
install_requires=install_requires,
36+
include_package_data=True,
37+
zip_safe=False,
38+
platforms="any",
39+
test_suite="tests",
40+
classifiers=[
41+
"Development Status :: 5 - Production/Stable",
42+
"Intended Audience :: Developers",
43+
"Intended Audience :: End Users/Desktop",
44+
"Intended Audience :: Information Technology",
45+
"Intended Audience :: Science/Research",
46+
"Intended Audience :: System Administrators",
47+
"License :: OSI Approved :: Apache Software License",
48+
"Operating System :: POSIX",
49+
"Operating System :: MacOS",
50+
"Operating System :: Unix",
51+
"Programming Language :: Python",
52+
"Topic :: Software Development :: Libraries :: Python Modules",
53+
"Topic :: Internet",
54+
"Topic :: Scientific/Engineering",
55+
],
56+
)

0 commit comments

Comments
 (0)