yapCAD 0.5.0__py2.py3-none-any.whl → 0.5.1__py2.py3-none-any.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.
- yapcad/boolean/__init__.py +21 -0
- yapcad/boolean/native.py +1012 -0
- yapcad/boolean/trimesh_engine.py +155 -0
- yapcad/combine.py +52 -14
- yapcad/drawable.py +404 -26
- yapcad/geom.py +116 -0
- yapcad/geom3d.py +237 -7
- yapcad/geom3d_util.py +486 -30
- yapcad/geom_util.py +160 -61
- yapcad/io/__init__.py +2 -1
- yapcad/io/step.py +323 -0
- yapcad/spline.py +232 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/METADATA +60 -14
- yapcad-0.5.1.dist-info/RECORD +32 -0
- yapcad-0.5.0.dist-info/RECORD +0 -27
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/WHEEL +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE.txt +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
"""Trimesh-backed boolean engine for yapCAD solids.
|
2
|
+
|
3
|
+
This engine is optional. It converts yapCAD solids to ``trimesh.Trimesh``
|
4
|
+
instances, dispatches boolean operations via :mod:`trimesh.boolean`, and
|
5
|
+
converts the resulting mesh back into a yapCAD solid.
|
6
|
+
|
7
|
+
Availability depends on both the ``trimesh`` package and at least one
|
8
|
+
boolean backend supported by ``trimesh`` (e.g. Blender, OpenSCAD, Cork).
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from typing import Iterable
|
14
|
+
|
15
|
+
import os
|
16
|
+
import numpy as np
|
17
|
+
|
18
|
+
try:
|
19
|
+
import trimesh
|
20
|
+
except ImportError: # pragma: no cover - optional dependency
|
21
|
+
trimesh = None # type: ignore[assignment]
|
22
|
+
|
23
|
+
from . import native as _native
|
24
|
+
|
25
|
+
ENGINE_NAME = "trimesh"
|
26
|
+
|
27
|
+
|
28
|
+
def engines_available() -> set[str]:
|
29
|
+
"""Return the set of trimesh boolean backends that are operational."""
|
30
|
+
|
31
|
+
if trimesh is None: # pragma: no cover - optional dependency
|
32
|
+
return set()
|
33
|
+
return set(trimesh.boolean.engines_available)
|
34
|
+
|
35
|
+
|
36
|
+
def is_available(backend: str | None = None) -> bool:
|
37
|
+
"""Check whether the engine can run (trimesh + backend present)."""
|
38
|
+
|
39
|
+
available = engines_available()
|
40
|
+
if not available:
|
41
|
+
return False
|
42
|
+
if backend is None:
|
43
|
+
return True
|
44
|
+
return backend in available
|
45
|
+
|
46
|
+
|
47
|
+
def _solid_to_mesh(sld) -> "trimesh.Trimesh":
|
48
|
+
if trimesh is None: # pragma: no cover - optional dependency
|
49
|
+
raise RuntimeError("trimesh is not installed")
|
50
|
+
|
51
|
+
triangles = list(_native._iter_triangles_from_solid(sld))
|
52
|
+
if not triangles:
|
53
|
+
return trimesh.Trimesh(vertices=np.zeros((0, 3)), faces=np.zeros((0, 3), dtype=np.int64), process=False)
|
54
|
+
|
55
|
+
vertex_map: dict[tuple[float, float, float], int] = {}
|
56
|
+
verts = []
|
57
|
+
faces = []
|
58
|
+
for tri in triangles:
|
59
|
+
face_inds = []
|
60
|
+
for pt in tri:
|
61
|
+
key = (round(pt[0], 9), round(pt[1], 9), round(pt[2], 9))
|
62
|
+
idx = vertex_map.get(key)
|
63
|
+
if idx is None:
|
64
|
+
idx = len(verts)
|
65
|
+
vertex_map[key] = idx
|
66
|
+
verts.append([pt[0], pt[1], pt[2]])
|
67
|
+
face_inds.append(idx)
|
68
|
+
faces.append(face_inds)
|
69
|
+
|
70
|
+
verts_np = np.asarray(verts, dtype=float)
|
71
|
+
faces_np = np.asarray(faces, dtype=np.int64)
|
72
|
+
mesh = trimesh.Trimesh(vertices=verts_np, faces=faces_np, process=False)
|
73
|
+
mesh.remove_unreferenced_vertices()
|
74
|
+
if not mesh.is_watertight:
|
75
|
+
mesh.merge_vertices()
|
76
|
+
mesh.remove_duplicate_faces()
|
77
|
+
mesh.remove_degenerate_faces()
|
78
|
+
mesh.process(validate=True)
|
79
|
+
try:
|
80
|
+
mesh.fix_normals()
|
81
|
+
except Exception:
|
82
|
+
pass
|
83
|
+
return mesh
|
84
|
+
|
85
|
+
|
86
|
+
def _mesh_to_solid(mesh: "trimesh.Trimesh", operation: str) -> list:
|
87
|
+
triangles = np.asarray(mesh.triangles)
|
88
|
+
if triangles.size == 0:
|
89
|
+
return _native._geom3d().solid([], [], ['boolean', f'{ENGINE_NAME}:{operation}'])
|
90
|
+
|
91
|
+
tri_points = []
|
92
|
+
for tri in triangles:
|
93
|
+
tri_points.append([
|
94
|
+
_native.point(tri[0][0], tri[0][1], tri[0][2]),
|
95
|
+
_native.point(tri[1][0], tri[1][1], tri[1][2]),
|
96
|
+
_native.point(tri[2][0], tri[2][1], tri[2][2]),
|
97
|
+
])
|
98
|
+
|
99
|
+
surface = _native._surface_from_triangles(tri_points)
|
100
|
+
if surface is None:
|
101
|
+
return _native._geom3d().solid([], [], ['boolean', f'{ENGINE_NAME}:{operation}'])
|
102
|
+
return _native._geom3d().solid([surface], [], ['boolean', f'{ENGINE_NAME}:{operation}'])
|
103
|
+
|
104
|
+
|
105
|
+
def solid_boolean(a, b, operation: str, tol=_native._DEFAULT_RAY_TOL, *, stitch: bool = False,
|
106
|
+
backend: str | None = None):
|
107
|
+
"""Perform a boolean between ``a`` and ``b`` using trimesh."""
|
108
|
+
|
109
|
+
if trimesh is None: # pragma: no cover - optional dependency
|
110
|
+
raise RuntimeError("trimesh is not installed; install trimesh to enable this engine")
|
111
|
+
|
112
|
+
available = engines_available()
|
113
|
+
if backend is not None and backend not in available:
|
114
|
+
raise RuntimeError(
|
115
|
+
f"trimesh backend '{backend}' is not available; install the appropriate binary (available: {available})"
|
116
|
+
)
|
117
|
+
if backend is None and not available:
|
118
|
+
raise RuntimeError(
|
119
|
+
"no trimesh boolean backends are available; install Blender, OpenSCAD, Cork, or another supported engine"
|
120
|
+
)
|
121
|
+
|
122
|
+
mesh_a = _solid_to_mesh(a)
|
123
|
+
mesh_b = _solid_to_mesh(b)
|
124
|
+
|
125
|
+
backup_cache = None
|
126
|
+
if backend == 'blender':
|
127
|
+
backup_cache = os.environ.get('ARCH_CACHE_LINE_SIZE')
|
128
|
+
os.environ['ARCH_CACHE_LINE_SIZE'] = '64'
|
129
|
+
|
130
|
+
op = operation.lower()
|
131
|
+
try:
|
132
|
+
if op == 'union':
|
133
|
+
result = trimesh.boolean.union([mesh_a, mesh_b], engine=backend, check_volume=False)
|
134
|
+
elif op == 'intersection':
|
135
|
+
result = trimesh.boolean.intersection([mesh_a, mesh_b], engine=backend, check_volume=False)
|
136
|
+
elif op == 'difference':
|
137
|
+
result = trimesh.boolean.difference([mesh_a, mesh_b], engine=backend, check_volume=False)
|
138
|
+
else:
|
139
|
+
raise RuntimeError(f"unsupported boolean operation '{operation}' for trimesh engine")
|
140
|
+
except Exception as exc: # pragma: no cover - depends on external binaries
|
141
|
+
raise RuntimeError(f"trimesh boolean operation failed: {exc}") from exc
|
142
|
+
finally:
|
143
|
+
if backend == 'blender':
|
144
|
+
if backup_cache is None:
|
145
|
+
os.environ.pop('ARCH_CACHE_LINE_SIZE', None)
|
146
|
+
else:
|
147
|
+
os.environ['ARCH_CACHE_LINE_SIZE'] = backup_cache
|
148
|
+
|
149
|
+
if result is None or result.faces.size == 0:
|
150
|
+
return _native._geom3d().solid([], [], ['boolean', f'{ENGINE_NAME}:{operation}'])
|
151
|
+
|
152
|
+
return _mesh_to_solid(result, operation)
|
153
|
+
|
154
|
+
|
155
|
+
__all__ = ['ENGINE_NAME', 'is_available', 'solid_boolean', 'engines_available']
|
yapcad/combine.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
## yapCAD boolen operation support
|
1
|
+
## yapCAD boolen operation support for 2D closed curves. For three dimensional
|
2
|
+
## boolean operations, see geom3d.py
|
2
3
|
|
3
4
|
from yapcad.geom import *
|
4
5
|
from yapcad.geom_util import *
|
@@ -12,7 +13,7 @@ class Boolean(Geometry):
|
|
12
13
|
def __repr__(self):
|
13
14
|
return f"Boolean({self.type},{self.elem})"
|
14
15
|
|
15
|
-
def __init__(self,type='union',polys=[]):
|
16
|
+
def __init__(self,type='union',polys=[], *, minang=5.0, minlen=0.25):
|
16
17
|
super().__init__()
|
17
18
|
self._setClosed(True)
|
18
19
|
self._setSampleable(True)
|
@@ -26,14 +27,49 @@ class Boolean(Geometry):
|
|
26
27
|
self.__type=type
|
27
28
|
self._setUpdate(True)
|
28
29
|
self.__outline=[]
|
30
|
+
self.__minang = float(minang)
|
31
|
+
self.__minlen = float(minlen)
|
29
32
|
|
30
33
|
@property
|
31
34
|
def type(self):
|
32
35
|
return self.__type
|
33
36
|
|
37
|
+
def _poly_to_segments(self, poly):
|
38
|
+
segments = []
|
39
|
+
if not poly:
|
40
|
+
return segments
|
41
|
+
for i in range(1, len(poly)):
|
42
|
+
segments.append([poly[i - 1], poly[i]])
|
43
|
+
return segments
|
44
|
+
|
45
|
+
def _prepare_geom(self, geom_obj):
|
46
|
+
if isinstance(geom_obj, Geometry):
|
47
|
+
gl = geom_obj.geom
|
48
|
+
else:
|
49
|
+
gl = geom_obj
|
50
|
+
|
51
|
+
# If gl is a single arc/line/poly, wrap it in a list for geomlist2poly_with_holes
|
52
|
+
if isarc(gl) or isline(gl) or ispoly(gl):
|
53
|
+
gl = [gl]
|
54
|
+
|
55
|
+
try:
|
56
|
+
outer, holes = geomlist2poly_with_holes(gl, self.__minang, self.__minlen, checkcont=False)
|
57
|
+
except Exception:
|
58
|
+
if isgeomlist(gl):
|
59
|
+
return gl
|
60
|
+
return []
|
61
|
+
|
62
|
+
if not outer:
|
63
|
+
return []
|
64
|
+
|
65
|
+
segments = self._poly_to_segments(outer)
|
66
|
+
for hole in holes:
|
67
|
+
segments.extend(self._poly_to_segments(hole))
|
68
|
+
return segments
|
69
|
+
|
34
70
|
def _combine_geom(self,g1,g2):
|
35
|
-
gl1 = g1
|
36
|
-
gl2 = g2
|
71
|
+
gl1 = self._prepare_geom(g1)
|
72
|
+
gl2 = self._prepare_geom(g2)
|
37
73
|
|
38
74
|
return combineglist(gl1,gl2,self.type)
|
39
75
|
|
@@ -83,16 +119,18 @@ class Boolean(Geometry):
|
|
83
119
|
@property
|
84
120
|
def geom(self):
|
85
121
|
if self.update:
|
86
|
-
if len(self.elem)
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
self.
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
122
|
+
if len(self.elem) < 2:
|
123
|
+
raise ValueError('Boolean requires at least two operands')
|
124
|
+
|
125
|
+
result = self.elem[0]
|
126
|
+
for operand in self.elem[1:]:
|
127
|
+
result = self._combine_geom(result, operand)
|
128
|
+
|
129
|
+
self.__outline = cullZeroLength(result)
|
130
|
+
self._setUpdate(False)
|
131
|
+
self._setBbox(bbox(self.__outline))
|
132
|
+
self._setLength(length(self.__outline))
|
133
|
+
self._setCenter(self._calcCenter())
|
96
134
|
return deepcopy(self.__outline)
|
97
135
|
|
98
136
|
|