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
yapcad/boolean/native.py
ADDED
@@ -0,0 +1,1012 @@
|
|
1
|
+
"""Native boolean engine extracted from yapcad.geom3d."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import math
|
6
|
+
|
7
|
+
from yapcad.geom import *
|
8
|
+
from yapcad.geom_util import *
|
9
|
+
from yapcad.xform import *
|
10
|
+
from yapcad.triangulator import triangulate_polygon
|
11
|
+
from yapcad.octtree import NTree
|
12
|
+
|
13
|
+
|
14
|
+
def _geom3d():
|
15
|
+
from yapcad import geom3d as _g3
|
16
|
+
return _g3
|
17
|
+
|
18
|
+
def _ensure_surface_metadata_dict(s):
|
19
|
+
if not isinstance(s, list) or len(s) < 4 or s[0] != 'surface':
|
20
|
+
raise ValueError('bad surface passed to _ensure_surface_metadata_dict')
|
21
|
+
if len(s) == 4:
|
22
|
+
s.extend([[], [], {}])
|
23
|
+
return s[6]
|
24
|
+
if len(s) == 5:
|
25
|
+
s.extend([[], {}])
|
26
|
+
return s[6]
|
27
|
+
if len(s) == 6:
|
28
|
+
s.append({})
|
29
|
+
return s[6]
|
30
|
+
metadata = s[6]
|
31
|
+
if metadata is None:
|
32
|
+
metadata = {}
|
33
|
+
elif not isinstance(metadata, dict):
|
34
|
+
metadata = {'legacy_metadata': metadata}
|
35
|
+
s[6] = metadata
|
36
|
+
return metadata
|
37
|
+
|
38
|
+
|
39
|
+
def _triangle_bbox(tri, tol):
|
40
|
+
mins = [min(pt[i] for pt in tri) - tol for i in range(3)]
|
41
|
+
maxs = [max(pt[i] for pt in tri) + tol for i in range(3)]
|
42
|
+
return [point(mins[0], mins[1], mins[2]),
|
43
|
+
point(maxs[0], maxs[1], maxs[2])]
|
44
|
+
|
45
|
+
|
46
|
+
def _triangle_plane_intersection_points(tri, plane, tol):
|
47
|
+
n, d = plane
|
48
|
+
points = []
|
49
|
+
for idx in range(3):
|
50
|
+
p0 = tri[idx]
|
51
|
+
p1 = tri[(idx + 1) % 3]
|
52
|
+
dir_vec = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]]
|
53
|
+
denom = n[0] * dir_vec[0] + n[1] * dir_vec[1] + n[2] * dir_vec[2]
|
54
|
+
if abs(denom) < tol:
|
55
|
+
continue
|
56
|
+
numer = d - (n[0] * p0[0] + n[1] * p0[1] + n[2] * p0[2])
|
57
|
+
t = numer / denom
|
58
|
+
if t < -tol or t > 1.0 + tol:
|
59
|
+
continue
|
60
|
+
t_clamped = max(0.0, min(1.0, t))
|
61
|
+
pt = _lerp_point(p0, p1, t_clamped)
|
62
|
+
if abs(n[0] * pt[0] + n[1] * pt[1] + n[2] * pt[2] - d) <= tol * 10.0:
|
63
|
+
points.append(pt)
|
64
|
+
return _unique_points(points, tol) if points else []
|
65
|
+
|
66
|
+
|
67
|
+
def _candidate_planes_for_triangle(tri, target, tri_plane, tol):
|
68
|
+
box = _geom3d().solidbbox(target)
|
69
|
+
if box:
|
70
|
+
center = point((box[0][0] + box[1][0]) / 2.0,
|
71
|
+
(box[0][1] + box[1][1]) / 2.0,
|
72
|
+
(box[0][2] + box[1][2]) / 2.0)
|
73
|
+
extent = max(box[1][0] - box[0][0],
|
74
|
+
box[1][1] - box[0][1],
|
75
|
+
box[1][2] - box[0][2])
|
76
|
+
else:
|
77
|
+
center = point(0, 0, 0)
|
78
|
+
extent = 0.0
|
79
|
+
|
80
|
+
epsilon_dot = max(extent * 1e-6, 1e-6)
|
81
|
+
|
82
|
+
tri_bbox = _triangle_bbox(tri, tol * 2.0)
|
83
|
+
seen = set()
|
84
|
+
planes = []
|
85
|
+
snap_candidates = []
|
86
|
+
for surf in target[1]:
|
87
|
+
tree = surface_octree(surf)
|
88
|
+
if tree is None:
|
89
|
+
candidates = list(_iter_triangles_from_surface(surf))
|
90
|
+
else:
|
91
|
+
elems = tree.getElements(tri_bbox)
|
92
|
+
candidates = []
|
93
|
+
for elem in elems:
|
94
|
+
if isinstance(elem, list):
|
95
|
+
candidates.append(elem)
|
96
|
+
elif isinstance(elem, tuple):
|
97
|
+
candidates.append(elem[0])
|
98
|
+
else:
|
99
|
+
candidates.append(elem)
|
100
|
+
if not candidates:
|
101
|
+
candidates = list(_iter_triangles_from_surface(surf))
|
102
|
+
for cand in candidates:
|
103
|
+
plane = _triangle_plane(cand)
|
104
|
+
n, d = plane
|
105
|
+
centroid = point(
|
106
|
+
(cand[0][0] + cand[1][0] + cand[2][0]) / 3.0,
|
107
|
+
(cand[0][1] + cand[1][1] + cand[2][1]) / 3.0,
|
108
|
+
(cand[0][2] + cand[1][2] + cand[2][2]) / 3.0,
|
109
|
+
)
|
110
|
+
vec = point(centroid[0] - center[0],
|
111
|
+
centroid[1] - center[1],
|
112
|
+
centroid[2] - center[2])
|
113
|
+
dot_sign = n[0] * vec[0] + n[1] * vec[1] + n[2] * vec[2]
|
114
|
+
if dot_sign > epsilon_dot:
|
115
|
+
sense = -1
|
116
|
+
elif dot_sign < -epsilon_dot:
|
117
|
+
sense = 1
|
118
|
+
else:
|
119
|
+
center_eval = _plane_eval(plane, center)
|
120
|
+
sense = -1 if center_eval <= 0 else 1
|
121
|
+
plane_with_sense = (n, d, sense)
|
122
|
+
key = _plane_key(plane_with_sense, tol)
|
123
|
+
if key not in seen:
|
124
|
+
seen.add(key)
|
125
|
+
planes.append(plane_with_sense)
|
126
|
+
snap_candidates.extend(_triangle_plane_intersection_points(cand, tri_plane, tol))
|
127
|
+
unique_snap = _unique_points(snap_candidates, max(tol * 10.0, 1e-6)) if snap_candidates else []
|
128
|
+
return planes, unique_snap
|
129
|
+
|
130
|
+
|
131
|
+
_DEFAULT_RAY_TOL = 1e-7
|
132
|
+
|
133
|
+
|
134
|
+
def invalidate_surface_octree(s):
|
135
|
+
meta = _ensure_surface_metadata_dict(s)
|
136
|
+
meta.pop('_octree', None)
|
137
|
+
meta['_octree_dirty'] = True
|
138
|
+
|
139
|
+
|
140
|
+
def _bbox_overlap(box_a, box_b, tol):
|
141
|
+
return not (
|
142
|
+
box_a[1][0] < box_b[0][0] - tol or
|
143
|
+
box_a[0][0] > box_b[1][0] + tol or
|
144
|
+
box_a[1][1] < box_b[0][1] - tol or
|
145
|
+
box_a[0][1] > box_b[1][1] + tol or
|
146
|
+
box_a[1][2] < box_b[0][2] - tol or
|
147
|
+
box_a[0][2] > box_b[1][2] + tol
|
148
|
+
)
|
149
|
+
|
150
|
+
|
151
|
+
def _segment_bbox(p0, p1, pad):
|
152
|
+
return [
|
153
|
+
point(min(p0[0], p1[0]) - pad, min(p0[1], p1[1]) - pad, min(p0[2], p1[2]) - pad),
|
154
|
+
point(max(p0[0], p1[0]) + pad, max(p0[1], p1[1]) + pad, max(p0[2], p1[2]) + pad),
|
155
|
+
]
|
156
|
+
|
157
|
+
|
158
|
+
def _plane_eval(plane, p):
|
159
|
+
n, d = plane
|
160
|
+
return dot(n, p) - d
|
161
|
+
|
162
|
+
|
163
|
+
def _segment_plane_intersection(p1, p2, plane, tol):
|
164
|
+
n, d = plane
|
165
|
+
direction = sub(p2, p1)
|
166
|
+
denom = dot(n, direction)
|
167
|
+
if abs(denom) < tol:
|
168
|
+
return point((p1[0] + p2[0]) * 0.5, (p1[1] + p2[1]) * 0.5, (p1[2] + p2[2]) * 0.5)
|
169
|
+
t = (d - dot(n, p1)) / denom
|
170
|
+
t = max(0.0, min(1.0, t))
|
171
|
+
return point(p1[0] + direction[0] * t,
|
172
|
+
p1[1] + direction[1] * t,
|
173
|
+
p1[2] + direction[2] * t)
|
174
|
+
|
175
|
+
|
176
|
+
def _dedupe_polygon(poly, tol):
|
177
|
+
if not poly:
|
178
|
+
return []
|
179
|
+
deduped = [poly[0]]
|
180
|
+
for pt in poly[1:]:
|
181
|
+
if dist(pt, deduped[-1]) > tol:
|
182
|
+
deduped.append(pt)
|
183
|
+
if len(deduped) > 2 and dist(deduped[0], deduped[-1]) <= tol:
|
184
|
+
deduped[-1] = deduped[0]
|
185
|
+
deduped.pop()
|
186
|
+
return deduped
|
187
|
+
|
188
|
+
|
189
|
+
def _clip_polygon_against_plane(poly, plane, tol, keep_inside=True):
|
190
|
+
if not poly:
|
191
|
+
return []
|
192
|
+
n, d, sense = plane
|
193
|
+
clipped = []
|
194
|
+
prev = poly[-1]
|
195
|
+
prev_eval = _plane_eval((n, d), prev)
|
196
|
+
if sense <= 0:
|
197
|
+
prev_inside = prev_eval <= tol if keep_inside else prev_eval >= -tol
|
198
|
+
else:
|
199
|
+
prev_inside = prev_eval >= -tol if keep_inside else prev_eval <= tol
|
200
|
+
for curr in poly:
|
201
|
+
curr_eval = _plane_eval((n, d), curr)
|
202
|
+
if sense <= 0:
|
203
|
+
curr_inside = curr_eval <= tol if keep_inside else curr_eval >= -tol
|
204
|
+
else:
|
205
|
+
curr_inside = curr_eval >= -tol if keep_inside else curr_eval <= tol
|
206
|
+
if curr_inside:
|
207
|
+
if not prev_inside:
|
208
|
+
clipped.append(_segment_plane_intersection(prev, curr, (n, d), tol))
|
209
|
+
clipped.append(point(curr))
|
210
|
+
elif prev_inside:
|
211
|
+
clipped.append(_segment_plane_intersection(prev, curr, (n, d), tol))
|
212
|
+
prev = curr
|
213
|
+
prev_eval = curr_eval
|
214
|
+
prev_inside = curr_inside
|
215
|
+
clipped = _dedupe_polygon(clipped, tol)
|
216
|
+
if not keep_inside and len(clipped) >= 3:
|
217
|
+
clipped = list(reversed(clipped))
|
218
|
+
return clipped
|
219
|
+
|
220
|
+
|
221
|
+
def _split_polygon_by_plane(poly, plane, tol):
|
222
|
+
if not poly:
|
223
|
+
return [], []
|
224
|
+
|
225
|
+
n, d, sense = plane
|
226
|
+
evals = [_plane_eval((n, d), p) for p in poly]
|
227
|
+
max_eval = max(evals)
|
228
|
+
min_eval = min(evals)
|
229
|
+
|
230
|
+
if sense <= 0:
|
231
|
+
if max_eval <= tol:
|
232
|
+
return [point(p) for p in poly], []
|
233
|
+
if min_eval >= -tol:
|
234
|
+
return [], [[point(p) for p in poly]]
|
235
|
+
else:
|
236
|
+
if min_eval >= -tol:
|
237
|
+
return [point(p) for p in poly], []
|
238
|
+
if max_eval <= tol:
|
239
|
+
return [], [[point(p) for p in poly]]
|
240
|
+
|
241
|
+
inside = _clip_polygon_against_plane(poly, plane, tol, keep_inside=True)
|
242
|
+
outside = _clip_polygon_against_plane(poly, plane, tol, keep_inside=False)
|
243
|
+
outside_polys = [outside] if outside else []
|
244
|
+
return inside, outside_polys
|
245
|
+
|
246
|
+
|
247
|
+
def _split_polygon_by_planes(poly, planes, tol):
|
248
|
+
inside_polys = [poly]
|
249
|
+
outside_polys = []
|
250
|
+
for plane in planes:
|
251
|
+
next_inside = []
|
252
|
+
for current in inside_polys:
|
253
|
+
inside, outside = _split_polygon_by_plane(current, plane, tol)
|
254
|
+
outside_polys.extend(outside)
|
255
|
+
if inside:
|
256
|
+
next_inside.append(inside)
|
257
|
+
inside_polys = next_inside
|
258
|
+
if not inside_polys:
|
259
|
+
break
|
260
|
+
return inside_polys, outside_polys
|
261
|
+
|
262
|
+
|
263
|
+
def _triangulate_polygon(poly, reference_normal=None):
|
264
|
+
if len(poly) < 3:
|
265
|
+
return []
|
266
|
+
if len(poly) == 3:
|
267
|
+
tri = [point(poly[0]), point(poly[1]), point(poly[2])]
|
268
|
+
if reference_normal is not None:
|
269
|
+
v01 = sub(tri[1], tri[0])
|
270
|
+
v02 = sub(tri[2], tri[0])
|
271
|
+
if dot(cross(v01, v02), reference_normal) < 0:
|
272
|
+
tri[1], tri[2] = tri[2], tri[1]
|
273
|
+
return [tri]
|
274
|
+
|
275
|
+
anchor = point(poly[0])
|
276
|
+
triangles = []
|
277
|
+
for i in range(1, len(poly) - 1):
|
278
|
+
tri = [anchor, point(poly[i]), point(poly[i + 1])]
|
279
|
+
if reference_normal is not None:
|
280
|
+
v01 = sub(tri[1], tri[0])
|
281
|
+
v02 = sub(tri[2], tri[0])
|
282
|
+
if dot(cross(v01, v02), reference_normal) < 0:
|
283
|
+
tri[1], tri[2] = tri[2], tri[1]
|
284
|
+
triangles.append(tri)
|
285
|
+
return triangles
|
286
|
+
|
287
|
+
|
288
|
+
def _triangle_plane(tri):
|
289
|
+
p0, n = _geom3d().tri2p0n(tri)
|
290
|
+
d = dot(n, p0)
|
291
|
+
return n, d
|
292
|
+
|
293
|
+
|
294
|
+
def _plane_key(plane, tol):
|
295
|
+
n, d, sense = plane
|
296
|
+
scale = 1.0 / tol
|
297
|
+
return (round(n[0] * scale), round(n[1] * scale), round(n[2] * scale), round(d * scale), sense)
|
298
|
+
|
299
|
+
|
300
|
+
def _sub3(a, b):
|
301
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
302
|
+
|
303
|
+
|
304
|
+
def _dot3(a, b):
|
305
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
306
|
+
|
307
|
+
|
308
|
+
def _cross3(a, b):
|
309
|
+
return [
|
310
|
+
a[1] * b[2] - a[2] * b[1],
|
311
|
+
a[2] * b[0] - a[0] * b[2],
|
312
|
+
a[0] * b[1] - a[1] * b[0],
|
313
|
+
]
|
314
|
+
|
315
|
+
|
316
|
+
def _mag3(v):
|
317
|
+
return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
|
318
|
+
|
319
|
+
|
320
|
+
def _normalize3(v, tol):
|
321
|
+
m = _mag3(v)
|
322
|
+
if m < tol:
|
323
|
+
return [0.0, 0.0, 0.0]
|
324
|
+
return [v[0] / m, v[1] / m, v[2] / m]
|
325
|
+
|
326
|
+
|
327
|
+
def _lerp_point(p0, p1, t):
|
328
|
+
return point(
|
329
|
+
p0[0] + (p1[0] - p0[0]) * t,
|
330
|
+
p0[1] + (p1[1] - p0[1]) * t,
|
331
|
+
p0[2] + (p1[2] - p0[2]) * t,
|
332
|
+
)
|
333
|
+
|
334
|
+
|
335
|
+
|
336
|
+
def _point_in_triangle(pt, tri, tol):
|
337
|
+
a = [tri[0][0], tri[0][1], tri[0][2]]
|
338
|
+
b = [tri[1][0], tri[1][1], tri[1][2]]
|
339
|
+
c = [tri[2][0], tri[2][1], tri[2][2]]
|
340
|
+
p = [pt[0], pt[1], pt[2]]
|
341
|
+
|
342
|
+
v0 = _sub3(c, a)
|
343
|
+
v1 = _sub3(b, a)
|
344
|
+
v2 = _sub3(p, a)
|
345
|
+
|
346
|
+
dot00 = _dot3(v0, v0)
|
347
|
+
dot01 = _dot3(v0, v1)
|
348
|
+
dot02 = _dot3(v0, v2)
|
349
|
+
dot11 = _dot3(v1, v1)
|
350
|
+
dot12 = _dot3(v1, v2)
|
351
|
+
|
352
|
+
denom = dot00 * dot11 - dot01 * dot01
|
353
|
+
if abs(denom) < tol:
|
354
|
+
return False
|
355
|
+
inv = 1.0 / denom
|
356
|
+
u = (dot11 * dot02 - dot01 * dot12) * inv
|
357
|
+
v = (dot00 * dot12 - dot01 * dot02) * inv
|
358
|
+
return u >= -tol and v >= -tol and (u + v) <= 1.0 + tol
|
359
|
+
|
360
|
+
|
361
|
+
def _segment_triangle_intersection_param(p0, p1, tri, tol):
|
362
|
+
n, d = _triangle_plane(tri)
|
363
|
+
dir_vec = _sub3([p1[0], p1[1], p1[2]], [p0[0], p0[1], p0[2]])
|
364
|
+
denom = _dot3(n, dir_vec)
|
365
|
+
if abs(denom) < tol:
|
366
|
+
return None
|
367
|
+
numer = d - (n[0] * p0[0] + n[1] * p0[1] + n[2] * p0[2])
|
368
|
+
t = numer / denom
|
369
|
+
if t < -tol or t > 1.0 + tol:
|
370
|
+
return None
|
371
|
+
t_clamped = max(0.0, min(1.0, t))
|
372
|
+
intersection = _lerp_point(p0, p1, t_clamped)
|
373
|
+
if not _point_in_triangle(intersection, tri, tol):
|
374
|
+
return None
|
375
|
+
return (t_clamped, intersection)
|
376
|
+
|
377
|
+
|
378
|
+
def _collect_segment_intersections(p0, p1, solid, tol):
|
379
|
+
results = []
|
380
|
+
query_box = _segment_bbox(p0, p1, tol * 5)
|
381
|
+
for surf in solid[1]:
|
382
|
+
tree = surface_octree(surf)
|
383
|
+
if tree is None:
|
384
|
+
candidates = list(_iter_triangles_from_surface(surf))
|
385
|
+
else:
|
386
|
+
elems = tree.getElements(query_box)
|
387
|
+
candidates = []
|
388
|
+
for elem in elems:
|
389
|
+
if isinstance(elem, list):
|
390
|
+
candidates.append(elem)
|
391
|
+
elif isinstance(elem, tuple):
|
392
|
+
candidates.append(elem[0])
|
393
|
+
else:
|
394
|
+
candidates.append(elem)
|
395
|
+
for tri in candidates:
|
396
|
+
res = _segment_triangle_intersection_param(p0, p1, tri, tol)
|
397
|
+
if res is not None:
|
398
|
+
results.append(res)
|
399
|
+
return results
|
400
|
+
|
401
|
+
|
402
|
+
def _unique_points(points, tol):
|
403
|
+
unique = []
|
404
|
+
for pt in points:
|
405
|
+
candidate = point(pt)
|
406
|
+
if not any(dist(candidate, existing) <= tol for existing in unique):
|
407
|
+
unique.append(candidate)
|
408
|
+
return unique
|
409
|
+
|
410
|
+
|
411
|
+
def _points_to_polygon(points, reference_normal, tol):
|
412
|
+
unique = _unique_points(points, tol)
|
413
|
+
if len(unique) < 3:
|
414
|
+
return []
|
415
|
+
|
416
|
+
centroid = point(
|
417
|
+
sum(p[0] for p in unique) / len(unique),
|
418
|
+
sum(p[1] for p in unique) / len(unique),
|
419
|
+
sum(p[2] for p in unique) / len(unique),
|
420
|
+
)
|
421
|
+
|
422
|
+
basis_u = None
|
423
|
+
for pt in unique:
|
424
|
+
vec = _sub3([pt[0], pt[1], pt[2]], [centroid[0], centroid[1], centroid[2]])
|
425
|
+
if _mag3(vec) > tol:
|
426
|
+
basis_u = _normalize3(vec, tol)
|
427
|
+
break
|
428
|
+
if basis_u is None:
|
429
|
+
return []
|
430
|
+
|
431
|
+
ref = _normalize3([reference_normal[0], reference_normal[1], reference_normal[2]], tol)
|
432
|
+
basis_v = _cross3(ref, basis_u)
|
433
|
+
if _mag3(basis_v) < tol:
|
434
|
+
basis_v = _cross3(basis_u, ref)
|
435
|
+
basis_v = _normalize3(basis_v, tol)
|
436
|
+
|
437
|
+
def _angle(pt):
|
438
|
+
vec = _sub3([pt[0], pt[1], pt[2]], [centroid[0], centroid[1], centroid[2]])
|
439
|
+
x = _dot3(vec, basis_u)
|
440
|
+
y = _dot3(vec, basis_v)
|
441
|
+
return math.atan2(y, x)
|
442
|
+
|
443
|
+
ordered = sorted(unique, key=_angle)
|
444
|
+
compact = [ordered[0]]
|
445
|
+
for pt in ordered[1:]:
|
446
|
+
if dist(compact[-1], pt) > tol:
|
447
|
+
compact.append(pt)
|
448
|
+
if dist(compact[0], compact[-1]) <= tol:
|
449
|
+
compact[-1] = compact[0]
|
450
|
+
compact = compact[:-1]
|
451
|
+
if len(compact) < 3:
|
452
|
+
return []
|
453
|
+
|
454
|
+
v0 = _sub3([compact[1][0], compact[1][1], compact[1][2]], [compact[0][0], compact[0][1], compact[0][2]])
|
455
|
+
v1 = _sub3([compact[2][0], compact[2][1], compact[2][2]], [compact[1][0], compact[1][1], compact[1][2]])
|
456
|
+
normal = _cross3(v0, v1)
|
457
|
+
if _mag3(normal) < tol:
|
458
|
+
return []
|
459
|
+
if _dot3(normal, [reference_normal[0], reference_normal[1], reference_normal[2]]) < 0:
|
460
|
+
compact.reverse()
|
461
|
+
|
462
|
+
return compact
|
463
|
+
|
464
|
+
|
465
|
+
def _orient_triangle(tri, reference_normal, tol):
|
466
|
+
pts = [point(tri[0]), point(tri[1]), point(tri[2])]
|
467
|
+
v01 = sub(pts[1], pts[0])
|
468
|
+
v02 = sub(pts[2], pts[0])
|
469
|
+
normal = cross(v01, v02)
|
470
|
+
area_mag = mag(normal)
|
471
|
+
if area_mag < tol:
|
472
|
+
return None
|
473
|
+
ref = reference_normal
|
474
|
+
if ref is not None:
|
475
|
+
ref_vec = [ref[0], ref[1], ref[2]]
|
476
|
+
if dot(normal, ref_vec) < 0:
|
477
|
+
pts[1], pts[2] = pts[2], pts[1]
|
478
|
+
return pts
|
479
|
+
|
480
|
+
|
481
|
+
def _clip_triangle_against_solid(tri, target, tol, reference_normal):
|
482
|
+
polygon = [point(tri[0]), point(tri[1]), point(tri[2])]
|
483
|
+
centroid = point(
|
484
|
+
(tri[0][0] + tri[1][0] + tri[2][0]) / 3.0,
|
485
|
+
(tri[0][1] + tri[1][1] + tri[2][1]) / 3.0,
|
486
|
+
(tri[0][2] + tri[1][2] + tri[2][2]) / 3.0,
|
487
|
+
)
|
488
|
+
|
489
|
+
tri_plane = _triangle_plane(tri)
|
490
|
+
planes, snap_candidates = _candidate_planes_for_triangle(tri, target, tri_plane, tol)
|
491
|
+
|
492
|
+
inside_polys = []
|
493
|
+
if planes:
|
494
|
+
inside_raw, _ = _split_polygon_by_planes(polygon, planes, tol)
|
495
|
+
for poly in inside_raw:
|
496
|
+
cleaned = _dedupe_polygon([point(p) for p in poly], tol)
|
497
|
+
if len(cleaned) >= 3:
|
498
|
+
inside_polys.append(cleaned)
|
499
|
+
|
500
|
+
if not inside_polys:
|
501
|
+
if solid_contains_point(target, centroid, tol=tol):
|
502
|
+
inside_polys = [polygon]
|
503
|
+
else:
|
504
|
+
oriented = _orient_triangle(tri, reference_normal, tol)
|
505
|
+
if oriented:
|
506
|
+
return [], [oriented], False
|
507
|
+
return [], [], False
|
508
|
+
|
509
|
+
_, _, to_local, to_world = _geom3d().tri2p0n(tri, basis=True)
|
510
|
+
|
511
|
+
if snap_candidates:
|
512
|
+
snap_tol = max(5e-2, tol * 1e6)
|
513
|
+
|
514
|
+
def _snap_point(pt):
|
515
|
+
best = snap_tol
|
516
|
+
best_candidate = None
|
517
|
+
for cand in snap_candidates:
|
518
|
+
d = dist(pt, cand)
|
519
|
+
if d < best:
|
520
|
+
best = d
|
521
|
+
best_candidate = cand
|
522
|
+
return point(best_candidate) if best_candidate is not None else point(pt)
|
523
|
+
|
524
|
+
snapped = []
|
525
|
+
for poly in inside_polys:
|
526
|
+
snapped_poly = [_snap_point(pt) for pt in poly]
|
527
|
+
snapped_poly = _dedupe_polygon(snapped_poly, tol)
|
528
|
+
if len(snapped_poly) >= 3:
|
529
|
+
snapped.append(snapped_poly)
|
530
|
+
if snapped:
|
531
|
+
inside_polys = snapped
|
532
|
+
|
533
|
+
def _to_local(pt):
|
534
|
+
loc = to_local.mul(pt)
|
535
|
+
return (loc[0], loc[1])
|
536
|
+
|
537
|
+
def _from_local(xy):
|
538
|
+
local_pt = point(xy[0], xy[1], 0.0)
|
539
|
+
world_pt = to_world.mul(local_pt)
|
540
|
+
return point(world_pt[0], world_pt[1], world_pt[2])
|
541
|
+
|
542
|
+
outer_loop = [_to_local(pt) for pt in polygon]
|
543
|
+
inside_loops = [[_to_local(pt) for pt in poly] for poly in inside_polys]
|
544
|
+
|
545
|
+
inside_triangles = []
|
546
|
+
for loop in inside_loops:
|
547
|
+
tri2d_list = triangulate_polygon(loop)
|
548
|
+
for tri2d in tri2d_list:
|
549
|
+
tri3d = [_from_local(pt) for pt in tri2d]
|
550
|
+
oriented = _orient_triangle(tri3d, reference_normal, tol)
|
551
|
+
if oriented:
|
552
|
+
inside_triangles.append(oriented)
|
553
|
+
|
554
|
+
holes = inside_loops if inside_loops else None
|
555
|
+
outside_triangles = []
|
556
|
+
tri2d_list = triangulate_polygon(outer_loop, holes=holes)
|
557
|
+
for tri2d in tri2d_list:
|
558
|
+
tri3d = [_from_local(pt) for pt in tri2d]
|
559
|
+
# Don't call _orient_triangle! The winding order is already preserved through:
|
560
|
+
# 1. Original triangle had correct outward normal
|
561
|
+
# 2. Clipping preserves vertex order (just removes parts)
|
562
|
+
# 3. Transform to 2D preserves order
|
563
|
+
# 4. triangulate_polygon produces CCW triangles in 2D
|
564
|
+
# 5. Transform back to 3D should maintain correct orientation
|
565
|
+
# Check for degeneracy with better quality metric:
|
566
|
+
v01 = sub(tri3d[1], tri3d[0])
|
567
|
+
v02 = sub(tri3d[2], tri3d[0])
|
568
|
+
cross_prod = cross(v01, v02)
|
569
|
+
area = mag(cross_prod)
|
570
|
+
if area >= tol:
|
571
|
+
# Also filter out very thin slivers by checking aspect ratio
|
572
|
+
# A degenerate sliver has tiny area relative to edge lengths
|
573
|
+
edge_lengths = [mag(v01), mag(v02), dist(tri3d[1], tri3d[2])]
|
574
|
+
max_edge = max(edge_lengths)
|
575
|
+
# For a reasonable triangle, area should be at least 1% of max_edge^2
|
576
|
+
# This filters out slivers where one edge is extremely small
|
577
|
+
quality_threshold = 0.01 * max_edge * max_edge
|
578
|
+
if area >= max(tol, quality_threshold):
|
579
|
+
outside_triangles.append([point(tri3d[0]), point(tri3d[1]), point(tri3d[2])])
|
580
|
+
|
581
|
+
split = bool(inside_triangles) and bool(outside_triangles)
|
582
|
+
return inside_triangles, outside_triangles, split
|
583
|
+
|
584
|
+
|
585
|
+
|
586
|
+
|
587
|
+
def stitch_open_edges(triangles, tol):
|
588
|
+
"""Experimental: attempt to close open edge loops by triangulating them.
|
589
|
+
|
590
|
+
Parameters
|
591
|
+
----------
|
592
|
+
triangles : Iterable
|
593
|
+
A sequence of triangle coordinate lists ``[[x, y, z, w], ...]``
|
594
|
+
describing a mesh with potential boundary edges.
|
595
|
+
tol : float
|
596
|
+
Tolerance used when deduplicating vertices and detecting shared
|
597
|
+
edges. Typically reuse ``_DEFAULT_RAY_TOL``.
|
598
|
+
|
599
|
+
Returns
|
600
|
+
-------
|
601
|
+
list
|
602
|
+
A new list of triangles with any stitched faces appended.
|
603
|
+
"""
|
604
|
+
if not triangles:
|
605
|
+
return triangles
|
606
|
+
|
607
|
+
oriented_edges = []
|
608
|
+
canonical_counts = {}
|
609
|
+
base_triangles = []
|
610
|
+
for tri in triangles:
|
611
|
+
try:
|
612
|
+
n = _triangle_normal(tri)
|
613
|
+
except ValueError:
|
614
|
+
continue
|
615
|
+
pts = [point(tri[0]), point(tri[1]), point(tri[2])]
|
616
|
+
base_triangles.append(pts)
|
617
|
+
edges = ((pts[0], pts[1]), (pts[1], pts[2]), (pts[2], pts[0]))
|
618
|
+
for start, end in edges:
|
619
|
+
canonical = _canonical_edge_key(start, end)
|
620
|
+
canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1
|
621
|
+
oriented_edges.append((start, end, n))
|
622
|
+
|
623
|
+
boundary = []
|
624
|
+
for start, end, normal in oriented_edges:
|
625
|
+
if canonical_counts[_canonical_edge_key(start, end)] == 1:
|
626
|
+
boundary.append((start, end, normal))
|
627
|
+
|
628
|
+
if not boundary:
|
629
|
+
return base_triangles
|
630
|
+
|
631
|
+
def edge_id(start, end):
|
632
|
+
return (_point_to_key(start), _point_to_key(end))
|
633
|
+
|
634
|
+
adjacency = {}
|
635
|
+
for start, end, normal in boundary:
|
636
|
+
adjacency.setdefault(_point_to_key(start), []).append((start, end, normal))
|
637
|
+
|
638
|
+
used = set()
|
639
|
+
loops = []
|
640
|
+
for start, end, normal in boundary:
|
641
|
+
eid = edge_id(start, end)
|
642
|
+
if eid in used:
|
643
|
+
continue
|
644
|
+
loop = []
|
645
|
+
normals = []
|
646
|
+
current_start = start
|
647
|
+
current_end = end
|
648
|
+
current_normal = normal
|
649
|
+
start_key = _point_to_key(current_start)
|
650
|
+
while True:
|
651
|
+
loop.append(point(current_start))
|
652
|
+
normals.append(current_normal)
|
653
|
+
used.add(edge_id(current_start, current_end))
|
654
|
+
next_key = _point_to_key(current_end)
|
655
|
+
if next_key == start_key:
|
656
|
+
break
|
657
|
+
candidates = adjacency.get(next_key)
|
658
|
+
if not candidates:
|
659
|
+
loop = []
|
660
|
+
break
|
661
|
+
next_edge = None
|
662
|
+
for candidate in candidates:
|
663
|
+
cid = edge_id(candidate[0], candidate[1])
|
664
|
+
if cid not in used:
|
665
|
+
next_edge = candidate
|
666
|
+
break
|
667
|
+
if next_edge is None:
|
668
|
+
loop = []
|
669
|
+
break
|
670
|
+
current_start, current_end, current_normal = next_edge
|
671
|
+
if loop and len(loop) >= 3:
|
672
|
+
loops.append((loop, normals))
|
673
|
+
|
674
|
+
if not loops:
|
675
|
+
return base_triangles
|
676
|
+
|
677
|
+
stitched = list(base_triangles)
|
678
|
+
for loop_points, normals in loops:
|
679
|
+
polygon = _unique_points(loop_points, tol)
|
680
|
+
if len(polygon) < 3:
|
681
|
+
continue
|
682
|
+
avg = [0.0, 0.0, 0.0]
|
683
|
+
for n in normals:
|
684
|
+
avg[0] += n[0]
|
685
|
+
avg[1] += n[1]
|
686
|
+
avg[2] += n[2]
|
687
|
+
avg = _normalize3(avg, tol)
|
688
|
+
if _mag3(avg) < tol:
|
689
|
+
avg = normals[0]
|
690
|
+
poly = _points_to_polygon(polygon, avg, tol)
|
691
|
+
if not poly:
|
692
|
+
continue
|
693
|
+
for tri in _triangulate_polygon(poly, avg):
|
694
|
+
try:
|
695
|
+
normal = _triangle_normal(tri)
|
696
|
+
except ValueError:
|
697
|
+
continue
|
698
|
+
if _dot3(normal, [avg[0], avg[1], avg[2]]) < 0:
|
699
|
+
tri = [tri[0], tri[2], tri[1]]
|
700
|
+
try:
|
701
|
+
normal = _triangle_normal(tri)
|
702
|
+
except ValueError:
|
703
|
+
continue
|
704
|
+
stitched.append(tri)
|
705
|
+
return stitched
|
706
|
+
|
707
|
+
def stitch_solid(sld, tol=_DEFAULT_RAY_TOL):
|
708
|
+
"""Experimental helper that runs :func:`stitch_open_edges` on a solid."""
|
709
|
+
|
710
|
+
if not _geom3d().issolid(sld, fast=False):
|
711
|
+
raise ValueError('invalid solid passed to stitch_solid')
|
712
|
+
|
713
|
+
triangles = list(_iter_triangles_from_solid(sld))
|
714
|
+
stitched = stitch_open_edges(triangles, tol)
|
715
|
+
if not stitched:
|
716
|
+
return sld
|
717
|
+
|
718
|
+
surface_result = _surface_from_triangles(stitched)
|
719
|
+
if surface_result:
|
720
|
+
return _geom3d().solid([surface_result], [], ['utility', 'stitch_open_edges'])
|
721
|
+
return sld
|
722
|
+
|
723
|
+
|
724
|
+
|
725
|
+
def _boolean_fragments(source, target, tol):
|
726
|
+
outside_tris = []
|
727
|
+
inside_tris = []
|
728
|
+
inside_overlap = []
|
729
|
+
|
730
|
+
for tri in _iter_triangles_from_solid(source):
|
731
|
+
reference_normal = _triangle_normal(tri)
|
732
|
+
inside_parts, outside_parts, split = _clip_triangle_against_solid(
|
733
|
+
tri, target, tol, reference_normal
|
734
|
+
)
|
735
|
+
if inside_parts:
|
736
|
+
inside_tris.extend(inside_parts)
|
737
|
+
if outside_parts:
|
738
|
+
outside_tris.extend(outside_parts)
|
739
|
+
if split and inside_parts:
|
740
|
+
inside_overlap.extend(inside_parts)
|
741
|
+
return outside_tris, inside_tris, inside_overlap
|
742
|
+
|
743
|
+
|
744
|
+
|
745
|
+
def _ray_triangle_intersection(origin, direction, triangle, tol=_DEFAULT_RAY_TOL):
|
746
|
+
v0, v1, v2 = triangle
|
747
|
+
e1 = sub(v1, v0)
|
748
|
+
e2 = sub(v2, v0)
|
749
|
+
h = cross(direction, e2)
|
750
|
+
a = dot(e1, h)
|
751
|
+
if abs(a) < tol:
|
752
|
+
return None
|
753
|
+
f = 1.0 / a
|
754
|
+
s = sub(origin, v0)
|
755
|
+
u = f * dot(s, h)
|
756
|
+
if u < -tol or u > 1.0 + tol:
|
757
|
+
return None
|
758
|
+
q = cross(s, e1)
|
759
|
+
v = f * dot(direction, q)
|
760
|
+
if v < -tol or u + v > 1.0 + tol:
|
761
|
+
return None
|
762
|
+
t = f * dot(e2, q)
|
763
|
+
if t < -tol:
|
764
|
+
return None
|
765
|
+
w = 1.0 - u - v
|
766
|
+
if w < -tol:
|
767
|
+
return None
|
768
|
+
hit = point(origin[0] + direction[0] * t,
|
769
|
+
origin[1] + direction[1] * t,
|
770
|
+
origin[2] + direction[2] * t)
|
771
|
+
return t, hit, (u, v, w)
|
772
|
+
|
773
|
+
|
774
|
+
def surface_octree(s, rebuild=False):
|
775
|
+
meta = _ensure_surface_metadata_dict(s)
|
776
|
+
tree = meta.get('_octree')
|
777
|
+
if rebuild or meta.get('_octree_dirty') or tree is None:
|
778
|
+
verts = s[1]
|
779
|
+
faces = s[3]
|
780
|
+
if not faces:
|
781
|
+
meta['_octree'] = None
|
782
|
+
meta['_octree_dirty'] = False
|
783
|
+
return None
|
784
|
+
extent = 0.0
|
785
|
+
for axis in range(3):
|
786
|
+
vals = [v[axis] for v in verts]
|
787
|
+
extent = max(extent, max(vals) - min(vals))
|
788
|
+
mindim = max(extent / 32.0, epsilon)
|
789
|
+
tree = NTree(n=8, mindim=mindim)
|
790
|
+
for face in faces:
|
791
|
+
if len(face) != 3:
|
792
|
+
continue
|
793
|
+
tri = [verts[face[0]], verts[face[1]], verts[face[2]]]
|
794
|
+
tree.addElement(tri)
|
795
|
+
tree.updateTree()
|
796
|
+
meta['_octree'] = tree
|
797
|
+
meta['_octree_dirty'] = False
|
798
|
+
return meta['_octree']
|
799
|
+
|
800
|
+
|
801
|
+
def _surface_from_triangles(triangles):
|
802
|
+
if not triangles:
|
803
|
+
return None
|
804
|
+
verts = []
|
805
|
+
normals = []
|
806
|
+
faces = []
|
807
|
+
for tri in triangles:
|
808
|
+
v01 = sub(tri[1], tri[0])
|
809
|
+
v02 = sub(tri[2], tri[0])
|
810
|
+
if mag(cross(v01, v02)) < epsilon:
|
811
|
+
continue
|
812
|
+
n = _triangle_normal(tri)
|
813
|
+
indices = []
|
814
|
+
for pt in tri:
|
815
|
+
verts.append(point(pt))
|
816
|
+
normals.append([n[0], n[1], n[2], 0.0])
|
817
|
+
indices.append(len(verts) - 1)
|
818
|
+
faces.append(indices)
|
819
|
+
surface_obj = ['surface', verts, normals, faces, [], []]
|
820
|
+
_ensure_surface_metadata_dict(surface_obj)
|
821
|
+
invalidate_surface_octree(surface_obj)
|
822
|
+
return surface_obj
|
823
|
+
|
824
|
+
|
825
|
+
def _iter_triangles_from_surface(surf):
|
826
|
+
verts = surf[1]
|
827
|
+
faces = surf[3]
|
828
|
+
for face in faces:
|
829
|
+
if len(face) != 3:
|
830
|
+
continue
|
831
|
+
yield [verts[face[0]], verts[face[1]], verts[face[2]]]
|
832
|
+
|
833
|
+
|
834
|
+
def _iter_triangles_from_solid(sld):
|
835
|
+
for surf in sld[1]:
|
836
|
+
yield from _iter_triangles_from_surface(surf)
|
837
|
+
|
838
|
+
|
839
|
+
def _group_hits(hits, tol):
|
840
|
+
if not hits:
|
841
|
+
return []
|
842
|
+
hits.sort(key=lambda x: x[0])
|
843
|
+
groups = [[hits[0]]]
|
844
|
+
for hit in hits[1:]:
|
845
|
+
if abs(hit[0] - groups[-1][-1][0]) <= tol:
|
846
|
+
groups[-1].append(hit)
|
847
|
+
else:
|
848
|
+
groups.append([hit])
|
849
|
+
return groups
|
850
|
+
|
851
|
+
|
852
|
+
def _triangle_normal(tri):
|
853
|
+
_, n = _geom3d().tri2p0n(tri)
|
854
|
+
return n
|
855
|
+
def solid_contains_point(sld, p, tol=_DEFAULT_RAY_TOL):
|
856
|
+
if not _geom3d().issolid(sld, fast=False):
|
857
|
+
raise ValueError('invalid solid passed to solid_contains_point')
|
858
|
+
if not _geom3d().ispoint(p):
|
859
|
+
raise ValueError('invalid point passed to solid_contains_point')
|
860
|
+
|
861
|
+
surfaces = sld[1]
|
862
|
+
if not surfaces:
|
863
|
+
return False
|
864
|
+
|
865
|
+
box = _geom3d().solidbbox(sld)
|
866
|
+
if box:
|
867
|
+
expanded = [
|
868
|
+
point(box[0][0] - tol, box[0][1] - tol, box[0][2] - tol),
|
869
|
+
point(box[1][0] + tol, box[1][1] + tol, box[1][2] + tol),
|
870
|
+
]
|
871
|
+
if not _geom3d().isinsidebbox(expanded, p):
|
872
|
+
return False
|
873
|
+
extent = max(box[1][0] - box[0][0],
|
874
|
+
box[1][1] - box[0][1],
|
875
|
+
box[1][2] - box[0][2])
|
876
|
+
ray_length = max(extent * 1.5, 1.0)
|
877
|
+
else:
|
878
|
+
ray_length = 1.0
|
879
|
+
|
880
|
+
directions = [
|
881
|
+
vect(1, 0, 0, 0),
|
882
|
+
vect(-1, 0, 0, 0),
|
883
|
+
vect(0, 1, 0, 0),
|
884
|
+
vect(0, -1, 0, 0),
|
885
|
+
vect(0, 0, 1, 0),
|
886
|
+
vect(0, 0, -1, 0),
|
887
|
+
]
|
888
|
+
|
889
|
+
seen_inside = False
|
890
|
+
for direction in directions:
|
891
|
+
far_point = point(p[0] + direction[0] * ray_length,
|
892
|
+
p[1] + direction[1] * ray_length,
|
893
|
+
p[2] + direction[2] * ray_length)
|
894
|
+
query_box = _segment_bbox(p, far_point, tol * 5)
|
895
|
+
hits = []
|
896
|
+
for surf in surfaces:
|
897
|
+
tree = surface_octree(surf)
|
898
|
+
if tree is None:
|
899
|
+
candidates = list(_iter_triangles_from_surface(surf))
|
900
|
+
else:
|
901
|
+
elems = tree.getElements(query_box)
|
902
|
+
candidates = []
|
903
|
+
for elem in elems:
|
904
|
+
if isinstance(elem, list):
|
905
|
+
candidates.append(elem)
|
906
|
+
elif isinstance(elem, tuple):
|
907
|
+
candidates.append(elem[0])
|
908
|
+
else:
|
909
|
+
candidates.append(elem)
|
910
|
+
if not candidates:
|
911
|
+
candidates = list(_iter_triangles_from_surface(surf))
|
912
|
+
for tri in candidates:
|
913
|
+
result = _ray_triangle_intersection(p, direction, tri, tol=tol)
|
914
|
+
if result is None:
|
915
|
+
continue
|
916
|
+
t_hit, hit_point, _ = result
|
917
|
+
if t_hit < -tol or t_hit > ray_length + tol:
|
918
|
+
continue
|
919
|
+
if t_hit <= tol:
|
920
|
+
return True
|
921
|
+
normal = _triangle_normal(tri)
|
922
|
+
sign = -1 if dot(normal, direction) > 0 else 1
|
923
|
+
hits.append((t_hit, hit_point, sign))
|
924
|
+
groups = _group_hits(hits, tol)
|
925
|
+
parity = 0
|
926
|
+
for group in groups:
|
927
|
+
sign_sum = sum(hit[2] for hit in group)
|
928
|
+
if sign_sum == 0:
|
929
|
+
continue
|
930
|
+
parity ^= 1
|
931
|
+
if parity == 0:
|
932
|
+
return False
|
933
|
+
seen_inside = True
|
934
|
+
return seen_inside
|
935
|
+
|
936
|
+
|
937
|
+
def solids_intersect(a, b, tol=_DEFAULT_RAY_TOL):
|
938
|
+
if not _geom3d().issolid(a, fast=False) or not _geom3d().issolid(b, fast=False):
|
939
|
+
raise ValueError('invalid solid passed to solids_intersect')
|
940
|
+
if not a[1] or not b[1]:
|
941
|
+
return False
|
942
|
+
|
943
|
+
box_a = _geom3d().solidbbox(a)
|
944
|
+
box_b = _geom3d().solidbbox(b)
|
945
|
+
if box_a and box_b and not _bbox_overlap(box_a, box_b, tol):
|
946
|
+
return False
|
947
|
+
|
948
|
+
center_a = point((box_a[0][0] + box_a[1][0]) / 2.0,
|
949
|
+
(box_a[0][1] + box_a[1][1]) / 2.0,
|
950
|
+
(box_a[0][2] + box_a[1][2]) / 2.0)
|
951
|
+
center_b = point((box_b[0][0] + box_b[1][0]) / 2.0,
|
952
|
+
(box_b[0][1] + box_b[1][1]) / 2.0,
|
953
|
+
(box_b[0][2] + box_b[1][2]) / 2.0)
|
954
|
+
if solid_contains_point(a, center_b, tol=tol) or solid_contains_point(b, center_a, tol=tol):
|
955
|
+
return True
|
956
|
+
|
957
|
+
for tri_a in _iter_triangles_from_solid(a):
|
958
|
+
for tri_b in _iter_triangles_from_solid(b):
|
959
|
+
if _geom3d().triTriIntersect(tri_a, tri_b):
|
960
|
+
return True
|
961
|
+
return False
|
962
|
+
|
963
|
+
|
964
|
+
def solid_boolean(a, b, operation, tol=_DEFAULT_RAY_TOL, *, stitch=False):
|
965
|
+
if operation not in {'union', 'intersection', 'difference'}:
|
966
|
+
raise ValueError(f'unsupported solid boolean operation {operation!r}')
|
967
|
+
|
968
|
+
outside_a, inside_a, overlap_a = _boolean_fragments(a, b, tol)
|
969
|
+
outside_b, inside_b, overlap_b = _boolean_fragments(b, a, tol)
|
970
|
+
|
971
|
+
if operation == 'union':
|
972
|
+
result_tris = outside_a + outside_b
|
973
|
+
if not result_tris:
|
974
|
+
result_tris = inside_a if inside_a else inside_b
|
975
|
+
|
976
|
+
# Filter out triangles in the interior of the overlap region
|
977
|
+
# For union, if a triangle's center is inside both input solids,
|
978
|
+
# it's in the interior and should not be on the surface
|
979
|
+
filtered_tris = []
|
980
|
+
for tri in result_tris:
|
981
|
+
center = point((tri[0][0] + tri[1][0] + tri[2][0]) / 3.0,
|
982
|
+
(tri[0][1] + tri[1][1] + tri[2][1]) / 3.0,
|
983
|
+
(tri[0][2] + tri[1][2] + tri[2][2]) / 3.0)
|
984
|
+
# Use a tighter tolerance for containment check to avoid false positives
|
985
|
+
check_tol = tol * 100
|
986
|
+
in_a = solid_contains_point(a, center, tol=check_tol)
|
987
|
+
in_b = solid_contains_point(b, center, tol=check_tol)
|
988
|
+
# Keep triangle only if it's not clearly inside both solids
|
989
|
+
if not (in_a and in_b):
|
990
|
+
filtered_tris.append(tri)
|
991
|
+
result_tris = filtered_tris
|
992
|
+
|
993
|
+
elif operation == 'intersection':
|
994
|
+
result_tris = inside_a + inside_b
|
995
|
+
if not result_tris:
|
996
|
+
result_tris = overlap_a + overlap_b
|
997
|
+
else: # difference
|
998
|
+
reversed_inside_b = [[tri[0], tri[2], tri[1]] for tri in inside_b]
|
999
|
+
if not reversed_inside_b and overlap_b:
|
1000
|
+
reversed_inside_b = [[tri[0], tri[2], tri[1]] for tri in overlap_b]
|
1001
|
+
result_tris = outside_a + reversed_inside_b
|
1002
|
+
|
1003
|
+
if stitch:
|
1004
|
+
result_tris = stitch_open_edges(result_tris, tol)
|
1005
|
+
|
1006
|
+
surface_result = _surface_from_triangles(result_tris)
|
1007
|
+
if surface_result:
|
1008
|
+
return _geom3d().solid([surface_result], [], ['boolean', operation])
|
1009
|
+
return _geom3d().solid([], [], ['boolean', operation])
|
1010
|
+
|
1011
|
+
|
1012
|
+
|