planex2d 0.2.2__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. planex/__init__.py +125 -0
  2. planex/_auto.py +17 -0
  3. planex/_caller.py +21 -0
  4. planex/_closing.py +45 -0
  5. planex/_compound.py +441 -0
  6. planex/_deformable.py +143 -0
  7. planex/_dimensions.py +818 -0
  8. planex/_drawing.py +1162 -0
  9. planex/_dxf.py +341 -0
  10. planex/_expressions.py +358 -0
  11. planex/_fileunits.py +40 -0
  12. planex/_geom.py +177 -0
  13. planex/_gmsh.py +210 -0
  14. planex/_ids.py +135 -0
  15. planex/_io.py +205 -0
  16. planex/_kicad.py +1139 -0
  17. planex/_kicadwrite.py +773 -0
  18. planex/_levelset.py +429 -0
  19. planex/_measurements.py +477 -0
  20. planex/_meshsection.py +219 -0
  21. planex/_naca.py +117 -0
  22. planex/_occ.py +56 -0
  23. planex/_parameters.py +143 -0
  24. planex/_planex.cp313-win_amd64.pyd +0 -0
  25. planex/_planex.pyi +2088 -0
  26. planex/_plot.py +649 -0
  27. planex/_plot3d.py +106 -0
  28. planex/_plot_fields.py +318 -0
  29. planex/_plot_mesh.py +260 -0
  30. planex/_plot_sketch.py +668 -0
  31. planex/_plot_style.py +222 -0
  32. planex/_select.py +1209 -0
  33. planex/_shapelyio.py +153 -0
  34. planex/_sketches.py +61 -0
  35. planex/_svg.py +785 -0
  36. planex/_svgfile.py +348 -0
  37. planex/_units.py +14 -0
  38. planex/_views.py +83 -0
  39. planex/_workplane.py +786 -0
  40. planex/debug.py +8 -0
  41. planex/fonts/LICENSE-GPL-3.0.txt +674 -0
  42. planex/fonts/LICENSE-LGPL-3.0.txt +165 -0
  43. planex/fonts/NOTICE.txt +19 -0
  44. planex/fonts/osifont-lgpl3fe.ttf +0 -0
  45. planex/measurements.py +12 -0
  46. planex/py.typed +0 -0
  47. planex/stackups/__init__.py +29 -0
  48. planex/stackups/_jlcpcb.py +108 -0
  49. planex/templates/title_block.dxf +5180 -0
  50. planex/types.py +70 -0
  51. planex2d-0.2.2.dist-info/METADATA +133 -0
  52. planex2d-0.2.2.dist-info/RECORD +62 -0
  53. planex2d-0.2.2.dist-info/WHEEL +5 -0
  54. planex2d-0.2.2.dist-info/licenses/LICENSE +504 -0
  55. planex2d-0.2.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +435 -0
  56. planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.APACHE +203 -0
  57. planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.BSD +26 -0
  58. planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.MPL2 +373 -0
  59. planex2d-0.2.2.dist-info/licenses/external_dependencies/taskflow/LICENSE +25 -0
  60. planex2d-0.2.2.dist-info/licenses/src/python/planex/fonts/LICENSE-GPL-3.0.txt +674 -0
  61. planex2d-0.2.2.dist-info/licenses/src/python/planex/fonts/LICENSE-LGPL-3.0.txt +165 -0
  62. planex2d-0.2.2.dist-info/licenses/src/python/planex/fonts/NOTICE.txt +19 -0
planex/__init__.py ADDED
@@ -0,0 +1,125 @@
1
+ # SPDX-FileCopyrightText: 2026 Sebastian Hirnschall
2
+ # SPDX-License-Identifier: LGPL-2.1-or-later
3
+
4
+ """planex: exact 2d geometry of lines, arcs and splines (sketches, constraint sketches, booleans, offsets) for netgen,
5
+ NGSolve and OCC.
6
+
7
+ Coordinates and sizes must lie below about 1e70: beyond that the tolerance tests, which square their values,
8
+ overflow and booleans become silently wrong. Lengths below about 1e-14 count as rounding: the tolerance is
9
+ relative to the coordinates above 1 and absolute (1e-14) below; OCC and netgen resolve only 1e-7."""
10
+ from importlib.metadata import PackageNotFoundError as _PackageNotFoundError, version as _version
11
+
12
+ try:
13
+ __version__ = _version("planex2d")
14
+ except _PackageNotFoundError:
15
+ __version__ = "unknown"
16
+ # on Windows the core's C++ runtime is not in the wheel: it comes with Microsoft's Visual C++ Redistributable
17
+ try:
18
+ from . import _planex
19
+ except ImportError as _e:
20
+ if "DLL load failed" not in str(_e):
21
+ raise
22
+ raise ImportError(f"{_e} planex needs the latest Microsoft Visual C++ Redistributable (x64): "
23
+ "https://aka.ms/vc14/vc_redist.x64.exe") from None
24
+ from . import _occ
25
+ from ._planex import (Arc, ArcEdge, Circle, CircleEdge, Constraint, Curve, EdgeView, EdgesView, Ellipse, Face, FaceView,
26
+ FacesView, FromNumpy, FromPolyline, GetTransformation, Glue, H, Intersect, Line, LineEdge, Move,
27
+ OCCGeometry, Pad, Plane, PlanexWarning, Pocket, Project, QuickSketch, ResetIdCounters, Rotation,
28
+ Shape, Spline, SplineEdge, Subtract, SymmetricDifference, Translation, Unify, V, Vertex, VertexView,
29
+ VerticesView, Wire, Wires, XY, XZ, YZ, ZX, origin)
30
+ # outside __all__, re-exported for type checkers by the alias
31
+ from ._planex import BSpline as BSpline, config as config, first as first, last as last
32
+ from ._planex import Auto as Auto
33
+ from ._planex import (Angle, Coincident, Concentric, Curvature, Diameter, Distance, Equal, Fix, Horizontal, Parallel,
34
+ Perpendicular, Radius, RigidGroup, Symmetric, SymmetricCarriers, Tangent, Vertical)
35
+ from ._planex import Chamfer, Flat, OffsetForm, Round, Sharp, Square
36
+ from ._closing import ClosedOpenWires
37
+ from ._compound import Compound
38
+ from ._deformable import DeformableShape
39
+ from ._dxf import FromDXF, WriteDXF
40
+ from ._svgfile import FromSVG, WriteSVG
41
+ from ._drawing import Drawing, Sheet
42
+ from ._gmsh import WriteGeo
43
+ from ._io import FromBrep, FromOCC, FromStep, WriteBrep, WriteStep
44
+ from ._meshsection import Intersect3d
45
+ from ._kicad import FromKiCad
46
+ from ._kicadwrite import WriteKiCad
47
+ from . import _geom, debug, stackups, types
48
+ from ._levelset import FromLevelSet
49
+ from ._naca import FromNACA
50
+ from . import _plot
51
+ from ._plot import Plot, PlotField, PlotMesh
52
+ from ._plot3d import Plot3d
53
+ from ._workplane import WorkPlane
54
+ from . import _plot_sketch
55
+ from ._shapelyio import FromShapely, ToShapely
56
+ from . import _svg
57
+ from ._svg import ToSVG
58
+ from ._units import cm, inch, m, mil, mm, um
59
+ from . import _views
60
+ from ._views import Edge
61
+
62
+ for _cls in (Face, Shape, Wire, Compound):
63
+ _cls.Plot = Plot
64
+ _cls.Plot3d = Plot3d
65
+ _cls.WriteStep = WriteStep
66
+ _cls.WriteBrep = WriteBrep
67
+ _cls.WriteDXF = WriteDXF
68
+ _cls.WriteSVG = WriteSVG
69
+ _cls.WriteGeo = WriteGeo
70
+ _cls.ToShapely = ToShapely
71
+ _cls.ToSVG = ToSVG
72
+ _cls._repr_svg_ = _svg._repr_svg
73
+ del _cls
74
+
75
+ # the offset's flags print as their names, so that help shows a default as px.Sharp (as px.Auto prints px.Auto)
76
+ OffsetForm.__repr__ = lambda self: "px." + self.name
77
+
78
+ # the two kinds of sketch: with constraints (Sketch, a stand-in that raises when the build has none) and without
79
+ from ._sketches import Sketch, sketch_types as _sketch_types # noqa: E402
80
+ for _cls in _sketch_types:
81
+ _cls.ToSVG = ToSVG
82
+ _cls.Plot3d = Plot3d
83
+ _cls.WriteStep = WriteStep
84
+ _cls.WriteBrep = WriteBrep
85
+ _cls.WriteDXF = WriteDXF
86
+ _cls.WriteSVG = WriteSVG
87
+ _cls.WriteGeo = WriteGeo
88
+ _cls.ToShapely = ToShapely
89
+ _cls.Plot = _plot_sketch.Plot
90
+ _cls._repr_svg_ = _svg._repr_svg
91
+ _cls.Polylines = _geom.shape_polylines
92
+ del _cls
93
+ from ._parameters import Parameters, use as _use # noqa: E402
94
+ Sketch.Use = _use
95
+ DeformableShape.Plot = Plot
96
+ DeformableShape.ToSVG = ToSVG
97
+ DeformableShape._repr_svg_ = _svg._repr_svg
98
+ _views.complete_face_view()
99
+ _views.add_wire_operators(Sketch, EdgeView)
100
+ _views.add_wire_operators(QuickSketch, EdgeView)
101
+ from . import _ids, _select # noqa: E402
102
+ from ._select import Find, Selection # noqa: E402
103
+
104
+ from . import _measurements, measurements # noqa: E402
105
+
106
+ _ids.add_id_arguments(globals())
107
+ _select.add_selectors()
108
+ _measurements.add_measures()
109
+ # coordinates of an element's centre (3d, and h, v in its plane) for conditions; the axes for Min, Max and Sorted
110
+ x, y, z, h, v = (_select.Coordinate(c) for c in "xyzhv")
111
+ X, Y, Z = (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)
112
+
113
+ # not x, y, z, h, v, X, Y, Z, BSpline and config: a star import after NGSolve's or netgen.occ's keeps theirs
114
+ __all__ = ["Angle", "Arc", "ArcEdge", "Auto", "Chamfer", "Circle", "CircleEdge", "ClosedOpenWires", "Coincident", "Compound", "Concentric", "Constraint", "Curvature", "Curve",
115
+ "DeformableShape", "Diameter", "Distance", "Drawing", "Edge", "EdgeView",
116
+ "EdgesView", "Ellipse", "Equal", "Face", "FaceView", "FacesView", "Find", "Fix", "Flat", "FromBrep", "FromDXF", "FromKiCad", "FromLevelSet",
117
+ "FromNACA", "FromNumpy", "FromOCC", "FromPolyline", "FromSVG", "FromShapely", "FromStep", "GetTransformation", "Glue", "H", "Horizontal", "Intersect",
118
+ "Intersect3d", "Line", "LineEdge", "Move", "OCCGeometry", "OffsetForm", "Pad", "Parallel", "Parameters", "Perpendicular", "Plane",
119
+ "PlanexWarning", "Plot", "Plot3d", "PlotField", "PlotMesh", "Pocket", "Project", "QuickSketch", "Radius", "ResetIdCounters",
120
+ "RigidGroup", "Rotation", "Round", "Selection", "Shape", "Sharp", "Sheet", "Sketch", "Spline", "SplineEdge", "Square", "Subtract", "Symmetric", "SymmetricDifference",
121
+ "SymmetricCarriers", "Tangent", "ToSVG", "ToShapely", "Translation", "Unify", "V", "Vertex", "VertexView",
122
+ "Vertical", "VerticesView", "Wire", "Wires", "WorkPlane", "WriteBrep", "WriteDXF", "WriteGeo", "WriteKiCad",
123
+ "WriteSVG", "WriteStep", "XY", "XZ", "YZ", "ZX", "cm", "inch", "m", "mil", "mm", "origin", "um"]
124
+
125
+ types._unquote_annotations([globals()[name] for name in __all__] + [getattr(measurements, name) for name in measurements.__all__])
planex/_auto.py ADDED
@@ -0,0 +1,17 @@
1
+ # SPDX-FileCopyrightText: 2026 Sebastian Hirnschall
2
+ # SPDX-License-Identifier: LGPL-2.1-or-later
3
+
4
+ from ._planex import Auto, AutoType
5
+
6
+ __all__ = ["Auto", "AutoType", "is_auto"]
7
+
8
+
9
+ def is_auto(value, call, name):
10
+ """True for px.Auto, the default worked out from the situation; None and px.Auto({...}) raise"""
11
+ if value is None:
12
+ raise TypeError(f"{call}({name}=None): None is no value here; the default px.Auto works it out")
13
+ if isinstance(value, AutoType):
14
+ if value.relations:
15
+ raise TypeError(f"{call}({name}=px.Auto({{...}})): there is nothing to switch here; px.Auto alone works it out")
16
+ return True
17
+ return False
planex/_caller.py ADDED
@@ -0,0 +1,21 @@
1
+ # SPDX-FileCopyrightText: 2026 Sebastian Hirnschall
2
+ # SPDX-License-Identifier: LGPL-2.1-or-later
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import sys
8
+ import warnings
9
+
10
+ PACKAGE = os.path.dirname(os.path.abspath(__file__)) + os.sep
11
+
12
+
13
+ def warn(message, category=UserWarning):
14
+ """warnings.warn at the first frame outside the planex package, as the C++ warnings take it: the warning names the
15
+ caller's line, whatever wrappers lie between"""
16
+ # stacklevel 2 is the frame that called warn
17
+ level, frame = 2, sys._getframe(1)
18
+ while frame is not None and os.path.abspath(frame.f_code.co_filename).startswith(PACKAGE):
19
+ level += 1
20
+ frame = frame.f_back
21
+ warnings.warn(message, category, stacklevel=level)
planex/_closing.py ADDED
@@ -0,0 +1,45 @@
1
+ # SPDX-FileCopyrightText: 2026 Sebastian Hirnschall
2
+ # SPDX-License-Identifier: LGPL-2.1-or-later
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ import planex as px
9
+
10
+ if TYPE_CHECKING:
11
+ from ._planex import Wire
12
+
13
+
14
+ def ClosedOpenWires(wires: Wire | list[Wire], /) -> list[Wire]:
15
+ """Copies of the wires, each open one closed by a straight line from its end back to its start.
16
+
17
+ Closed wires are copied as they are.
18
+
19
+ Args:
20
+ wires: a wire or a list of wires.
21
+
22
+ Returns:
23
+ a list of wires.
24
+
25
+ Raises:
26
+ ValueError: the closing line would cross or overlap its wire (a single straight edge cannot be closed).
27
+
28
+ Examples:
29
+ px.ClosedOpenWires(u)
30
+ px.Face(px.ClosedOpenWires([outline, u]))
31
+ """
32
+ out = []
33
+ for w in [wires] if isinstance(wires, px.Wire) else wires:
34
+ closed = w.Copy()
35
+ edges = w.edges
36
+ ends = [edges[k].end if at_end else edges[k].start for k, at_end in w.OpenEnds()]
37
+ if len(ends) == 2:
38
+ a, b = ends
39
+ try:
40
+ closed.Add(px.Line(a, b, plane=w))
41
+ except ValueError as e:
42
+ raise ValueError(f"ClosedOpenWires: the straight line from ({a.x:g}, {a.y:g}) to ({b.x:g}, {b.y:g}) "
43
+ f"cannot close the wire: {e}") from None
44
+ out.append(closed)
45
+ return out
planex/_compound.py ADDED
@@ -0,0 +1,441 @@
1
+ # SPDX-FileCopyrightText: 2026 Sebastian Hirnschall
2
+ # SPDX-License-Identifier: LGPL-2.1-or-later
3
+
4
+ from __future__ import annotations
5
+
6
+ import types
7
+ import weakref
8
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, overload
9
+
10
+ import numpy as np
11
+
12
+ from . import _planex
13
+
14
+ if TYPE_CHECKING:
15
+ from ._planex import EdgeView, Face, FaceView, Shape, VertexView, Wire
16
+ from .types import Point, Point3
17
+
18
+ # every compound alive, for px.Find
19
+ _ALL = weakref.WeakSet()
20
+
21
+
22
+ def _same(part):
23
+ if isinstance(part, Compound):
24
+ same = Compound(part.parts)
25
+ same._id = part._id
26
+ return same
27
+ return part._Same() if hasattr(part, "_Same") else part
28
+
29
+
30
+ def _points(x):
31
+ """many points as one float64 array, converted once for all parts, anything else as it is; and the number of
32
+ coordinates, None for what is no point"""
33
+ if isinstance(x, (list, tuple)) and x and isinstance(x[0], (list, tuple, np.ndarray)):
34
+ try:
35
+ x = np.asarray(x, dtype=np.float64)
36
+ except (TypeError, ValueError):
37
+ return x, None
38
+ if isinstance(x, np.ndarray):
39
+ return x, x.shape[-1] if x.ndim in (1, 2) else None
40
+ if isinstance(x, (list, tuple)):
41
+ return x, len(x)
42
+ return x, None
43
+
44
+
45
+ def _many(x):
46
+ return isinstance(x, np.ndarray) and x.ndim == 2
47
+
48
+
49
+ def _nearest(values):
50
+ """per row the index of the smallest of the parts' values, the first on ties"""
51
+ return np.argmin(np.stack(values), axis=0)
52
+
53
+
54
+ def _on_one_plane(parts, message):
55
+
56
+ if len(parts) > 1:
57
+ try:
58
+ _planex.AxesOf(types.SimpleNamespace(parts=parts))
59
+ except ValueError:
60
+ raise ValueError(message) from None
61
+
62
+
63
+ class Compound:
64
+ """A plain list of faces, shapes, wires and compounds, also on different planes.
65
+
66
+ Nothing is cut or merged and there are no booleans (+, -, * raise TypeError); px.Unify(compound) merges its parts.
67
+ It is exported as it is. The parts are copies that keep their ids, so moving the compound moves each part once and
68
+ leaves the objects it was made of where they are. len(), iteration and indexing go over its parts.
69
+
70
+ Args:
71
+ parts: the faces, shapes, wires and compounds.
72
+ id: the compound's id; None gives Compound.N when first asked for.
73
+
74
+ Raises:
75
+ ValueError: id does not follow the rule of a user-given id.
76
+
77
+ Examples:
78
+ px.Compound([plate, rib.Rotated((0, 0, 0), (1, 0, 0), 90)])
79
+ px.Compound([outline, holes], id="board")
80
+ """
81
+
82
+ # attached by the planex module, as to Face, Shape and Wire; declared so that type checkers see them
83
+ Plot: Callable[..., Any]
84
+ Plot3d: Callable[..., Any]
85
+ WriteStep: Callable[..., None]
86
+ WriteBrep: Callable[..., None]
87
+ WriteDXF: Callable[..., None]
88
+ WriteSVG: Callable[..., None]
89
+ WriteGeo: Callable[..., None]
90
+ ToShapely: Callable[..., Any]
91
+ ToSVG: Callable[..., str]
92
+
93
+ def __init__(self, parts: Iterable[Face | Shape | Wire | Compound] = (), /, id: str | None = None) -> None:
94
+ self.parts = [_same(p) for p in parts]
95
+ if id is not None:
96
+ _planex._CheckUserId(id)
97
+ self._id = id
98
+ _ALL.add(self)
99
+
100
+ @property
101
+ def id(self) -> str:
102
+ """The compound's id: Compound.N, given when first asked for, or the one set."""
103
+ if self._id is None:
104
+ self._id = _planex._NextId("Compound")
105
+ return self._id
106
+
107
+ @id.setter
108
+ def id(self, value):
109
+ _planex._CheckUserId(value)
110
+ self._id = value
111
+
112
+ def __len__(self):
113
+ return len(self.parts)
114
+
115
+ def __iter__(self):
116
+ return iter(self.parts)
117
+
118
+ def __getitem__(self, i):
119
+ return self.parts[i]
120
+
121
+ def _no_booleans(self, other):
122
+ raise TypeError("a Compound has no booleans, merge it first with px.Unify(comp)")
123
+
124
+ __add__ = __radd__ = __sub__ = __rsub__ = __mul__ = __rmul__ = __xor__ = __rxor__ = _no_booleans
125
+
126
+ def __repr__(self):
127
+ return "Compound(%r)" % self.parts
128
+
129
+ def Copy(self) -> Compound:
130
+ """An independent copy of the compound and of its parts.
131
+
132
+ Examples:
133
+ compound.Copy()
134
+ """
135
+ c = Compound()
136
+ c.parts = [p.Copy() for p in self.parts]
137
+ return c
138
+
139
+ def __copy__(self):
140
+ return self.Copy()
141
+
142
+ def __deepcopy__(self, memo):
143
+ return self.Copy()
144
+
145
+ @property
146
+ def bounding_box(self) -> tuple[tuple[float, float], tuple[float, float]] | None:
147
+ """The box ((hmin, vmin), (hmax, vmax)) over the parts, each in its own plane coordinates; None if it is empty."""
148
+ boxes = [b for b in (p.bounding_box for p in self.parts) if b is not None]
149
+ if not boxes:
150
+ return None
151
+ return ((min(b[0][0] for b in boxes), min(b[0][1] for b in boxes)),
152
+ (max(b[1][0] for b in boxes), max(b[1][1] for b in boxes)))
153
+
154
+ def ToOCCShape(self, **options: Any) -> Any:
155
+ """The OCC compound of every part's ToOCCShape(**options).
156
+
157
+ Args:
158
+ options: the options of the parts' ToOCCShape (fit_splines, remove_edges_smaller, ...).
159
+
160
+ Returns:
161
+ a netgen.occ Compound.
162
+
163
+ Examples:
164
+ compound.ToOCCShape()
165
+ compound.ToOCCShape(fit_arcs=True)
166
+ """
167
+ from netgen.occ import Compound as OCCCompound
168
+
169
+ return OCCCompound([p.ToOCCShape(**options) for p in self.parts])
170
+
171
+ def _each(self, name, *args, **kwargs):
172
+ for p in self.parts:
173
+ getattr(p, name)(*args, **kwargs)
174
+ return self
175
+
176
+ def _leaves(self):
177
+ """the parts of this compound and of the compounds in it"""
178
+ return [q for p in self.parts for q in (p._leaves() if isinstance(p, Compound) else [p])]
179
+
180
+ def _filled(self):
181
+ """the parts of this compound and of the compounds in it that are not empty"""
182
+ return [p for p in self._leaves() if p.bounding_box is not None]
183
+
184
+ def _measured(self, call, coordinates):
185
+ """the parts that are not empty; (h, v) points only if they all lie on one plane"""
186
+ leaves = self._filled()
187
+ if coordinates == 2:
188
+ _on_one_plane(leaves, call + ": the parts of the compound lie on different planes, give the points as (x, y, z)")
189
+ return leaves
190
+
191
+ def _union(self, call):
192
+ """the union of the faces of the parts"""
193
+
194
+ leaves = self._filled()
195
+ if not leaves:
196
+ raise ValueError("Distance: nothing to measure from")
197
+ faces = [p for p in leaves if p.faces]
198
+ if not faces:
199
+ raise ValueError(call + ": the compound has no faces, so no inside")
200
+ if len(faces) == 1:
201
+ return faces[0]
202
+ _on_one_plane(faces, call + ": the faces of the compound lie on different planes")
203
+ return _planex.Unify(faces)
204
+
205
+ def Polylines(self, tol: float) -> list[np.ndarray]:
206
+ """Every part's Polylines(tol) in turn, (n, 2) arrays of plane coordinates.
207
+
208
+ Args:
209
+ tol: the largest distance of a chord from its edge, a length > 0.
210
+
211
+ Raises:
212
+ ValueError: the parts lie on different planes, or tol is not positive.
213
+
214
+ Examples:
215
+ compound.Polylines(0.01)
216
+ """
217
+ leaves = self._filled()
218
+ _on_one_plane(leaves, "Polylines: the parts of the compound lie on different planes")
219
+ return [a for p in leaves for a in p.Polylines(tol)]
220
+
221
+ # the forms for type checkers: many points give an array
222
+ @overload
223
+ def Contains(self, points: np.ndarray | list[tuple[float, float]] | list[tuple[float, float, float]], /, boundary: bool = True) -> np.ndarray: ...
224
+ @overload
225
+ def Contains(self, points: Point | Point3, /, boundary: bool = True) -> bool: ...
226
+ def Contains(self, points: Point | Point3 | np.ndarray, /, boundary: bool = True) -> bool | np.ndarray:
227
+ """True if a part contains the point, as Shape.Contains.
228
+
229
+ Args:
230
+ points: (h, v) if all parts lie on one plane, else (x, y, z); many points as an (n, 2) or (n, 3) array give
231
+ a bool array (n,).
232
+ boundary: the answer for a point on a boundary.
233
+
234
+ Raises:
235
+ ValueError: points are (h, v) and the parts lie on different planes.
236
+
237
+ Examples:
238
+ compound.Contains((1, 2))
239
+ compound.Contains([(0, 0, 0), (1, 2, 0)], boundary=False)
240
+ """
241
+
242
+ points, coordinates = _points(points)
243
+ found = [p.Contains(points, boundary=boundary) for p in self._measured("Contains", coordinates)
244
+ if hasattr(p, "Contains") and not isinstance(p, _planex.Wire)]
245
+ if not found:
246
+ return _planex.Shape().Contains(points, boundary=boundary)
247
+ if _many(points):
248
+ return np.logical_or.reduce(found)
249
+ return any(found)
250
+
251
+ @overload
252
+ def Distance(self, target: np.ndarray | list[tuple[float, float]] | list[tuple[float, float, float]], /, project: bool = False, components: str | None = None,
253
+ signed: bool = False) -> np.ndarray: ...
254
+ @overload
255
+ def Distance(self, target: Point | Point3 | Face | Shape | Wire | EdgeView | VertexView | FaceView, /, project: bool = False,
256
+ components: None = None, signed: bool = False) -> float: ...
257
+ @overload
258
+ def Distance(self, target: Point | Point3 | Face | Shape | Wire | EdgeView | VertexView | FaceView, /, project: bool = False, *,
259
+ components: str) -> float | tuple[float, ...]: ...
260
+ def Distance(self, target: Point | Point3 | Face | Shape | Wire | EdgeView | VertexView | FaceView | np.ndarray, /,
261
+ project: bool = False, components: str | None = None,
262
+ signed: bool = False) -> float | tuple[float, ...] | np.ndarray:
263
+ """The smallest distance of the parts to the target, as Shape.Distance.
264
+
265
+ Args:
266
+ target: what Shape.Distance takes; a point is (h, v) if all parts lie on one plane, else (x, y, z); many
267
+ points as an (n, 2) or (n, 3) array.
268
+ project: True projects a target in another plane into the parts' planes.
269
+ components: letters of 'hvxyz': the components of the shortest connection to the target from the nearest
270
+ part (the first of equally near ones) instead of its length.
271
+ signed: True measures as Shape.Distance(signed=True) of the union of the faces of all parts.
272
+
273
+ Returns:
274
+ a float; with components a tuple of them, one letter a float; many points an array (n,), with k
275
+ components (n, k), one letter (n,).
276
+
277
+ Raises:
278
+ ValueError: the compound is empty; points are (h, v) and the parts lie on different planes; signed and the
279
+ compound has no faces or its faces lie on different planes.
280
+
281
+ Examples:
282
+ compound.Distance((5, 0))
283
+ compound.Distance((5, 0, 1), components="xyz")
284
+ """
285
+ if signed:
286
+ return self._union("Distance(signed=True)").Distance(target, project=project, components=components, signed=True)
287
+ target, coordinates = _points(target)
288
+ leaves = self._measured("Distance", coordinates)
289
+ if not leaves:
290
+ raise ValueError("Distance: nothing to measure from")
291
+ d = [p.Distance(target, project=project) for p in leaves]
292
+ if components is None:
293
+ return np.min(np.stack(d), axis=0) if _many(target) else min(d)
294
+ c = [p.Distance(target, project=project, components=components) for p in leaves]
295
+ if _many(target):
296
+ return np.stack(c)[_nearest(d), np.arange(len(target))]
297
+ return c[d.index(min(d))]
298
+
299
+ @overload
300
+ def ClosestPoint(self, point: np.ndarray | list[tuple[float, float]] | list[tuple[float, float, float]], /) -> np.ndarray: ...
301
+ @overload
302
+ def ClosestPoint(self, point: Point | Point3, /) -> tuple[float, ...]: ...
303
+ def ClosestPoint(self, point: Point | Point3 | np.ndarray, /) -> tuple[float, ...] | np.ndarray:
304
+ """The closest point of the nearest part, as Shape.ClosestPoint.
305
+
306
+ Args:
307
+ point: (h, v) if all parts lie on one plane, else (x, y, z); many points as an (n, 2) or (n, 3) array give
308
+ an array of that shape.
309
+
310
+ Raises:
311
+ ValueError: the compound is empty, or point is (h, v) and the parts lie on different planes.
312
+
313
+ Examples:
314
+ compound.ClosestPoint((5, 0))
315
+ compound.ClosestPoint([(0, 0, 0), (5, 0, 0)])
316
+ """
317
+ point, coordinates = _points(point)
318
+ leaves = self._measured("ClosestPoint", coordinates)
319
+ if not leaves:
320
+ raise ValueError("ClosestPoint: nothing to measure from")
321
+ c = [p.ClosestPoint(point) for p in leaves]
322
+ d = [p.Distance(point) for p in leaves]
323
+ if _many(point):
324
+ return np.stack(c)[_nearest(d), np.arange(len(point))]
325
+ return c[d.index(min(d))]
326
+
327
+ def Rotate(self, *args: Any, **kwargs: Any) -> Compound:
328
+ """Rotates every part in place, as Shape.Rotate.
329
+
330
+ Args:
331
+ args: the arguments of Shape.Rotate: center, angle in the plane, or point, dir, angle in 3d.
332
+ kwargs: its keyword arguments.
333
+
334
+ Returns:
335
+ this compound.
336
+
337
+ Examples:
338
+ compound.Rotate((0, 0), 90)
339
+ compound.Rotate((0, 0, 0), (0, 0, 1), 45)
340
+ """
341
+ return self._each("Rotate", *args, **kwargs)
342
+
343
+ def Translate(self, *args: Any, **kwargs: Any) -> Compound:
344
+ """Moves every part in place, as Shape.Translate.
345
+
346
+ Args:
347
+ args: the arguments of Shape.Translate: d or dh, dv in the plane, or d or dx, dy, dz in 3d.
348
+ kwargs: its keyword arguments.
349
+
350
+ Returns:
351
+ this compound.
352
+
353
+ Examples:
354
+ compound.Translate((10, 0))
355
+ compound.Translate(0, 0, 5)
356
+ """
357
+ return self._each("Translate", *args, **kwargs)
358
+
359
+ def Mirror(self, *args: Any, **kwargs: Any) -> Compound:
360
+ """Mirrors every part in place, as Shape.Mirror.
361
+
362
+ Args:
363
+ args: the arguments of Shape.Mirror: point, dir in the plane, or point, normal in 3d.
364
+ kwargs: its keyword arguments.
365
+
366
+ Returns:
367
+ this compound.
368
+
369
+ Examples:
370
+ compound.Mirror((0, 0), (0, 1))
371
+ compound.Mirror((0, 0, 0), (1, 0, 0))
372
+ """
373
+ return self._each("Mirror", *args, **kwargs)
374
+
375
+ def Scale(self, *args: Any, **kwargs: Any) -> Compound:
376
+ """Scales every part in place, as Shape.Scale.
377
+
378
+ Args:
379
+ args: the arguments of Shape.Scale: center, factor in the plane, or point, factor in 3d.
380
+ kwargs: its keyword arguments.
381
+
382
+ Returns:
383
+ this compound.
384
+
385
+ Examples:
386
+ compound.Scale((0, 0), 2)
387
+ compound.Scale((0, 0), 0.5, scale_maxh=True)
388
+ """
389
+ return self._each("Scale", *args, **kwargs)
390
+
391
+ def Rotated(self, *args: Any, **kwargs: Any) -> Compound:
392
+ """A copy with every part rotated, as Shape.Rotated; this compound is not changed.
393
+
394
+ Args:
395
+ args: the arguments of Shape.Rotated: center, angle in the plane, or point, dir, angle in 3d.
396
+ kwargs: its keyword arguments.
397
+
398
+ Examples:
399
+ compound.Rotated((0, 0), 90)
400
+ compound.Rotated((0, 0, 0), (0, 0, 1), 45)
401
+ """
402
+ return self.Copy().Rotate(*args, **kwargs)
403
+
404
+ def Translated(self, *args: Any, **kwargs: Any) -> Compound:
405
+ """A copy with every part moved, as Shape.Translated; this compound is not changed.
406
+
407
+ Args:
408
+ args: the arguments of Shape.Translated: d or dh, dv in the plane, or d or dx, dy, dz in 3d.
409
+ kwargs: its keyword arguments.
410
+
411
+ Examples:
412
+ compound.Translated((10, 0))
413
+ compound.Translated(0, 0, 5)
414
+ """
415
+ return self.Copy().Translate(*args, **kwargs)
416
+
417
+ def Mirrored(self, *args: Any, **kwargs: Any) -> Compound:
418
+ """A copy with every part mirrored, as Shape.Mirrored; this compound is not changed.
419
+
420
+ Args:
421
+ args: the arguments of Shape.Mirrored: point, dir in the plane, or point, normal in 3d.
422
+ kwargs: its keyword arguments.
423
+
424
+ Examples:
425
+ compound.Mirrored((0, 0), (0, 1))
426
+ compound.Mirrored((0, 0, 0), (1, 0, 0))
427
+ """
428
+ return self.Copy().Mirror(*args, **kwargs)
429
+
430
+ def Scaled(self, *args: Any, **kwargs: Any) -> Compound:
431
+ """A copy with every part scaled, as Shape.Scaled; this compound is not changed.
432
+
433
+ Args:
434
+ args: the arguments of Shape.Scaled: center, factor in the plane, or point, factor in 3d.
435
+ kwargs: its keyword arguments.
436
+
437
+ Examples:
438
+ compound.Scaled((0, 0), 2)
439
+ compound.Scaled((0, 0), 0.5, scale_maxh=True)
440
+ """
441
+ return self.Copy().Scale(*args, **kwargs)