1+ from typing import Callable , List , TypeVar
2+
13import cadquery as cq
24
35
4- def applyToEachFace (wp , f_workplane_selector , f_draw ):
6+ def applyToEachFace (
7+ wp : cq .Workplane ,
8+ f_workplane_selector : Callable [[cq .Face ], cq .Workplane ],
9+ f_draw : Callable [[cq .Workplane , cq .Face ], cq .Workplane ],
10+ ) -> cq .Workplane :
11+ """
12+ Basically equivalent to `Workplane.each(..)` but
13+ applicable only to faces and with tasks of face coordinate
14+ system selection and actually drawing in this coordinate
15+ system separated.
16+
17+ :param wp: Workplane with some faces selected
18+ :param f_workplane_selector: callback that accepts
19+ a face and returns a Workplane (a coordinate system to
20+ that is passed to `f_draw`). See `XAxisInPlane`
21+ and `XAxisClosestTo`
22+ :param f_draw: a callback that accepts a workplane and
23+ a face and draws something in that workplane
24+ """
25+
526 def each_callback (face ):
627 wp_face = f_workplane_selector (face )
728
@@ -47,16 +68,36 @@ def each_callback(face):
4768WORLD_AXIS_PLANES_ZX_YZ_XY = [WORLD_ZX_NORMAL , WORLD_YZ_NORMAL , WORLD_XY_NORMAL ]
4869
4970
50- def _create_workplane (v_center , v_xaxis , v_zaxis ):
71+ def _create_workplane (
72+ v_center : cq .Vector , v_xaxis : cq .Vector , v_zaxis : cq .Vector
73+ ) -> cq .Workplane :
5174 return cq .Workplane (cq .Plane (v_center , v_xaxis , v_zaxis ), origin = v_center )
5275
5376
5477class XAxisInPlane :
55- def __init__ (self , plane_normals , tolerance = 1e-3 ):
78+ """
79+ Selects face center of origin at face center
80+ (`Face.Center()`) and face normal at face
81+ center as Z axis.
82+
83+ Selects X axis as intersection of face plane and one of
84+ user provided planes (specified by their unit normal vectors).
85+
86+ The first one of the list that is not too close to
87+ being parallel to face plane is chosen.
88+
89+ User-provided plane normals do not
90+ have to be linearly independent but their span
91+ (linear hull) should be all 3D vector space
92+ for XAxisInPlane to work on arbitrary faces.
93+ In some cases this requirement can be relaxed.
94+ """
95+
96+ def __init__ (self , plane_normals : List [cq .Vector ], tolerance : float = 1e-3 ):
5697 self .__plane_normals = [x .normalized () for x in plane_normals ]
5798 self .__tolerance = tolerance
5899
59- def __call__ (self , face ) :
100+ def __call__ (self , face : cq . Face ) -> cq . Workplane :
60101 v_zaxis = face .normalAt ()
61102
62103 selected_plane_normal = None
@@ -69,20 +110,49 @@ def __call__(self, face):
69110 raise ValueError (
70111 "All plane normals are too close to face normal %s" % v_zaxis
71112 )
113+
72114 v_xaxis = selected_plane_normal .cross (v_zaxis )
73115
74116 return _create_workplane (face .Center (), v_xaxis , v_zaxis )
75117
76118
119+ T = TypeVar ("T" )
120+
121+
77122class XAxisClosestTo :
78- def __init__ (self , candidate_vectors , tolerance = 1e-3 ):
123+ """
124+ Selects face center of origin at face center
125+ (`Face.Center()`) and face normal at face
126+ center as Z axis.
127+
128+ Selects one of provided vectors with the smallest
129+ projection on face normal and choses normalized
130+ projection of that vector onto face plane as X axis.
131+
132+ If two or more vectors have the same and smallest
133+ projection on face normal the one that comes first
134+ in provided list is chosen.
135+
136+ User-provided vectors do not
137+ have to be linearly independent but their span
138+ (linear hull) should be all 3D vector space
139+ for XAxisInPlane to work on arbitrary faces.
140+ In some cases this requirement can be relaxed.
141+ """
142+
143+ def __init__ (self , candidate_vectors : List [cq .Vector ], tolerance : float = 1e-3 ):
79144
80145 self .__tolerance = tolerance
81146 self .__weighted_candidate_vectors = [
82147 (i , x .normalized ()) for i , x in enumerate (candidate_vectors )
83148 ]
84149
85- def __get_best_candidate (self , objectlist , key_selector , cluster_sort_key ):
150+ def __get_best_candidate (
151+ self ,
152+ objectlist : List [T ],
153+ key_selector : Callable [[T ], float ],
154+ cluster_sort_key : Callable [[T ], float ],
155+ ):
86156 # idea borrowed from
87157 # https://github.com/CadQuery/cadquery/blob/a71a93ea274089ddbd48dbbd84d84710fc82a432/cadquery/selectors.py#L343
88158 key_and_obj = []
@@ -101,15 +171,20 @@ def __get_best_candidate(self, objectlist, key_selector, cluster_sort_key):
101171
102172 return first_cluster [0 ]
103173
104- def __call__ (self , face ) :
174+ def __call__ (self , face : cq . Face ) -> cq . Workplane :
105175 v_zaxis = face .normalAt ()
106176
177+ # Choosing user-specified vector with minimum
178+ # face normal projection. If multiple vectors
179+ # have the same projection, the one that
180+ # comes first in the list is chosen
107181 best_xax_candidate = self .__get_best_candidate (
108182 self .__weighted_candidate_vectors ,
109183 lambda x : abs (x [1 ].dot (v_zaxis )),
110184 lambda x : x [0 ],
111185 )[1 ]
112186
187+ # projecting onto face plane and normalizing
113188 v_xaxis = (
114189 best_xax_candidate - v_zaxis .multiply (best_xax_candidate .dot (v_zaxis ))
115190 ).normalized ()
0 commit comments