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.
- planex/__init__.py +125 -0
- planex/_auto.py +17 -0
- planex/_caller.py +21 -0
- planex/_closing.py +45 -0
- planex/_compound.py +441 -0
- planex/_deformable.py +143 -0
- planex/_dimensions.py +818 -0
- planex/_drawing.py +1162 -0
- planex/_dxf.py +341 -0
- planex/_expressions.py +358 -0
- planex/_fileunits.py +40 -0
- planex/_geom.py +177 -0
- planex/_gmsh.py +210 -0
- planex/_ids.py +135 -0
- planex/_io.py +205 -0
- planex/_kicad.py +1139 -0
- planex/_kicadwrite.py +773 -0
- planex/_levelset.py +429 -0
- planex/_measurements.py +477 -0
- planex/_meshsection.py +219 -0
- planex/_naca.py +117 -0
- planex/_occ.py +56 -0
- planex/_parameters.py +143 -0
- planex/_planex.cp313-win_amd64.pyd +0 -0
- planex/_planex.pyi +2088 -0
- planex/_plot.py +649 -0
- planex/_plot3d.py +106 -0
- planex/_plot_fields.py +318 -0
- planex/_plot_mesh.py +260 -0
- planex/_plot_sketch.py +668 -0
- planex/_plot_style.py +222 -0
- planex/_select.py +1209 -0
- planex/_shapelyio.py +153 -0
- planex/_sketches.py +61 -0
- planex/_svg.py +785 -0
- planex/_svgfile.py +348 -0
- planex/_units.py +14 -0
- planex/_views.py +83 -0
- planex/_workplane.py +786 -0
- planex/debug.py +8 -0
- planex/fonts/LICENSE-GPL-3.0.txt +674 -0
- planex/fonts/LICENSE-LGPL-3.0.txt +165 -0
- planex/fonts/NOTICE.txt +19 -0
- planex/fonts/osifont-lgpl3fe.ttf +0 -0
- planex/measurements.py +12 -0
- planex/py.typed +0 -0
- planex/stackups/__init__.py +29 -0
- planex/stackups/_jlcpcb.py +108 -0
- planex/templates/title_block.dxf +5180 -0
- planex/types.py +70 -0
- planex2d-0.2.2.dist-info/METADATA +133 -0
- planex2d-0.2.2.dist-info/RECORD +62 -0
- planex2d-0.2.2.dist-info/WHEEL +5 -0
- planex2d-0.2.2.dist-info/licenses/LICENSE +504 -0
- planex2d-0.2.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +435 -0
- planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.APACHE +203 -0
- planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.BSD +26 -0
- planex2d-0.2.2.dist-info/licenses/external_dependencies/eigen/COPYING.MPL2 +373 -0
- planex2d-0.2.2.dist-info/licenses/external_dependencies/taskflow/LICENSE +25 -0
- planex2d-0.2.2.dist-info/licenses/src/python/planex/fonts/LICENSE-GPL-3.0.txt +674 -0
- planex2d-0.2.2.dist-info/licenses/src/python/planex/fonts/LICENSE-LGPL-3.0.txt +165 -0
- 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)
|