-
Notifications
You must be signed in to change notification settings - Fork 291
/
cq.py
3776 lines (3005 loc) · 140 KB
/
cq.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""
import math
from copy import copy
from itertools import chain
from typing import (
overload,
Sequence,
Union,
Tuple,
Optional,
Any,
Iterable,
Callable,
List,
cast,
Dict,
)
from typing_extensions import Literal
from .occ_impl.geom import Vector, Plane, Location
from .occ_impl.shapes import (
Shape,
Edge,
Wire,
Face,
Solid,
Compound,
sortWiresByBuildOrder,
)
from .occ_impl.exporters.svg import getSVG, exportSVG
from .utils import deprecate_kwarg, deprecate
from .selectors import (
Selector,
PerpendicularDirSelector,
NearestToPointSelector,
StringSyntaxSelector,
)
CQObject = Union[Vector, Location, Shape]
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]
def _selectShapes(objects: Iterable[Any]) -> List[Shape]:
return [el for el in objects if isinstance(el, Shape)]
class CQContext(object):
"""
A shared context for modeling.
All objects in the same CQ chain share a reference to this same object instance
which allows for shared state when needed.
"""
pendingWires: List[Wire]
pendingEdges: List[Edge]
firstPoint: Optional[Vector]
tolerance: float
tags: Dict[str, "Workplane"]
def __init__(self):
self.pendingWires = (
[]
) # a list of wires that have been created and need to be extruded
# a list of created pending edges that need to be joined into wires
self.pendingEdges = []
# a reference to the first point for a set of edges.
# Used to determine how to behave when close() is called
self.firstPoint = None
self.tolerance = 0.0001 # user specified tolerance
self.tags = {}
class Workplane(object):
"""
Defines a coordinate system in space, in which 2-d coordinates can be used.
:param plane: the plane in which the workplane will be done
:type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
:param origin: the desired origin of the new workplane
:type origin: a 3-tuple in global coordinates, or None to default to the origin
:param obj: an object to use initially for the stack
:type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial
stack value.
:raises: ValueError if the provided plane is not a plane, a valid named workplane
:return: A Workplane object, with coordinate system matching the supplied plane.
The most common use is::
s = Workplane("XY")
After creation, the stack contains a single point, the origin of the underlying plane,
and the *current point* is on the origin.
.. note::
You can also create workplanes on the surface of existing faces using
:py:meth:`CQ.workplane`
"""
objects: List[CQObject]
ctx: CQContext
parent: Optional["Workplane"]
plane: Plane
_tag: Optional[str]
@overload
def __init__(self, obj: CQObject) -> None:
...
@overload
def __init__(
self,
inPlane: Union[Plane, str] = "XY",
origin: VectorLike = (0, 0, 0),
obj: Optional[CQObject] = None,
) -> None:
...
def __init__(self, inPlane="XY", origin=(0, 0, 0), obj=None):
"""
make a workplane from a particular plane
:param inPlane: the plane in which the workplane will be done
:type inPlane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
:param origin: the desired origin of the new workplane
:type origin: a 3-tuple in global coordinates, or None to default to the origin
:param obj: an object to use initially for the stack
:type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial
stack value.
:raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
:return: A Workplane object, with coordinate system matching the supplied plane.
The most common use is::
s = Workplane("XY")
After creation, the stack contains a single point, the origin of the underlying plane, and
the *current point* is on the origin.
"""
if isinstance(inPlane, Plane):
tmpPlane = inPlane
elif isinstance(inPlane, str):
tmpPlane = Plane.named(inPlane, origin)
elif isinstance(inPlane, (Vector, Location, Shape)):
obj = inPlane
tmpPlane = Plane.named("XY", origin)
else:
raise ValueError(
"Provided value {} is not a valid work plane".format(inPlane)
)
self.plane = tmpPlane
# Changed so that workplane has the center as the first item on the stack
if obj:
self.objects = [obj]
else:
self.objects = []
self.parent = None
self.ctx = CQContext()
self._tag = None
def tag(self, name: str) -> "Workplane":
"""
Tags the current CQ object for later reference.
:param name: the name to tag this object with
:type name: string
:returns: self, a cq object with tag applied
"""
self._tag = name
self.ctx.tags[name] = self
return self
def _collectProperty(self, propName: str) -> List[CQObject]:
"""
Collects all of the values for propName,
for all items on the stack.
OCCT objects do not implement id correctly,
so hashCode is used to ensure we don't add the same
object multiple times.
One weird use case is that the stack could have a solid reference object
on it. This is meant to be a reference to the most recently modified version
of the context solid, whatever it is.
"""
all = {}
for o in self.objects:
# tricky-- if an object is a compound of solids,
# do not return all of the solids underneath-- typically
# then we'll keep joining to ourself
if (
propName == "Solids"
and isinstance(o, Solid)
and o.ShapeType() == "Compound"
):
for i in getattr(o, "Compounds")():
all[i.hashCode()] = i
else:
if hasattr(o, propName):
for i in getattr(o, propName)():
all[i.hashCode()] = i
return list(all.values())
def split(self, keepTop: bool = False, keepBottom: bool = False) -> "Workplane":
"""
Splits a solid on the stack into two parts, optionally keeping the separate parts.
:param boolean keepTop: True to keep the top, False or None to discard it
:param boolean keepBottom: True to keep the bottom, False or None to discard it
:raises: ValueError if keepTop and keepBottom are both false.
:raises: ValueError if there is not a solid in the current stack or the parent chain
:returns: CQ object with the desired objects on the stack.
The most common operation splits a solid and keeps one half. This sample creates
split bushing::
# drill a hole in the side
c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()
# now cut it in half sideways
c = c.faces(">Y").workplane(-0.5).split(keepTop=True)
"""
solid = self.findSolid()
if (not keepTop) and (not keepBottom):
raise ValueError("You have to keep at least one half")
maxDim = solid.BoundingBox().DiagonalLength * 10.0
topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim)
bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim)
top = solid.cut(bottomCutBox)
bottom = solid.cut(topCutBox)
if keepTop and keepBottom:
# Put both on the stack, leave original unchanged.
return self.newObject([top, bottom])
else:
# Put the one we are keeping on the stack, and also update the
# context solidto the one we kept.
if keepTop:
return self.newObject([top])
else:
return self.newObject([bottom])
@deprecate()
def combineSolids(
self, otherCQToCombine: Optional["Workplane"] = None
) -> "Workplane":
"""
!!!DEPRECATED!!! use union()
Combines all solids on the current stack, and any context object, together
into a single object.
After the operation, the returned solid is also the context solid.
:param otherCQToCombine: another CadQuery to combine.
:return: a cQ object with the resulting combined solid on the stack.
Most of the time, both objects will contain a single solid, which is
combined and returned on the stack of the new object.
"""
# loop through current stack objects, and combine them
toCombine = self.solids().vals()
if otherCQToCombine:
for obj in otherCQToCombine.solids().vals():
toCombine.append(obj)
if len(toCombine) < 1:
raise ValueError("Cannot Combine: at least one solid required!")
# get context solid and we don't want to find our own objects
ctxSolid = self.findSolid(searchStack=False, searchParents=True)
if ctxSolid is None:
ctxSolid = toCombine.pop(0)
# now combine them all. make sure to save a reference to the ctxSolid pointer!
s: Shape = ctxSolid
if toCombine:
s = s.fuse(*_selectShapes(toCombine))
return self.newObject([s])
def all(self) -> List["Workplane"]:
"""
Return a list of all CQ objects on the stack.
useful when you need to operate on the elements
individually.
Contrast with vals, which returns the underlying
objects for all of the items on the stack
"""
return [self.newObject([o]) for o in self.objects]
def size(self) -> int:
"""
Return the number of objects currently on the stack
"""
return len(self.objects)
def vals(self) -> List[CQObject]:
"""
get the values in the current list
:rtype: list of occ_impl objects
:returns: the values of the objects on the stack.
Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack
"""
return self.objects
@overload
def add(self, obj: "Workplane") -> "Workplane":
...
@overload
def add(self, obj: CQObject) -> "Workplane":
...
@overload
def add(self, obj: Iterable[CQObject]) -> "Workplane":
...
def add(self, obj):
"""
Adds an object or a list of objects to the stack
:param obj: an object to add
:type obj: a Workplane, CAD primitive, or list of CAD primitives
:return: a Workplane with the requested operation performed
If an Workplane object, the values of that object's stack are added. If
a list of cad primitives, they are all added. If a single CAD primitive
then it is added.
Used in rare cases when you need to combine the results of several CQ
results into a single Workplane object. Shelling is one common example.
"""
if isinstance(obj, list):
self.objects.extend(obj)
elif isinstance(obj, Workplane):
self.objects.extend(obj.objects)
else:
self.objects.append(obj)
return self
def val(self) -> CQObject:
"""
Return the first value on the stack. If no value is present, current plane origin is returned.
:return: the first value on the stack.
:rtype: A CAD primitive
"""
return self.objects[0] if self.objects else self.plane.origin
def _getTagged(self, name: str) -> "Workplane":
"""
Search the parent chain for a an object with tag == name.
:param name: the tag to search for
:type name: string
:returns: the CQ object with tag == name
:raises: ValueError if no object tagged name
"""
rv = self.ctx.tags.get(name)
if rv is None:
raise ValueError(f"No CQ object named {name} in chain")
return rv
def toOCC(self) -> Any:
"""
Directly returns the wrapped OCCT object.
:return: The wrapped OCCT object
:rtype TopoDS_Shape or a subclass
"""
return self.val().wrapped
def workplane(
self,
offset: float = 0.0,
invert: bool = False,
centerOption: Literal[
"CenterOfMass", "ProjectedOrigin", "CenterOfBoundBox"
] = "ProjectedOrigin",
origin: Optional[VectorLike] = None,
) -> "Workplane":
"""
Creates a new 2-D workplane, located relative to the first face on the stack.
:param offset: offset for the work plane in the Z direction. Default
:param invert: invert the Z direction from that of the face.
:param centerOption: how local origin of workplane is determined.
:param origin: origin for plane center, requires 'ProjectedOrigin' centerOption.
:type offset: float or None=0.0
:type invert: boolean or None=False
:type centerOption: string or None='ProjectedOrigin'
:type origin: Vector or None
:rtype: Workplane object ( which is a subclass of CQ )
The first element on the stack must be a face, a set of
co-planar faces or a vertex. If a vertex, then the parent
item on the chain immediately before the vertex must be a
face.
The result will be a 2-d working plane
with a new coordinate system set up as follows:
* The centerOption paramter sets how the center is defined.
Options are 'CenterOfMass', 'CenterOfBoundBox', or 'ProjectedOrigin'.
'CenterOfMass' and 'CenterOfBoundBox' are in relation to the selected
face(s) or vertex (vertices). 'ProjectedOrigin' uses by default the current origin
or the optional origin parameter (if specified) and projects it onto the plane
defined by the selected face(s).
* The Z direction will be normal to the plane of the face,computed
at the center point.
* The X direction will be parallel to the x-y plane. If the workplane is parallel to
the global x-y plane, the x direction of the workplane will co-incide with the
global x direction.
Most commonly, the selected face will be planar, and the workplane lies in the same plane
of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from
an existing surface, and even more rarely to define a workplane based on a face that is
not planar.
To create a workplane without first having a face, use the Workplane() method.
Future Enhancements:
* Allow creating workplane from planar wires
* Allow creating workplane based on an arbitrary point on a face, not just the center.
For now you can work around by creating a workplane and then offsetting the center
afterwards.
"""
def _isCoPlanar(f0, f1):
"""Test if two faces are on the same plane."""
p0 = f0.Center()
p1 = f1.Center()
n0 = f0.normalAt()
n1 = f1.normalAt()
# test normals (direction of planes)
if not (
(abs(n0.x - n1.x) < self.ctx.tolerance)
or (abs(n0.y - n1.y) < self.ctx.tolerance)
or (abs(n0.z - n1.z) < self.ctx.tolerance)
):
return False
# test if p1 is on the plane of f0 (offset of planes)
return abs(n0.dot(p0.sub(p1)) < self.ctx.tolerance)
def _computeXdir(normal):
"""
Figures out the X direction based on the given normal.
:param :normal The direction that's normal to the plane.
:type :normal A Vector
:return A vector representing the X direction.
"""
xd = Vector(0, 0, 1).cross(normal)
if xd.Length < self.ctx.tolerance:
# this face is parallel with the x-y plane, so choose x to be in global coordinates
xd = Vector(1, 0, 0)
return xd
if centerOption not in {"CenterOfMass", "ProjectedOrigin", "CenterOfBoundBox"}:
raise ValueError("Undefined centerOption value provided.")
if len(self.objects) > 1:
objs: List[Face] = [o for o in self.objects if isinstance(o, Face)]
if not all(o.geomType() in ("PLANE", "CIRCLE") for o in objs) or len(
objs
) < len(self.objects):
raise ValueError(
"If multiple objects selected, they all must be planar faces."
)
# are all faces co-planar with each other?
if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
raise ValueError("Selected faces must be co-planar.")
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = Shape.CombinedCenter(_selectShapes(self.objects))
elif centerOption == "CenterOfBoundBox":
center = Shape.CombinedCenterOfBoundBox(_selectShapes(self.objects))
normal = objs[0].normalAt()
xDir = _computeXdir(normal)
else:
obj = self.val()
if isinstance(obj, Face):
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = obj.Center()
elif centerOption == "CenterOfBoundBox":
center = obj.CenterOfBoundBox()
normal = obj.normalAt(center)
xDir = _computeXdir(normal)
elif isinstance(obj, (Shape, Vector)):
if centerOption in {"CenterOfMass", "ProjectedOrigin"}:
center = obj.Center()
elif centerOption == "CenterOfBoundBox":
center = (
obj.CenterOfBoundBox()
if isinstance(obj, Shape)
else obj.Center()
)
val = self.parent.val() if self.parent else None
if isinstance(val, Face):
normal = val.normalAt(center)
xDir = _computeXdir(normal)
else:
normal = self.plane.zDir
xDir = self.plane.xDir
else:
raise ValueError("Needs a face or a vertex or point on a work plane")
# update center to projected origin if desired
if centerOption == "ProjectedOrigin":
orig: Vector
if origin is None:
orig = self.plane.origin
elif isinstance(origin, tuple):
orig = Vector(origin)
else:
orig = origin
center = orig.projectToPlane(Plane(center, xDir, normal))
# invert if requested
if invert:
normal = normal.multiply(-1.0)
# offset origin if desired
offsetVector = normal.normalized().multiply(offset)
offsetCenter = center.add(offsetVector)
# make the new workplane
plane = Plane(offsetCenter, xDir, normal)
s = Workplane(plane)
s.parent = self
s.ctx = self.ctx
# a new workplane has the center of the workplane on the stack
return s
def copyWorkplane(self, obj: "Workplane") -> "Workplane":
"""
Copies the workplane from obj.
:param obj: an object to copy the workplane from
:type obj: a CQ object
:returns: a CQ object with obj's workplane
"""
out = Workplane(obj.plane)
out.parent = self
out.ctx = self.ctx
return out
def workplaneFromTagged(self, name: str) -> "Workplane":
"""
Copies the workplane from a tagged parent.
:param name: tag to search for
:type name: string
:returns: a CQ object with name's workplane
"""
tagged = self._getTagged(name)
out = self.copyWorkplane(tagged)
return out
def first(self) -> "Workplane":
"""
Return the first item on the stack
:returns: the first item on the stack.
:rtype: a CQ object
"""
return self.newObject(self.objects[0:1])
def item(self, i: int) -> "Workplane":
"""
Return the ith item on the stack.
:rtype: a CQ object
"""
return self.newObject([self.objects[i]])
def last(self) -> "Workplane":
"""
Return the last item on the stack.
:rtype: a CQ object
"""
return self.newObject([self.objects[-1]])
def end(self, n: int = 1) -> "Workplane":
"""
Return the nth parent of this CQ element
:param n: number of ancestor to return (default: 1)
:rtype: a CQ object
:raises: ValueError if there are no more parents in the chain.
For example::
CQ(obj).faces("+Z").vertices().end()
will return the same as::
CQ(obj).faces("+Z")
"""
rv = self
for _ in range(n):
if rv.parent:
rv = rv.parent
else:
raise ValueError("Cannot End the chain-- no parents!")
return rv
def _findType(self, types, searchStack=True, searchParents=True):
if searchStack:
rv = [s for s in self.objects if isinstance(s, types)]
if rv and types == (Solid, Compound):
return Compound.makeCompound(rv)
elif rv:
return rv[0]
if searchParents and self.parent is not None:
return self.parent._findType(types, searchStack=True, searchParents=True)
return None
def findSolid(
self, searchStack: bool = True, searchParents: bool = True
) -> Union[Solid, Compound]:
"""
Finds the first solid object in the chain, searching from the current node
backwards through parents until one is found.
:param searchStack: should objects on the stack be searched first.
:param searchParents: should parents be searched?
:raises: ValueError if no solid is found in the current object or its parents,
and errorOnEmpty is True
This function is very important for chains that are modifying a single parent object,
most often a solid.
Most of the time, a chain defines or selects a solid, and then modifies it using workplanes
or other operations.
Plugin Developers should make use of this method to find the solid that should be modified,
if the plugin implements a unary operation, or if the operation will automatically merge its
results with an object already on the stack.
"""
return self._findType((Solid, Compound), searchStack, searchParents)
def findFace(self, searchStack: bool = True, searchParents: bool = True) -> Face:
"""
Finds the first face object in the chain, searching from the current node
backwards through parents until one is found.
:param searchStack: should objects on the stack be searched first.
:param searchParents: should parents be searched?
:raises: ValueError if no face is found in the current object or its parents,
and errorOnEmpty is True
"""
return self._findType(Face, searchStack, searchParents)
def _selectObjects(
self,
objType: Any,
selector: Optional[Union[Selector, str]] = None,
tag: Optional[str] = None,
) -> "Workplane":
"""
Filters objects of the selected type with the specified selector,and returns results
:param objType: the type of object we are searching for
:type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object with the selected objects on the stack.
**Implementation Note**: This is the base implementation of the vertices,edges,faces,
solids,shells, and other similar selector methods. It is a useful extension point for
plugin developers to make other selector methods.
"""
cq_obj = self._getTagged(tag) if tag else self
# A single list of all faces from all objects on the stack
toReturn = cq_obj._collectProperty(objType)
selectorObj: Selector
if selector:
if isinstance(selector, str):
selectorObj = StringSyntaxSelector(selector)
else:
selectorObj = selector
toReturn = selectorObj.filter(toReturn)
return self.newObject(toReturn)
def vertices(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the vertices of objects on the stack, optionally filtering the selection. If there
are multiple objects on the stack, the vertices of all objects are collected and a list of
all the distinct vertices is returned.
:param selector:
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the
current stack, after being filtered by the selector, if provided
If there are no vertices for any objects on the current stack, an empty CQ object
is returned
The typical use is to select the vertices of a single object on the stack. For example::
Workplane().box(1,1,1).faces("+Z").vertices().size()
returns 4, because the topmost face of cube will contain four vertices. While this::
Workplane().box(1,1,1).faces().vertices().size()
returns 8, because a cube has a total of 8 vertices
**Note** Circles are peculiar, they have a single vertex at the center!
:py:class:`StringSyntaxSelector`
"""
return self._selectObjects("Vertices", selector, tag)
def faces(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the faces of objects on the stack, optionally filtering the selection. If there are
multiple objects on the stack, the faces of all objects are collected and a list of all the
distinct faces is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on
the current stack, filtered by the provided selector.
If there are no vertices for any objects on the current stack, an empty CQ object
is returned.
The typical use is to select the faces of a single object on the stack. For example::
CQ(aCube).faces("+Z").size()
returns 1, because a cube has one face with a normal in the +Z direction. Similarly::
CQ(aCube).faces().size()
returns 6, because a cube has a total of 6 faces, And::
CQ(aCube).faces("|Z").size()
returns 2, because a cube has 2 faces having normals parallel to the z direction
"""
return self._selectObjects("Faces", selector, tag)
def edges(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the edges of objects on the stack, optionally filtering the selection. If there are
multiple objects on the stack, the edges of all objects are collected and a list of all the
distinct edges is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on
the current stack, filtered by the provided selector.
If there are no edges for any objects on the current stack, an empty CQ object is returned
The typical use is to select the edges of a single object on the stack. For example::
CQ(aCube).faces("+Z").edges().size()
returns 4, because a cube has one face with a normal in the +Z direction. Similarly::
CQ(aCube).edges().size()
returns 12, because a cube has a total of 12 edges, And::
CQ(aCube).edges("|Z").size()
returns 4, because a cube has 4 edges parallel to the z direction
"""
return self._selectObjects("Edges", selector, tag)
def wires(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the wires of objects on the stack, optionally filtering the selection. If there are
multiple objects on the stack, the wires of all objects are collected and a list of all the
distinct wires is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on
the current stack, filtered by the provided selector.
If there are no wires for any objects on the current stack, an empty CQ object is returned
The typical use is to select the wires of a single object on the stack. For example::
CQ(aCube).faces("+Z").wires().size()
returns 1, because a face typically only has one outer wire
"""
return self._selectObjects("Wires", selector, tag)
def solids(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the solids of objects on the stack, optionally filtering the selection. If there are
multiple objects on the stack, the solids of all objects are collected and a list of all the
distinct solids is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
the current stack, filtered by the provided selector.
If there are no solids for any objects on the current stack, an empty CQ object is returned
The typical use is to select the a single object on the stack. For example::
CQ(aCube).solids().size()
returns 1, because a cube consists of one solid.
It is possible for single CQ object ( or even a single CAD primitive ) to contain
multiple solids.
"""
return self._selectObjects("Solids", selector, tag)
def shells(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select the shells of objects on the stack, optionally filtering the selection. If there are
multiple objects on the stack, the shells of all objects are collected and a list of all the
distinct shells is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
the current stack, filtered by the provided selector.
If there are no shells for any objects on the current stack, an empty CQ object is returned
Most solids will have a single shell, which represents the outer surface. A shell will
typically be composed of multiple faces.
"""
return self._selectObjects("Shells", selector, tag)
def compounds(
self, selector: Optional[Union[Selector, str]] = None, tag: Optional[str] = None
) -> "Workplane":
"""
Select compounds on the stack, optionally filtering the selection. If there are multiple
objects on the stack, they are collected and a list of all the distinct compounds
is returned.
:param selector: A selector
:type selector: None, a Selector object, or a string selector expression.
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on
the current stack, filtered by the provided selector.
A compound contains multiple CAD primitives that resulted from a single operation, such as
a union, cut, split, or fillet. Compounds can contain multiple edges, wires, or solids.
"""
return self._selectObjects("Compounds", selector, tag)
def toSvg(self, opts: Any = None) -> str:
"""
Returns svg text that represents the first item on the stack.
for testing purposes.
:param opts: svg formatting options
:type opts: dictionary, width and height
:return: a string that contains SVG that represents this item.
"""
return getSVG(self.val(), opts)
def exportSvg(self, fileName: str) -> None:
"""
Exports the first item on the stack as an SVG file
For testing purposes mainly.
:param fileName: the filename to export
:type fileName: String, absolute path to the file
"""
exportSVG(self, fileName)
def rotateAboutCenter(
self, axisEndPoint: VectorLike, angleDegrees: float
) -> "Workplane":
"""
Rotates all items on the stack by the specified angle, about the specified axis
The center of rotation is a vector starting at the center of the object on the stack,
and ended at the specified point.
:param axisEndPoint: the second point of axis of rotation
:type axisEndPoint: a three-tuple in global coordinates
:param angleDegrees: the rotation angle, in degrees
:type angleDegrees: float
:returns: a CQ object, with all items rotated.
WARNING: This version returns the same cq object instead of a new one-- the
old object is not accessible.
Future Enhancements:
* A version of this method that returns a transformed copy, rather than modifying
the originals
* This method doesnt expose a very good interface, because the axis of rotation
could be inconsistent between multiple objects. This is because the beginning
of the axis is variable, while the end is fixed. This is fine when operating on
one object, but is not cool for multiple.
"""
# center point is the first point in the vector
endVec = Vector(axisEndPoint)
def _rot(obj):
startPt = obj.Center()
endPt = startPt + endVec
return obj.rotate(startPt, endPt, angleDegrees)
return self.each(_rot, False)
def rotate(