-
Notifications
You must be signed in to change notification settings - Fork 17
/
mesh.py
2005 lines (1755 loc) · 63.9 KB
/
mesh.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This file is part of pymadcad, distributed under license LGPL v3
'''
This module defines triangular meshes and edges webs.
containers
----------
The classes defined here are 'points containers' it means that they are storing a list of points (as a member `point`), a list of groups (member `group`) and index points and groups for other purpose (faces and edges).
All of these follow this line:
- storages are using basic types, no object inter-referencing, making it easy to copy it
- storages (points, groups, faces, edges, ...) are used as shared ressources
python objects are ref-counted, allowing multiple Mesh instance to have the same point buffer. The user is responsible to ensure that there will be no conflict in making one mesh modifying the buffer in a way that makes it invalid for an other mesh referencing it.
To avoid conflicts, operations that are changing each point (or the most of them) reallocate a new list. Then operations like `mergeclose` or `stripgroups` won't affect other Meshes that shared the same buffers initially.
- as build from shared ressources, these classes can be build from existing parts at a nearly zero cost (few verifications, no computation)
- the user is allowed to hack into the internal data, ensure that the Mesh is still consistent after.
'''
from copy import copy, deepcopy
from random import random
import numpy as np
import numpy.lib.recfunctions as rfn
from array import array
from collections import OrderedDict, Counter
from numbers import Integral, Real
import math
from .mathutils import *
from . import displays
from . import text
from . import hashing
from .asso import Asso
from . import settings
__all__ = [
'Mesh', 'Web', 'Wire', 'MeshError', 'web', 'wire',
'edgekey', 'lineedges', 'striplist', 'suites', 'line_simplification', 'mesh_distance',
'connpp', 'connpp', 'connpe', 'connef',
]
class MeshError(Exception): pass
class Container:
''' common methods for points container (typically Mesh or Wire) '''
def __init__(self, points=(), groups=(), options=None):
self.points = points
self.groups = groups
self.options = options or {}
# --- basic transformations of points ---
def transform(self, trans):
''' apply the transform to the points of the mesh, returning the new transformed mesh'''
trans = transformer(trans)
transformed = copy(self)
transformed.points = typedlist((trans(p) for p in self.points), dtype=vec3)
return transformed
def mergeclose(self, limit=None, start=0):
''' merge points below the specified distance, or below the precision
return a dictionnary of points remapping {src index: dst index}
'''
if limit is None: limit = self.precision()
'''
# O(n**2 /2) implementation
merges = {}
for j in reversed(range(start, len(self.points))):
for i in range(start, j):
if distance(self.points[i], self.points[j]) <= limit:
merges[j] = i
break
self.mergepoints(merges)
return merges
'''
# O(n) implementation thanks to hashing
merges = {}
points = hashing.PointSet(limit)
for i in range(start, len(self.points)):
used = points.add(self.points[i])
if used != i: merges[i] = used
self.mergepoints(merges)
self.points = points.points
self.points.shrink()
return merges
def mergegroups(self, defs=None, merges=None):
''' merge the groups according to the merge dictionnary
the new groups associated can be specified with defs
the former unused groups are not removed from the buffer and the new ones are appended
if merges is not provided, all groups are merged, and defs is the data associated to the only group after the merge
'''
if merges is None:
self.groups = [defs]
self.tracks = typedlist.full(0, len(self.tracks), 'I')
else:
l = len(self.groups)
self.groups.extend(defs)
for i,t in enumerate(self.tracks):
if t in merges:
self.tracks[i] = merges[t]+l
def stripgroups(self):
''' remove groups that are used by no faces, return the reindex list '''
used = [False] * len(self.groups)
for track in self.tracks:
used[track] = True
self.groups = copy(self.groups)
self.tracks = copy(self.tracks)
reindex = striplist(self.groups, used)
for i,track in enumerate(self.tracks):
self.tracks[i] = reindex[track]
return reindex
def finish(self):
''' finish and clean the mesh
note that this operation can cost as much as other transformation operation
job done
- mergeclose
- strippoints
- stripgroups
- check
'''
self.mergeclose()
#self.strippoints() # not needed since the new mergeclose implementation
self.stripgroups()
self.check()
return self
# --- verification methods ---
def isvalid(self):
''' return true if the internal data is consistent (all indices referes to actual points and groups) '''
try: self.check()
except MeshError: return False
else: return True
# --- selection methods ---
def maxnum(self):
''' maximum numeric value of the mesh, use this to get an hint on its size or to evaluate the numeric precision '''
m = 0
for p in self.points:
for v in p:
a = abs(v)
if a > m: m = a
return m
def precision(self, propag=3):
''' numeric coordinate precision of operations on this mesh, allowed by the floating point precision '''
return self.maxnum() * NUMPREC * (2**propag)
def usepointat(self, point, neigh=NUMPREC):
''' Return the index of the first point in the mesh at the location. If none is found, insert it and return the index '''
i = self.pointat(point, neigh=neigh)
if i is None:
i = len(self.points)
self.points.append(point)
return i
def pointat(self, point, neigh=NUMPREC):
''' return the index of the first point at the given location, or None '''
for i,p in enumerate(self.points):
if distance(p,point) <= neigh: return i
def pointnear(self, point):
''' return the nearest point the the given location '''
return min( range(len(self.points)),
lambda i: distance(self.points[i], point))
def box(self):
''' return the extreme coordinates of the mesh (vec3, vec3) '''
if not self.points: return Box()
max = deepcopy(self.points[0])
min = deepcopy(self.points[0])
for pt in self.points:
for i in range(3):
if pt[i] < min[i]: min[i] = pt[i]
elif pt[i] > max[i]: max[i] = pt[i]
return Box(min, max)
def option(self, update=None, **kwargs):
''' update the internal options with the given dictionnary and the keywords arguments.
This is only a shortcut to set options in a method style.
'''
if update: self.options.update(update)
if kwargs: self.options.update(kwargs)
return self
class Mesh(Container):
''' set of triangles, used to represent volumes or surfaces.
As volumes are represented by their exterior surface, there is no difference between representation of volumes and faces, juste the way we interpret it.
Attributes:
points: typedlist of vec3 for points
faces: typedlist of uvec3 for faces, the triplet is (a,b,c) such that cross(b-a, c-a) is the normal oriented to the exterior.
tracks: typedlist of integers giving the group each face belong to
groups: custom information for each group
options: custom informations for the entire mesh
'''
#__slots__ = 'points', 'faces', 'tracks', 'groups', 'options'
# --- standard point container methods ---
def __init__(self, points=None, faces=None, tracks=None, groups=None, options=None):
self.points = ensure_typedlist(points, vec3)
self.faces = ensure_typedlist(faces, uvec3)
self.tracks = ensure_typedlist(tracks if tracks is not None else typedlist.full(0, len(self.faces), 'I'), 'I')
self.groups = groups or [None] * (max(self.tracks, default=-1)+1)
self.options = options or {}
def __add__(self, other):
''' append the faces and points of the other mesh '''
if isinstance(other, Mesh):
r = Mesh(
self.points if self.points is other.points else self.points[:],
self.faces[:],
self.tracks[:],
self.groups if self.groups is other.groups else self.groups[:],
)
r.__iadd__(other)
return r
else:
return NotImplemented
def __iadd__(self, other):
''' append the faces and points of the other mesh '''
if isinstance(other, Mesh):
if self.points is other.points:
self.faces.extend(other.faces)
else:
lp = len(self.points)
self.points.extend(other.points)
self.faces.extend(f+lp for f in other.faces)
if self.groups is other.groups:
self.tracks.extend(other.tracks)
else:
lt = len(self.groups)
self.groups.extend(other.groups)
self.tracks.extend(track+lt for track in other.tracks)
return self
else:
return NotImplemented
# --- mesh optimization ---
def mergepoints(self, merges):
''' merge points with the merge dictionnary {src index: dst index}
merged points are not removed from the buffer.
'''
i = 0
while i < len(self.faces):
f = self.faces[i]
self.faces[i] = f = (
merges.get(f[0], f[0]),
merges.get(f[1], f[1]),
merges.get(f[2], f[2]),
)
if f[0] == f[1] or f[1] == f[2] or f[2] == f[0]:
self.faces.pop(i)
self.tracks.pop(i)
else:
i += 1
self.faces.shrink()
def strippoints(self, used=None):
''' remove points that are used by no faces, return the reindex list.
if used is provided, these points will be removed without usage verification
return a table of the reindex made
'''
if used is None:
used = [False] * len(self.points)
for face in self.faces:
for p in face:
used[p] = True
self.points = deepcopy(self.points)
self.faces = deepcopy(self.faces)
reindex = striplist(self.points, used)
self.points.shrink()
for i,f in enumerate(self.faces):
self.faces[i] = uvec3(reindex[f[0]], reindex[f[1]], reindex[f[2]])
return reindex
def flip(self):
''' flip all faces, getting the normals opposite '''
return Mesh(self.points,
typedlist((uvec3(f[0],f[2],f[1]) for f in self.faces), dtype=uvec3),
self.tracks,
self.groups)
def issurface(self):
''' return True if the mesh is a well defined surface (an edge has 2 connected triangles at maximum, with coherent normals)
such meshes are usually called 'manifold'
'''
reached = set()
for face in self.faces:
for e in ((face[0], face[1]), (face[1], face[2]), (face[2],face[0])):
if e in reached: return False
else: reached.add(e)
return True
def isenvelope(self):
''' return True if the surfaces are a closed envelope (the outline is empty)
'''
return len(self.outlines_oriented()) == 0
def check(self):
''' raise if the internal data is inconsistent '''
if not (isinstance(self.points, typedlist) and self.points.dtype == vec3): raise MeshError("points must be a typedlist(dtype=vec3)")
if not (isinstance(self.faces, typedlist) and self.faces.dtype == uvec3): raise MeshError("faces must be a typedlist(dtype=uvec3)")
if not (isinstance(self.tracks, typedlist) and self.tracks.dtype == 'I'): raise MeshError("tracks must be a typedlist(dtype='I')")
l = len(self.points)
for face in self.faces:
for p in face:
if p >= l: raise MeshError("some point indices are greater than the number of points", face, l)
if face[0] == face[1] or face[1] == face[2] or face[2] == face[0]: raise MeshError("some faces use the same point multiple times", face)
if len(self.faces) != len(self.tracks): raise MeshError("tracks list doesn't match faces list length")
if max(self.tracks, default=-1) >= len(self.groups): raise MeshError("some face group indices are greater than the number of groups", max(self.tracks, default=-1), len(self.groups))
# --- selection methods ---
def groupnear(self, point):
''' return the id of the group for the nearest surface to the given point '''
track = None
best = math.inf
for i,face in enumerate(self.faces):
n = self.facenormal(i)
dist = abs(dot(point - self.points[face[0]], n)) # TODO intergrer les limites du triangle
if dist < best:
track = self.tracks[i]
return track
# --- extraction methods ---
def facenormal(self, f):
''' normal for a face '''
if isinstance(f, Integral):
f = self.faces[f]
p0 = self.points[f[0]]
e1 = self.points[f[1]] - p0
e2 = self.points[f[2]] - p0
return normalize(cross(e1, e2))
def facenormals(self):
''' list normals for each face '''
return typedlist(map(self.facenormal, self.faces), vec3)
def edgenormals(self):
''' dict of normals for each UNORIENTED edge '''
normals = {}
for face in self.faces:
normal = self.facenormal(face)
for edge in ((face[0], face[1]), (face[1], face[2]), (face[2],face[0])):
e = edgekey(*edge)
normals[e] = normals.get(e,0) + normal
for e,normal in normals.items():
normals[e] = normalize(normal)
return normals
def vertexnormals(self):
''' list of normals for each point '''
# collect the mesh border as edges and as points
outline = self.outlines_oriented()
border = set()
for a,b in outline:
border.add(a)
border.add(b)
# sum contributions to normals
l = len(self.points)
normals = typedlist.full(vec3(0), l)
for face in self.faces:
normal = self.facenormal(face)
if not isfinite(normal): continue
for i in range(3):
o = self.points[face[i]]
# point on the surface
if face[i] not in border:
# triangle normals are weighted by their angle at the point
contrib = anglebt(self.points[face[i-2]]-o, self.points[face[i-1]]-o)
normals[face[i]] += contrib * normal
# point on the outline
elif (face[i], face[i-1]) in outline:
# only the triangle creating the edge does determine its normal
normals[face[i]] += normal
normals[face[i-1]] += normal
for i in range(l):
normals[i] = normalize(normals[i])
assert len(normals) == len(self.points)
return normals
def tangents(self):
''' tangents to outline points '''
# outline with associated face normals
edges = {}
for face in self.faces:
for e in ((face[0], face[1]), (face[1], face[2]), (face[2],face[0])):
if e in edges: del edges[e]
else: edges[(e[1], e[0])] = self.facenormal(face)
# cross neighbooring normals
tangents = {}
for loop in suites(edges, cut=False):
assert loop[0] == loop[-1], "an outline is not a loop"
loop.pop()
for i in range(len(loop)):
tangents[loop[i-1]] = normalize(cross( edges[(loop[i-2],loop[i-1])],
edges[(loop[i-1],loop[i])] ))
return tangents
def facepoints(self, f):
''' shorthand to get the points of a face (index is an int or a triplet) '''
if isinstance(f, Integral):
f = self.faces[f]
return self.points[f[0]], self.points[f[1]], self.points[f[2]]
def edges(self):
''' set of UNORIENTED edges present in the mesh '''
edges = set()
for face in self.faces:
edges.add(edgekey(face[0], face[1]))
edges.add(edgekey(face[1], face[2]))
edges.add(edgekey(face[2], face[0]))
return edges
def edges_oriented(self):
''' iterator of ORIENTED edges, directly retreived of each face '''
for face in self.faces:
yield face[0], face[1]
yield face[1], face[2]
yield face[2], face[0]
def group(self, groups):
''' return a new mesh linked with this one, containing only the faces belonging to the given groups '''
if isinstance(groups, set): pass
elif hasattr(groups, '__iter__'): groups = set(groups)
else: groups = (groups,)
faces = typedlist(dtype=uvec3)
tracks = typedlist(dtype='I')
for f,t in zip(self.faces, self.tracks):
if t in groups:
faces.append(f)
tracks.append(t)
return Mesh(self.points, faces, tracks, self.groups)
def outlines_oriented(self):
''' return a set of the ORIENTED edges delimiting the surfaces of the mesh '''
edges = set()
for face in self.faces:
for e in ((face[0], face[1]), (face[1], face[2]), (face[2],face[0])):
if e in edges: edges.remove(e)
else: edges.add((e[1], e[0]))
return edges
def outlines_unoriented(self):
''' return a set of the UNORIENTED edges delimiting the surfaces of the mesh
this method is robust to face orientation aberations
'''
edges = set()
for face in self.faces:
for edge in ((face[0],face[1]),(face[1],face[2]),(face[2],face[0])):
e = edgekey(*edge)
if e in edges: edges.remove(e)
else: edges.add(e)
return edges
def outlines(self):
''' return a Web of ORIENTED edges '''
return Web(self.points, self.outlines_oriented())
def groupoutlines(self):
''' return a dict of ORIENTED edges indexing groups.
On a frontier between multiple groups, there is as many edges as groups, each associated to a group.
'''
edges = typedlist(dtype=uvec2) # outline
tracks = typedlist(dtype='I') # groups for edges
tmp = {} # faces adjacent to edges
for i,face in enumerate(self.faces):
for e in ((face[1],face[0]),(face[2],face[1]),(face[0],face[2])):
track = self.tracks[i]
if e in tmp:
if tmp[e] != track:
edges.append(e)
tracks.append(track)
del tmp[e]
else:
tmp[(e[1],e[0])] = track
edges.extend(tmp.keys())
tracks.extend(tmp.values())
return Web(self.points, edges, tracks, self.groups)
def frontiers(self, *args):
''' return a Web of UNORIENTED edges that split the given groups appart.
if groups is None, then return the frontiers between any groups
'''
if len(args) == 1 and hasattr(args[0], '__iter__'):
args = args[0]
groups = set(args)
edges = typedlist(dtype=uvec2)
tracks = typedlist(dtype='I')
couples = OrderedDict()
belong = {}
for i,face in enumerate(self.faces):
if groups and self.tracks[i] not in groups:
continue
for edge in ((face[0],face[1]),(face[1],face[2]),(face[2],face[0])):
e = edgekey(*edge)
if e in belong:
if belong[e] != self.tracks[i]:
g = edgekey(belong[e],self.tracks[i])
edges.append(e)
tracks.append(couples.setdefault(g, len(couples)))
del belong[e]
else:
belong[e] = self.tracks[i]
return Web(self.points, edges, tracks, list(couples))
def surface(self):
''' total surface of triangles '''
s = 0
for f in self.faces:
a,b,c = self.facepoints(f)
s += length(cross(a-b, a,c))/2
return s
def barycenter(self):
''' surface barycenter of the mesh '''
if not self.faces: return vec3(0)
acc = vec3(0)
tot = 0
for f in self.faces:
a,b,c = self.facepoints(f)
weight = length(cross(b-a, c-a))
tot += weight
acc += weight*(a+b+c)
return acc / (3*tot)
def splitgroups(self, edges=None):
''' split the mesh groups into connectivity separated groups.
the points shared by multiple groups will be duplicated
if edges is provided, only the given edges at group frontier will be splitted
return a list of tracks for points
'''
if edges is None: edges = self.frontiers().edges
# mark points on the frontier
frontier = [False]*len(self.points)
for a,b in edges:
frontier[a] = True
frontier[b] = True
# duplicate points and reindex faces
points = copy(self.points)
idents = typedlist.full(0, len(self.points), 'I') # track id corresponding to each point
duplicated = {} # new point index for couples (frontierpoint, group)
def repl(pt, track):
if frontier[pt]:
key = (pt,track)
if key in duplicated: i = duplicated[key]
else:
i = duplicated[key] = len(points)
points.append(points[pt])
idents.append(track)
return i
else:
idents[pt] = track
return pt
faces = typedlist((uvec3(repl(a,t), repl(b,t), repl(c,t))
for (a,b,c),t in zip(self.faces, self.tracks)), dtype=uvec3)
self.points = points
self.faces = faces
return idents
def split(self, edges):
''' split the mesh around the given edges.
The points in common with two or more designated edges will be dupliated once or more, and the face indices will be reassigned so that faces each side of the given edges will own a duplicate of that point each.
'''
# get connectivity and set of edges to manage
conn = connef(self.faces)
edges = set(edgekey(*edge) for edge in edges)
# collect the points to multiply
ranks = Counter(p for e in edges for p in e)
separations = set(p for p, count in ranks.items() if count > 1)
newfaces = deepcopy(self.faces)
# for each edge, reassign neighboring faces to the proper points
for edge in edges:
for pivot in edge:
if edge in conn and pivot in newfaces[conn[edge]]:
dupli = len(self.points)
self.points.append(self.points[pivot])
# change the point index in every neighbooring face
front = edge
while front in conn:
fi = conn[front]
f = arrangeface(self.faces[fi], pivot)
fm = arrangeface(newfaces[fi], pivot)
assert f[0] == pivot
if fm[0] != pivot:
break
newfaces[fi] = uvec3(dupli, fm[1], fm[2])
if pivot == front[0]: front = (pivot, f[2])
elif pivot == front[1]: front = (f[1], pivot)
else:
raise AssertionError('error in connectivity')
if edgekey(*front) in edges:
break
self.faces = newfaces
# NOTE: splitfaces(self) ?
def islands(self, conn=None) -> '[Mesh]':
''' return the unconnected parts of the mesh as several meshes '''
if not conn:
conn = connef(self.faces)
# propagation
islands = []
reached = [False] * len(self.faces) # faces reached
stack = []
start = 0
while True:
# search start point
for start in range(start,len(reached)):
if not reached[start]:
stack.append(start)
break
# end when everything reached
if not stack: break
# propagate
island = Mesh(points=self.points, groups=self.groups)
while stack:
i = stack.pop()
if reached[i]: continue # make sure this face has not been stacked twice
reached[i] = True
island.faces.append(self.faces[i])
island.tracks.append(self.tracks[i])
f = self.faces[i]
for i in range(3):
e = f[i],f[i-1]
if e in conn and not reached[conn[e]]:
stack.append(conn[e])
islands.append(island)
return islands
def propagate(self, atface, atisland=None, find=None, conn=None):
''' return the unconnected parts of the mesh as several meshes '''
if not conn:
conn = connef(self.faces)
reached = [False] * len(self.faces) # faces reached
stack = []
# procedure for finding the new islands to propagate on
if not find:
start = [0]
def find(stack, reached):
for i in range(start[0],len(reached)):
if not reached[i]:
stack.append(i)
break
start[0] = i
# propagation
while True:
# search start point
find(stack, reached)
# end when everything reached
if not stack: break
# propagate
while stack:
i = stack.pop()
if reached[i]: continue # make sure this face has not been stacked twice
reached[i] = True
atface(i, reached)
f = self.faces[i]
for i in range(3):
e = f[i],f[i-1]
if e in conn and not reached[conn[e]]:
stack.append(conn[e])
if atisland:
atisland(reached)
def islands(self, conn=None) -> '[Mesh]':
''' return the unconnected parts of the mesh as several meshes '''
islands = []
faces = typedlist(dtype=uvec3)
tracks = typedlist(dtype='I')
def atface(i, reached):
faces.append(self.faces[i])
tracks.append(self.tracks[i])
def atisland(reached):
islands.append(Mesh(self.points, deepcopy(faces), deepcopy(tracks), self.groups))
faces.clear()
tracks.clear()
self.propagate(atface, atisland, conn=conn)
return islands
def orient(self, dir=None, conn=None) -> 'Mesh':
''' flip the necessary faces to make the normals consistent, ensuring the continuity of the out side.
Argument `dir` tries to make the result deterministic:
* if given, the outermost point in this direction will be considered pointing outside
* if not given, the farthest point to the barycenter will be considered pointing outside
note that if the mesh contains multiple islands, that direction must make sense for each single island
'''
if dir:
metric = lambda p, n: (dot(p, dir), abs(dot(n, dir)))
orient = lambda p, n: dot(n, dir)
else:
center = self.barycenter()
metric = lambda p, n: (length2(p-center), abs(dot(n, p-center)))
orient = lambda p, n: dot(n, p-center)
if not conn:
conn = Asso( (edgekey(*e),i)
for i,f in enumerate(self.faces)
for e in ((f[0],f[1]), (f[1],f[2]), (f[2],f[0]))
)
faces = self.faces[:]
normals = self.facenormals()
reached = [False] * len(self.faces) # faces reached
stack = []
# propagation
while True:
# search start point
best = (-inf,0)
candidate = None
for i,f in enumerate(faces):
if not reached[i]:
for p in f:
score = metric(self.points[p], normals[i])
if score > best:
best, candidate = score, i
if orient(self.points[p], normals[i]) < 0:
faces[i] = (f[2],f[1],f[0])
# end when everything reached
if candidate is None:
break
else:
stack.append(candidate)
# process neighbooring
while stack:
i = stack.pop()
if reached[i]: continue # make sure this face has not been stacked twice
reached[i] = True
f = faces[i]
for i in range(3):
e = f[i], f[i-1]
for n in conn[edgekey(*e)]:
if reached[n]: continue
nf = faces[n]
# check for orientation continuity
if arrangeface(nf,f[i-1])[1] == f[i]:
faces[n] = (nf[2],nf[1],nf[0])
# propagate
stack.append(n)
return Mesh(self.points, faces, self.tracks, self.groups)
# NOTE not sure this method is useful
def replace(self, mesh, groups=None) -> 'Mesh':
''' replace the given groups by the given mesh.
If no groups are specified, it will take the matching groups (with same index) in the current mesh
'''
if not groups:
groups = set(mesh.tracks)
new = copy(self)
new.faces = typedlist((f for f,t in zip(self.faces, self.tracks) if t not in groups), dtype=uvec3)
new.tracks = typedlist((t for t in self.tracks if t not in groups), dtype='I')
new += mesh
return new
# --- renderable interfaces ---
def display_triangles(self, scene):
from . import rendering, text
grp = []
if self.options.get('debug_points', False):
for i,p in enumerate(self.points):
grp.append(text.TextDisplay(scene,
p,
' '+str(i),
size=8,
color=(0.2, 0.8, 1),
align=('left', 'center'),
layer=-4e-4,
))
if self.options.get('debug_faces', None) == 'indices':
for i,f in enumerate(self.faces):
p = (self.points[f[0]] + self.points[f[1]] + self.points[f[2]]) /3
grp.append(text.TextDisplay(scene, p, str(i), 9, (1, 0.2, 0), align=('center', 'center'), layer=-4e-4))
if self.options.get('debug_faces', None) == 'tracks':
for i,f in enumerate(self.faces):
p = (self.points[f[0]] + self.points[f[1]] + self.points[f[2]]) /3
grp.append(text.TextDisplay(scene, p, str(self.tracks[i]), 9, (1, 0.2, 0), align=('center', 'center'), layer=-4e-4))
fn = np.array([tuple(self.facenormal(f)) for f in self.faces])
points = np.array([tuple(p) for p in self.points], dtype=np.float32)
edges = []
for i in range(0, 3*len(self.faces), 3):
edges.append((i, i+1))
edges.append((i+1, i+2))
edges.append((i, i+2))
idents = []
for i in self.tracks:
idents.append(i)
idents.append(i)
idents.append(i)
m = copy(self)
idents = m.splitgroups()
edges = m.groupoutlines().edges
normals = m.vertexnormals()
if not m.points or not m.faces:
return displays.Display()
grp.append(displays.SolidDisplay(scene,
glmarray(m.points),
glmarray(normals),
m.faces,
edges,
idents,
color = self.options.get('color'),
))
return rendering.Group(scene, grp)
def display_groups(self, scene):
m = copy(self)
idents = m.splitgroups()
edges = m.groupoutlines().edges
normals = m.vertexnormals()
return displays.SolidDisplay(scene,
m.points,
normals,
m.faces,
edges,
idents,
color = self.options.get('color'),
)
def display(self, scene):
m = copy(self)
m.split(m.frontiers().edges)
edges = m.outlines().edges
# select edges above a threshold
tosplit = []
thresh = cos(settings.display['sharp_angle'])
conn = connef(m.faces)
for edge, f1 in conn.items():
if edge[1] > edge[0]: continue
f2 = conn.get((edge[1],edge[0]))
if f2 is None: continue
if m.tracks[f1] != m.tracks[f2] or dot(m.facenormal(f1), m.facenormal(f2)) <= thresh:
tosplit.append(edge)
m.split(tosplit)
# get the group each point belong to
idents = [0] * len(m.points)
for face, track in zip(m.faces, m.tracks):
for p in face:
idents[p] = track
normals = m.vertexnormals()
if not m.points or not m.faces:
return displays.Display()
return displays.SolidDisplay(scene,
typedlist_to_numpy(m.points, 'f4'),
typedlist_to_numpy(normals, 'f4'),
typedlist_to_numpy(m.faces, 'u4'),
typedlist_to_numpy(edges, 'u4'),
typedlist_to_numpy(idents, 'u4'),
color = self.options.get('color'),
)
def __repr__(self):
return '<Mesh with {} points at 0x{:x}, {} faces>'.format(len(self.points), id(self.points), len(self.faces))
def __str__(self):
return 'Mesh(\n points={},\n faces={},\n tracks={},\n groups={},\n options={})'.format(
reprarray(self.points, 'points'),
reprarray(self.faces, 'faces'),
reprarray(self.tracks, 'tracks'),
reprarray(self.groups, 'groups'),
repr(self.options))
def reprarray(array, name):
content = ', '.join((repr(e) for e in array))
return '['+content+']'
def striplist(list, used):
''' remove all elements of list that match a False in used, return a reindexation list '''
reindex = [-1] * len(list)
j = 0
for i,u in enumerate(used):
if u:
list[j] = list[i]
reindex[i] = j
j += 1
list[j:] = []
return reindex
def npcast(storage, dtype=None):
if isinstance(storage, typedlist):
raw = np.array(storage, copy=False)
if raw.dtype.fields:
return np.stack([ raw[f] for f in raw.dtype.fields.keys() ], axis=-1)
else:
return raw
elif hasattr(storage[0], '__len__'):
buff = np.empty((len(storage), len(storage[0])), dtype=dtype)
for i,e in enumerate(storage):
buff[i][:] = e
return buff
else:
return np.array(storage, dtype=dtype)
def numpy_to_typedlist(array: 'ndarray', dtype) -> 'typedlist':
''' convert a numpy.ndarray into a typedlist with the given dtype, if the conversion is possible term to term '''
ndtype = np.array(typedlist(dtype)).dtype
if ndtype.fields:
return typedlist(rfn.unstructured_to_structured(array).astype(ndtype, copy=False), dtype)
else:
return typedlist(array.astype(ndtype, copy=False), dtype)
def typedlist_to_numpy(array: 'typedlist', dtype) -> 'ndarray':
''' convert a typedlist to a numpy.ndarray with the given dtype, if the conversion is possible term to term '''
tmp = np.array(array, copy=False)
if tmp.dtype.fields:
return rfn.structured_to_unstructured(tmp, dtype)
else:
return tmp.astype(dtype)
def ensure_typedlist(obj, dtype):
''' return a typedlist with the given dtype, create it from whatever is in obj if needed '''
if isinstance(obj, typedlist) and obj.dtype == dtype:
return obj
else:
return typedlist(obj, dtype)
class Web(Container):
''' set of bipoint edges, used to represent wires
this definition is very close to the definition of Mesh, but with edges instead of triangles
Attributes:
points: typedlist of vec3 for points
edges: typedlist of couples for edges, the couple is oriented (meanings of this depends on the usage)
tracks: typedlist of integers giving the group each line belong to
groups: custom information for each group
options: custom informations for the entire web
'''
# --- standard point container methods ---
def __init__(self, points=None, edges=None, tracks=None, groups=None, options=None):
self.points = ensure_typedlist(points, vec3)
self.edges = ensure_typedlist(edges, uvec2)
self.tracks = ensure_typedlist(tracks or typedlist.full(0, len(self.edges), 'I'), 'I')
self.groups = groups or [None] * (max(self.tracks, default=-1)+1)
self.options = options or {}
def __add__(self, other):
''' append the faces and points of the other mesh '''