@@ -16,8 +16,8 @@ def __init__(self, point_on_plane, unit_normal):
1616
1717 unit_normal = vx .normalize (unit_normal )
1818
19- self ._r0 = point_on_plane
20- self ._n = unit_normal
19+ self ._r0 = np . asarray ( point_on_plane )
20+ self ._n = np . asarray ( unit_normal )
2121
2222 def __repr__ (self ):
2323 return "<Plane of {} through {}>" .format (self .normal , self .reference_point )
@@ -127,7 +127,7 @@ def normal(self):
127127 Return the plane's normal vector.
128128
129129 '''
130- return np . array ( self ._n )
130+ return self ._n
131131
132132 def flipped (self ):
133133 '''
@@ -210,25 +210,22 @@ def polyline_xsection(self, polyline):
210210 return intersection_points
211211
212212 def line_xsection (self , pt , ray ):
213- pt = np .asarray (pt ).ravel ()
214- ray = np .asarray (ray ).ravel ()
215- assert len (pt ) == 3
216- assert len (ray ) == 3
213+ return self ._line_xsection (np .asarray (pt ).ravel (), np .asarray (ray ).ravel ())
214+
215+ def _line_xsection (self , pt , ray ):
217216 denom = np .dot (ray , self .normal )
218217 if denom == 0 :
219218 return None # parallel, either coplanar or non-intersecting
220219 p = np .dot (self .reference_point - pt , self .normal ) / denom
221220 return p * ray + pt
222221
223222 def line_segment_xsection (self , a , b ):
224- a = np .asarray (a ).ravel ()
225- b = np .asarray (b ).ravel ()
226- assert len (a ) == 3
227- assert len (b ) == 3
223+ return self ._line_segment_xsection (np .asarray (a ).ravel (), np .asarray (b ).ravel ())
228224
229- pt = self .line_xsection (a , b - a )
225+ def _line_segment_xsection (self , a , b ):
226+ pt = self ._line_xsection (a , b - a )
230227 if pt is not None :
231- if np .any (pt < np . min (( a , b ), axis = 0 )) or np .any (pt > np . max (( a , b ), axis = 0 )):
228+ if any ( np .logical_and (pt > a , pt > b )) or any ( np .logical_and (pt < a , pt < b )):
232229 return None
233230 return pt
234231
@@ -245,6 +242,11 @@ def mesh_xsection(self, m, neighborhood=None):
245242 return Polyline (None )
246243 return Polyline (np .vstack ([x .v for x in components ]), closed = True )
247244
245+ def mesh_intersecting_faces (self , m ):
246+ sgn_dists = self .signed_distance (m .v )
247+ which_fs = np .abs (np .sign (sgn_dists )[m .f ].sum (axis = 1 )) != 3
248+ return m .f [which_fs ]
249+
248250 def mesh_xsections (self , m , neighborhood = None ):
249251 '''
250252 Takes a cross section of planar point cloud with a Mesh object.
@@ -262,103 +264,131 @@ def mesh_xsections(self, m, neighborhood=None):
262264
263265 Returns a list of Polylines.
264266 '''
265- import operator
266267 from blmath .geometry import Polyline
267268
268269 # 1: Select those faces that intersect the plane, fs
269- sgn_dists = self .signed_distance (m .v )
270- which_fs = np .abs (np .sign (sgn_dists )[m .f ].sum (axis = 1 )) != 3
271- fs = m .f [which_fs ]
272-
270+ fs = self .mesh_intersecting_faces (m )
273271 if len (fs ) == 0 :
274272 return [] # Nothing intersects
273+ # and edges of those faces
274+ es = np .vstack ((fs [:, (0 , 1 )], fs [:, (1 , 2 )], fs [:, (2 , 0 )]))
275275
276276 # 2: Find the edges where each of those faces actually cross the plane
277- def edge_from_face (f ):
278- face_verts = [
279- [m .v [f [0 ]], m .v [f [1 ]]],
280- [m .v [f [1 ]], m .v [f [2 ]]],
281- [m .v [f [2 ]], m .v [f [0 ]]],
282- ]
283- e = [self .line_segment_xsection (a , b ) for a , b in face_verts ]
284- e = [x for x in e if x is not None ]
285- return e
286- edges = np .vstack ([np .hstack (edge_from_face (f )) for f in fs ])
287-
288- # 3: Find the set of unique vertices in `edges`
289- v1s , v2s = np .hsplit (edges , 2 )
290- verts = edges .reshape ((- 1 , 3 ))
291- verts = np .vstack (sorted (verts , key = operator .itemgetter (0 , 1 , 2 )))
292- eps = 1e-15 # the floating point calculation of the intersection locations is not _quite_ exact
293- verts = verts [list (np .sqrt (np .sum (np .diff (verts , axis = 0 ) ** 2 , axis = 1 )) > eps ) + [True ]]
294- # the True at the end there is because np.diff returns pairwise differences; one less element than the original array
277+ class EdgeMap (object ):
278+ # A quick two level dictionary where the two keys are interchangeable (i.e. a symmetric graph)
279+ def __init__ (self ):
280+ self .d = {} # store indicies into self.values here, to make it easier to get inds or values
281+ self .values = []
282+ def _order (self , u , v ):
283+ if u < v :
284+ return u , v
285+ else :
286+ return v , u
287+ def add (self , u , v , val ):
288+ low , high = self ._order (u , v )
289+ if low not in self .d :
290+ self .d [low ] = {}
291+ self .values .append (val )
292+ self .d [low ][high ] = len (self .values ) - 1
293+ def contains (self , u , v ):
294+ low , high = self ._order (u , v )
295+ if low in self .d and high in self .d [low ]:
296+ return True
297+ return False
298+ def index (self , u , v ):
299+ low , high = self ._order (u , v )
300+ try :
301+ return self .d [low ][high ]
302+ except KeyError :
303+ return None
304+ def get (self , u , v ):
305+ ii = self .index (u , v )
306+ if ii is not None :
307+ return self .values [ii ]
308+ else :
309+ return None
310+
311+ intersection_map = EdgeMap ()
312+ for e in es :
313+ if not intersection_map .contains (e [0 ], e [1 ]):
314+ val = self ._line_segment_xsection (m .v [e [0 ]], m .v [e [1 ]])
315+ if val is not None :
316+ intersection_map .add (e [0 ], e [1 ], val )
317+ verts = np .array (intersection_map .values )
295318
296319 class Graph (object ):
297- # A little utility class to build a symmetric graph
320+ # A little utility class to build a symmetric graph and calculate Euler Paths
298321 def __init__ (self , size ):
299322 self .size = size
300323 self .d = {}
301324 def __len__ (self ):
302325 return len (self .d )
303- def add_edge (self , ii , jj ):
304- assert ii >= 0 and ii < self .size
305- assert jj >= 0 and jj < self .size
306- if ii not in self .d :
307- self .d [ii ] = set ()
308- if jj not in self .d :
309- self .d [jj ] = set ()
310- self .d [ii ].add (jj )
311- self .d [jj ].add (ii )
326+ def add_edges (self , edges ):
327+ for u , v in edges :
328+ self .add_edge (u , v )
329+ def add_edge (self , u , v ):
330+ assert u >= 0 and u < self .size
331+ assert v >= 0 and v < self .size
332+ if u not in self .d :
333+ self .d [u ] = set ()
334+ if v not in self .d :
335+ self .d [v ] = set ()
336+ self .d [u ].add (v )
337+ self .d [v ].add (u )
338+ def remove_edge (self , u , v ):
339+ if u in self .d and v in self .d [u ]:
340+ self .d [u ].remove (v )
341+ if v in self .d and u in self .d [v ]:
342+ self .d [v ].remove (u )
343+ if v in self .d and len (self .d [v ]) == 0 :
344+ del self .d [v ]
345+ if u in self .d and len (self .d [u ]) == 0 :
346+ del self .d [u ]
347+ def pop_euler_path (self , allow_multiple_connected_components = True ):
348+ # Based on code from Przemek Drochomirecki, Krakow, 5 Nov 2006
349+ # http://code.activestate.com/recipes/498243-finding-eulerian-path-in-undirected-graph/
350+ # Under PSF License
351+ # NB: MUTATES d
352+
353+ # counting the number of vertices with odd degree
354+ odd = [x for x in self .d if len (self .d [x ])& 1 ]
355+ odd .append (self .d .keys ()[0 ])
356+ if not allow_multiple_connected_components and len (odd ) > 3 :
357+ return None
358+ stack = [odd [0 ]]
359+ path = []
360+ # main algorithm
361+ while stack :
362+ v = stack [- 1 ]
363+ if v in self .d :
364+ u = self .d [v ].pop ()
365+ stack .append (u )
366+ self .remove_edge (u , v )
367+ else :
368+ path .append (stack .pop ())
369+ return path
312370
313371 # 4: Build the edge adjacency graph
314372 G = Graph (verts .shape [0 ])
315- def indexof (v , in_this ):
316- return np .nonzero (np .all (np .abs (in_this - v ) < eps , axis = 1 ))[0 ]
317- for ii , v in enumerate (verts ):
318- for other_v in list (v2s [indexof (v , v1s )]) + list (v1s [indexof (v , v2s )]):
319- neighbors = indexof (other_v , verts )
320- for jj in neighbors :
321- G .add_edge (ii , jj )
322-
323- def euler_path (graph ):
324- # Based on code from Przemek Drochomirecki, Krakow, 5 Nov 2006
325- # http://code.activestate.com/recipes/498243-finding-eulerian-path-in-undirected-graph/
326- # Under PSF License
327- # NB: MUTATES graph
328-
329- # counting the number of vertices with odd degree
330- odd = [x for x in graph .keys () if len (graph [x ])& 1 ]
331- odd .append (graph .keys ()[0 ])
332- # This check is appropriate if there is a single connected component.
333- # Since we're willing to take away one connected component per call,
334- # we skip this check.
335- # if len(odd)>3:
336- # return None
337- stack = [odd [0 ]]
338- path = []
339- # main algorithm
340- while stack :
341- v = stack [- 1 ]
342- if v in graph :
343- u = graph [v ].pop ()
344- stack .append (u )
345- # deleting edge u-v (v-u already removed by pop)
346- graph [u ].remove (v )
347- # graph[v].remove(u)
348- if len (graph [v ]) == 0 :
349- del graph [v ]
350- if len (graph [u ]) == 0 :
351- del graph [u ]
352- else :
353- path .append (stack .pop ())
354- return path
373+ for f in fs :
374+ # Since we're dealing with a triangle that intersects the plane, exactly two of the edges
375+ # will intersect (note that the only other sorts of "intersections" are one edge in
376+ # plane or all three edges in plane, which won't be picked up by mesh_intersecting_faces).
377+ e0 = intersection_map .index (f [0 ], f [1 ])
378+ e1 = intersection_map .index (f [0 ], f [2 ])
379+ e2 = intersection_map .index (f [1 ], f [2 ])
380+ if e0 is None :
381+ G .add_edge (e1 , e2 )
382+ elif e1 is None :
383+ G .add_edge (e0 , e2 )
384+ else :
385+ G .add_edge (e0 , e1 )
355386
356387 # 5: Find the paths for each component
357388 components = []
358389 components_closed = []
359390 while len (G ) > 0 :
360- # This works because euler_path mutates the graph as it goes
361- path = euler_path (G .d )
391+ path = G .pop_euler_path ()
362392 if path is None :
363393 raise ValueError ("mesh slice has too many odd degree edges; can't find a path along the edge" )
364394 component_verts = verts [path ]
0 commit comments