Module sverchok.utils.geom_2d.dcel
Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
from itertools import cycle, chain
from typing import List, Union, Set
from .lin_alg import almost_equal, is_more, dot_product, is_ccw_polygon, cross_product
from .dcel_debugger import Debugger
"""
This module dedicated to Doubly-Connected Edge List data structure.
http://www.holmes3d.net/graphics/dcel/
At this stage the data structure works with faces only and does not support edges.
Advantages:
Have constant time access to the neighborhood of an arbitrary point can be very useful.
We can compute a normal for any given face easily, on demand, even if the vertex locations are changing.
We can also traverse every face touching a vertex (the "star" of the vertex) to easily estimate a normal for that vertex
If we are locally changing small portions of a mesh,
this is much easier than recomputing the normals for every vertex in the mesh.
Similarly, mesh simplification for Level of Detail or compressed storage becomes easier when neighbors are easily found.
Subdivision algorithms such as Loop and Catmull-Clark are relatively easy on a DCEL, especially adaptive subdivision.
"""
x, y, z = 0, 1, 2
class Point:
accuracy = 1e-5
def __init__(self, mesh, co):
self.mesh = mesh
self.co = co
self.hedge = None
def __str__(self):
return "Point"
def __add__(self, other):
# should returns object with class of inputs object
# will be an error if class of input object has other init arguments
return self.__class__(None, tuple(co1 + co2 for co1, co2 in zip(self.co, other.co)))
def __sub__(self, other):
# should returns object with class of inputs object
# will be an error if class of input object has other init arguments
return self.__class__(None, tuple(co1 - co2 for co1, co2 in zip(self.co, other.co)))
def __mul__(self, other):
# returns dot product or multiplication vector to scalar
if isinstance(other, self.__class__):
return dot_product(self.co, other.co)
else:
return self.__class__(None, [co * other for co in self.co])
def length(self):
# returns length as vector
return sum([co ** 2 for co in self.co]) ** 0.5
def normalize(self):
# making the length of the vector 1.0
mem_len = self.length()
self.co = (self.co[0] / mem_len, self.co[1] / mem_len, self.co[2] / mem_len)
return self
def cross_product(self, other):
return self.__class__(None, cross_product(self.co, other.co))
class HalfEdge:
accuracy = 1e-5
def __init__(self, mesh, point, face=None):
self.mesh = mesh # can be just None but in this case some method weren't be available
self.origin = point
self.face = face
self.twin = None
self.next = None
self.last = None
self.left = None # Information about nearest left neighbour for hole detection, intersection algorithm user
self.flags = set() # For any value which an algorithm would like to keep with object, only add or remove
self._slop = None # Should be recalculated if origin to origin of twin are changed
def __str__(self):
return "Hedge"
@property
def ccw_hedges(self):
# returns hedges originated in one point
if not self.mesh:
raise AttributeError("This method doesn't work with hedges({}) without link to a mesh."
"Besides, mesh object should have proper number of half edges "
"in hedges list".format(self))
yield self
next_edge = self.last.twin
counter = 0
while next_edge != self:
yield next_edge
next_edge = next_edge.last.twin
counter += 1
if counter > len(self.mesh.hedges):
raise RecursionError('Hedge - {} does not have a loop'.format(self))
@property
def cw_hedges(self):
# returns hedges originated in one point
if not self.mesh:
raise AttributeError("This method doesn't work with hedges({}) without link to a mesh."
"Besides, mesh object should have proper number of half edges "
"in hedges list".format(self))
yield self
next_edge = self.twin.next
counter = 0
while next_edge != self:
yield next_edge
next_edge = next_edge.twin.next
counter += 1
if counter > len(self.mesh.hedges):
raise RecursionError('Hedge - {} does not have a loop'.format(self))
@property
def loop_hedges(self):
# returns hedges bounding face
if not self.mesh:
raise AttributeError("This method doesn't work with hedges({}) without link to a mesh."
"Besides, mesh object should have proper number of half edges "
"in hedges list".format(self))
yield self
next_edge = self.next
counter = 0
while id(next_edge) != id(self):
yield next_edge
try:
next_edge = next_edge.next
except AttributeError:
raise AttributeError(' Some of half edges has incomplete data (does not have link to next half edge)')
counter += 1
if counter > len(self.mesh.hedges):
raise RecursionError('Hedge - {} does not have a loop'.format(self))
@property
def slop(self):
"""
Returns dot product of direction of half edges and -X direction in ccw order in such way
Angle 45 from -X direction in ccw order returns 0.707
Angle 90 from -X direction in ccw order returns 1.0
Angle 180 from -X direction in ccw order returns 2.0
Angle 360 or 0 from -X direction in ccw order returns 4.0
:return: float
"""
if self._slop:
pass
elif self.twin._slop:
self._slop = (self.twin._slop + 2) % 4 if self.twin._slop != 2 else 4 # corner case for horizontal
elif almost_equal(self.origin.co[y], self.twin.origin.co[y], self.accuracy): # is horizontal
if is_more(self.origin.co[x], self.twin.origin.co[x], self.accuracy):
self._slop = 4.0
else:
self._slop = 2.0
else:
direction = (self.twin.origin - self.origin).normalize()
product = dot_product(direction.co, (1, 0))
self._slop = product + 1 if direction.co[y] < 0 else 3 - product
return self._slop
class Face:
accuracy = 1e-5
def __init__(self, mesh):
self.mesh = mesh
self._outer = None # hedge of boundary loop
self._inners = [] # hedges of hole loops
self.select = False # actually should be careful with this parameter, some algorithm can use it or not
self.flags = set() # For any value which an algorithm would like to keep with object, only add or remove
self.sv_data = dict() # for any data which we would like to keep with face
def __str__(self):
return 'Face'
@property
def is_unbounded(self):
# returns true if face is boundless - includes all other faces of a mesh
return not self.outer
@property
def outer(self):
return self._outer
@outer.setter
def outer(self, value):
self.check_mesh()
if not isinstance(value, self.mesh.HalfEdge):
raise ValueError("HalfEdge type of object only can be set to outer attribute, "
"({}) was given".format(type(value)))
self._outer = value
@property
def inners(self):
# it would be nice to check whether all element of a list are correct, don't know how to do
return self._inners
@inners.setter
def inners(self, value):
self.check_mesh()
if not isinstance(value, list) and not isinstance(value[0], self.mesh.HalfEdge):
raise ValueError("List of HalfEdge(s) only can be set to inners attribute, "
"({}) was given".format(type(value)))
self._inners = value
def insert_holes(self, sv_verts, sv_faces, face_selection=None, face_data=None):
# not sure super useful, holes should not intersect with the face
self.check_mesh()
hole_mesh = generate_dcel_mesh(self.mesh, sv_verts, sv_faces, face_selection, face_data=face_data,
new_mesh=True)
self.inners.extend(hole_mesh.unbounded.inners)
[setattr(obj, 'mesh', self.mesh) for obj in chain(hole_mesh.points, hole_mesh.hedges, hole_mesh.faces)]
self.mesh.points.extend(hole_mesh.points)
self.mesh.hedges.extend(hole_mesh.hedges)
self.mesh.faces.extend(hole_mesh.faces)
for start_hedge in hole_mesh.unbounded.inners:
for hedge in start_hedge.loop_hedges:
hedge.face = self
def check_mesh(self):
# Ensure that the face belongs to a mesh
if not self.mesh:
raise AttributeError("This attribute is not available till the face does have link to a mesh")
class DCELMesh:
Point = Point
HalfEdge = HalfEdge
Face = Face
accuracy = 1e-5
def __init__(self, accuracy=None):
self.points = []
self.hedges = []
self.faces = []
self.unbounded = self.Face(self)
if accuracy:
self.set_accuracy(accuracy)
@classmethod
def set_accuracy(cls, accuracy):
# This value is using for comparing float figures
if isinstance(accuracy, int):
accuracy = 1 / 10 ** accuracy
if not (1e-1 > accuracy > 1e-15):
raise ValueError("Accuracy should between 1^-1 and 1^-15, {} value was given".format(accuracy))
cls.Point.accuracy = accuracy
cls.HalfEdge.accuracy = accuracy
cls.Face.accuracy = accuracy
cls.accuracy = accuracy
def from_sv_faces(self, verts, faces, face_selection=None, face_flag=None, face_data=None):
# face_data = {name of data: [value 1, val2, .., value n]} - number of values should be equal to number of faces
generate_dcel_mesh(self, verts, faces, face_selection, face_flag, face_data, False)
def from_sv_edges(self, verts, edges):
# Probably it is worth take in account such cases as: edge with 0 length at least
# Interesting that this method makes next attribute of end of an edge linked to a twin
edges = [edge for edge in edges if
not all([almost_equal(co1, co2, self.accuracy) for co1, co2 in zip(verts[edge[0]], verts[edge[1]])])]
points = [self.Point(self, co) for co in verts]
self.points.extend(points)
coincidence_hedges = [[] for _ in range(len(points))] # hedges coincident to points
# Generate hedges
for edge in edges:
hedge1 = self.HalfEdge(self, points[edge[0]])
hedge2 = self.HalfEdge(self, points[edge[1]])
self.points[edge[0]].hedge = hedge1 # this should be overrode several times but looks okay
self.points[edge[1]].hedge = hedge2
hedge1.twin = hedge2
hedge2.twin = hedge1
coincidence_hedges[edge[0]].append(hedge1)
coincidence_hedges[edge[1]].append(hedge2)
self.hedges.extend([hedge1, hedge2])
# Link hedges around all points
for hedges in coincidence_hedges:
hedges.sort(key=lambda hedge: hedge.slop)
for i in range(len(hedges)):
i_next = (i + 1) % len(hedges)
hedges[i].last = hedges[i_next].twin
hedges[i_next].twin.next = hedges[i]
def generate_faces_from_hedges(self):
# Generate face list from half edge list
# Tail edges will be dissolving
# Left component of hedges is taken in account
# build outer faces and detect inner faces (holes)
# if face is not ccw and there is no left neighbour it is boundless super face
# if there is left neighbour the face should be stored with only inner component,
# outer component will be find further
# will detect tails first
used = set() # type: Set[HalfEdge]
tail_key = 'tail'
rebuild = False # if there are tails some points and half edges can be loosed
for hedge in self.hedges:
if hedge in used:
continue
# https://github.com/nortikin/sverchok/pull/2623#issuecomment-546570210
used_in_loop = set() # type: Set[HalfEdge]
for loop_hedge in hedge.loop_hedges:
if loop_hedge.twin not in used_in_loop:
used_in_loop.add(loop_hedge)
else:
# this is tail, useless, for del method but not only
loop_hedge.flags.add(tail_key)
loop_hedge.twin.flags.add(tail_key)
rebuild = True
faces = [] # type: List[Face]
min_hedges = set() # type: Set[HalfEdge] # all detected leftmost half edges of every loop, not tails
inner_hedges = [] # type: List[List[HalfEdge]] # multiple loops can be produced be desolving tails algorithm
# relink half edges
# after this process it will be possible to get from tail to loop but impossible from loop to tail
# actually this process lead to losing information which it could be useful
# dissolving algorithm can create several loops from one but they are related with each other:
# - if at least one of them is outer loop so the rest are inner components of this one
# - or if they all are inner so they either belong to boundless face or are inside some another face
# Also left attribute can have links to tails what make the situation more complicated
used.clear()
for hedge in self.hedges:
if hedge in used:
continue
if tail_key in hedge.flags:
# avoid start form tails
continue
# Start handling a loop
loop_hedges = [] # type: List[HalfEdge]
for loop_hedge in hedge.loop_hedges:
loop_hedges.append(loop_hedge)
# links can be changed only when sub loop is left
if tail_key in loop_hedge.flags and tail_key not in loop_hedge.last.flags:
# this case about when previous step was from sub loop to tail
last_hedge = loop_hedge.last # origin of next hedge is in place where tail connects with a face
for cw_hedge in loop_hedge.cw_hedges:
# Try to find last normal half edge for next normal half edge
if id(cw_hedge) != id(loop_hedge) and tail_key not in cw_hedge.flags:
# check either there are other tails in the point
next_hedge = cw_hedge
break
next_hedge.last = last_hedge
last_hedge.next = next_hedge
# detect new sub loops, figure out weather loop is ccw or cw
# after code above loop_hedges attribute returns new sub loops according start hedge
new_outer = None # type: Union[None, HalfEdge]
new_inners = [] # type: List[HalfEdge]
for loop_hedge in loop_hedges:
if tail_key in loop_hedge.flags:
used.add(loop_hedge)
# just ignore tail
continue
elif loop_hedge in used:
# avoid reconsidering sub loop
continue
else:
# the start edge for sub loop is found
# mark all hedges of the sub loop for avoiding them later
[used.add(hedge) for hedge in loop_hedge.loop_hedges]
min_hedge = min([hedge for hedge in loop_hedge.loop_hedges],
key=lambda he: (he.origin.co[x], he.origin.co[y]))
min_hedges.add(min_hedge) # avoiding extra calculation later
_is_ccw = is_ccw_polygon(most_lefts=[min_hedge.last.origin.co, min_hedge.origin.co,
min_hedge.next.origin.co], accuracy=self.accuracy)
if not _is_ccw:
new_inners.append(min_hedge)
elif _is_ccw and new_outer is not None:
raise ValueError("During dissolving edges algorithm only one ccw face can be created")
else:
new_outer = loop_hedge
# handle case when after dissolving tails there are at list one outer face
if new_outer:
face = self.Face(self)
face.outer = new_outer
faces.append(face)
for h in new_outer.loop_hedges:
h.face = face
for start_hedge in new_inners:
face.inners.append(start_hedge)
for h in start_hedge.loop_hedges:
h.face = face
# case when only inners loops was found
# if left neighbour is None this mean that the inner face is a hole of boundless face in either way
# if left is not None it is impossible to say which face inner faces belongs at this stage
# if at list one left neighbour of inner start half edge is None then
# all inner loops belong to boundless face
elif new_inners:
# new inners should be also check because after dissolving half edges some loops can produce nothing
belong_to_boundless = any([start_hedge.left is None for start_hedge in new_inners])
if belong_to_boundless:
for start_hedge in new_inners:
self.unbounded.inners.append(start_hedge)
for loop_hedge in start_hedge.loop_hedges:
loop_hedge.face = self.unbounded
else:
# it impossible to say to which face the inner loops belong at this stage
# it is also possible that they belong to boundless face
inner_hedges.append(new_inners)
used.clear() # only for start half edges which are leftmost half edges
# This part about holes detection
for start_hedges in inner_hedges:
# check first probably some of the loops already was assigned to a face
# it is possible if some disjoint loop lies to the right of one sub loop face
# in this case during ahndling the loop sub loop also will be assigned to face
assigned_face = None # type: Union[None, Face]
for start_hedge in start_hedges:
if start_hedge.face and start_hedge.face.outer:
assigned_face = start_hedge.face # this can be weather boundless face or outer face
break # this means that hedge loops can belongs only one face
if assigned_face:
for start_hedge in start_hedges:
if start_hedge not in used:
used.add(start_hedge)
assigned_face.inners.append(start_hedge) # repeat of half edges should be avoided
for hedge in start_hedge.loop_hedges:
hedge.face = assigned_face
# Well, we have bad luck and no one loop was already marked in given sequence
# initialisation of walk to leftward direction should be done
# choose a hedge for start, does not matter which from the set
left_hedges = [start_hedges[0]] # type: List[HalfEdge] # list of start hedges of every inner loop detected
count = 0
while not left_hedges[-1].left or not left_hedges[-1].left.face or not left_hedges[-1].left.face.outer:
# At first check can be next jump done
if not left_hedges[-1].left:
break
# First of all try to find next loop
start_loop = None # type: Union[None, HalfEdge]
# It is necessary to know what is coming next, whether it tail half edge or normal half edge
if tail_key in left_hedges[-1].left.flags:
# it looks that iterate over tail half edges is not a good idea
# it is possible to get into endless loop,
# when leftmost point of a loop has left attribute with tail
# which leis right from left side of the loop and joined to it
# it will be better to make next jump immediately
# but if one of ccw half edges of tail has outer face it means it is boundary face
# or if it is inner component then next hole is found and should be iterated
# or if twin of one of ccw half edges of tail ahs outer face it means we also het into next hole
jump = True # True if boundary face or next hole in ccw hedges was not found
for ccw_hedge in left_hedges[-1].left.ccw_hedges:
if ccw_hedge.face and ccw_hedge.face.outer:
# the boundary face is found and should be linked to last left half edge
# it will be probably better to relink left half edge to half edge with boundary face
left_hedges[-1].left = ccw_hedge # probably not vary nice to do
jump = Face
break # todo check the end of while
elif ccw_hedge.face and ccw_hedge.face.inners:
# new next hole is found
start_loop = ccw_hedge
jump = False
break
elif ccw_hedge.twin.face:
# this also mean that next hole is found
start_loop = ccw_hedge
jump = False
break
if jump:
left_hedges.append(left_hedges[-1].left)
# start_tail = left_hedges[-1].left
# tail_hedges = [start_tail]
# next_tail = start_tail
# while True:
# if tail_key in next_tail.next.flags:
# # everything okay we are still in tail loop
# next_tail = next_tail.next
# tail_hedges.append(next_tail)
# else:
# # we step out from tail loop, switch to another side
# # so it is possible move further in normal way
# start_loop = next_tail.next
# break
# if id(next_tail) == id(start_tail):
# # this is the end of the loop
# break
# count += 1
# if count > len(self.hedges):
# raise RecursionError('Tail loop can not find the end of its tail')
# # We found tail loop it means inner face without twin outer face
# min_tail_hedge = min(tail_hedges, key=lambda he: (he.origin.co[x], he.origin.co[y]))
# left_hedges.append(min_tail_hedge) # next left half edge was found
else:
# we are in a normal loop
start_loop = left_hedges[-1].left
if start_loop:
# this means we have jumped to a normal loop or via tail joined to normal loop
# will find leftmost half edge
for hedge in start_loop.loop_hedges:
if hedge in min_hedges:
left_hedges.append(hedge)
break
count += 1
if count > len(self.hedges):
raise RecursionError('Hedge of hole can not find outer face')
# all left half edges was found and last left half edge keeps information about boundary face
# interesting thing about left hedges is the list can include tails which are useless in face creating
# set boundary face
if not left_hedges[-1].left:
face = self.unbounded
else:
face = left_hedges[-1].left.face
# make links between boundary face and inner loops
for start_hedge in left_hedges:
if tail_key in start_hedge.flags:
continue
if start_hedge in used:
continue
face.inners.append(start_hedge)
used.add(start_hedge) # this should be enough
for hedge in start_hedge.loop_hedges:
hedge.face = face
# all sub loops also can be assigned to founded face
for start_hedge in start_hedges:
if start_hedge in used:
continue
face.inners.append(start_hedge)
used.add(start_hedge)
for hedge in start_hedge.loop_hedges:
hedge.face = face
self.faces = faces
if rebuild:
self.del_loose_hedges(tail_key)
def to_sv_mesh(self, edges=True, faces=True, only_select=False, del_edge_flag=None, del_face_flag=None):
# all elements of mesh should have correct links
# will create only selected faces if only_select is True
sv_points, point_index = generate_sv_points(self, del_edge_flag=del_edge_flag, del_face_flag=del_face_flag)
if edges and not faces:
sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag)
return sv_points, sv_edges
elif faces and not edges:
sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag)
return sv_points, sv_faces
else:
sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag)
sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag)
return sv_points, sv_edges, sv_faces
def del_loose_hedges(self, flag=None):
# flag means that half edges with the value will be deleted
if flag:
hedges = []
for hedge in self.hedges:
if flag in hedge.flags:
hedge.mesh = None
else:
hedges.append(hedge)
self.hedges = hedges
else:
self.hedges = [hedge for hedge in self.hedges if hedge.mesh]
used = set()
points = []
for hedge in self.hedges:
if hedge.origin not in used:
used.add(hedge.origin)
points.append(hedge.origin)
hedge.origin.hedge = hedge # point can have link to not existing half edge
self.points = points
def del_face(self, face):
# does not remove face from self.faces, only switch link from face to unbounded face
# not sure that this is good idea, probably should not be used
# this can lead to wrong topology, difficult to predict
if face.is_unbounded:
raise ValueError('Unbounded face can not be deleted')
for start_hedge in chain([face.outer] if face.outer else [], face.inners):
self.unbounded.inners.append(start_hedge)
for hedge in start_hedge.loop_hedges:
hedge.face = self.unbounded
face.mesh = None
def generate_dcel_mesh(mesh, verts, faces, face_selection=None, face_flag=None, face_data=None, new_mesh=False):
# todo: self intersection polygons? double repeated polygons???
# face_data = {name of data: [value 1, val2, .., value n]} - number of values should be equal to number of faces
# I'm not going use zip longest or something else with face mask and face data inputs
# Because I think it can lead to unexpected behaviour of algorithms, which will be difficult to debug
# If mesh is None all will be bind to new instent
if face_selection and len(face_selection) != len(faces):
raise IndexError("Length of face_mask({}) input should be equal to"
" length of input faces({})".format(len(face_selection), len(faces)))
if face_flag and len(face_flag) != len(faces):
raise IndexError("Length of face_flag({}) input should be equal to"
" length of input faces({})".format(len(face_flag), len(faces)))
if face_data and any([len(val) != len(faces) for val in face_data.values()]):
bad_key, length = [(key, len(val)) for key, val in face_data.items() if len(val) != len(faces)][0]
raise IndexError("Face data should be a dictionary."
"Each value should be a list with length equal to length of input faces"
"At list with key({}) length of input list({}) is not equal to "
"length of input faces({})".format(bad_key, length, len(faces)))
if new_mesh:
mesh = type(mesh)(mesh.accuracy) # can bring trouble with isinstance, not sure
half_edges_list = dict()
len_added_points = len(mesh.points)
mesh.points.extend([mesh.Point(mesh, co) for co in verts])
# Generate outer faces and there hedges
# face_data_iter is tricky iterator for distributing custom properties among faces, example:
# d = {'a': [1, 2], 'b': [3, 4]}
# >>> for names, values in zip(cycle([d.keys()]), zip(*d.values())):
# ... print('Next Face')
# ... for n, v in zip(names, values):
# ... print(n, v)
# ...
# Next Face # a 1 # b 3 # Next Face # a 2 # b 4
face_data_iter = zip(cycle([face_data.keys()]), zip(*face_data.values())) if face_data else cycle([None])
for face, fm, ff, fd in zip(faces, face_selection or cycle([False]), face_flag or cycle([None]), face_data_iter):
face = face if is_ccw_polygon([verts[i] for i in face]) else face[::-1]
f = mesh.Face(mesh)
f.select = bool(fm)
if ff:
f.flags.add(ff)
if fd:
for property_name, value in zip(*fd):
f.sv_data[property_name] = value
loop = []
for i in range(len(face)):
origin_i = face[i]
next_i = face[(i + 1) % len(face)]
half_edge = mesh.HalfEdge(mesh, mesh.points[origin_i + len_added_points], f)
mesh.points[origin_i + len_added_points].hedge = half_edge # this should be overrode several times
loop.append(half_edge)
half_edges_list[(origin_i, next_i)] = half_edge
for i in range(len(face)):
loop[i].last = loop[(i - 1) % len(face)]
loop[i].next = loop[(i + 1) % len(face)]
f.outer = loop[0]
mesh.faces.append(f)
mesh.hedges.extend(list(half_edges_list.values()))
# to twin hedges and create hedges of unbounded face
outer_half_edges = dict()
for key in half_edges_list:
half_edge = half_edges_list[key]
if key[::-1] in half_edges_list:
half_edge.twin = half_edges_list[key[::-1]]
half_edges_list[key[::-1]].twin = half_edge
else:
outer_edge = mesh.HalfEdge(mesh, mesh.points[key[1] + len_added_points])
outer_edge.face = mesh.unbounded
half_edge.twin = outer_edge
outer_edge.twin = half_edge
if key[::-1] in outer_half_edges:
raise Exception("It looks like input mesh has adjacent faces with only one common point"
"Handle such meshes does not implemented yet.")
outer_half_edges[key[::-1]] = outer_edge
mesh.hedges.extend(list(outer_half_edges.values()))
# link hedges of unbounded face in loops
for key in outer_half_edges:
outer_edge = outer_half_edges[key]
next_edge = outer_edge.twin
count = 0
while next_edge:
next_edge = next_edge.last.twin
if next_edge.face.is_unbounded:
break
count += 1
if count > len(half_edges_list):
raise RecursionError("The hedge ({}) can't find next neighbour".format(outer_edge))
outer_edge.next = next_edge
next_edge.last = outer_edge
# link unbounded face to loops of edges of unbounded face
used = set()
for outer_hedge in outer_half_edges.values():
if outer_hedge in used:
continue
mesh.unbounded.inners.append(outer_hedge)
[used.add(hedge) for hedge in outer_hedge.loop_hedges]
return mesh
def generate_sv_points(dcel_mesh, del_edge_flag=None, del_face_flag=None):
# This function also takes in account faces which should be deleted
# if all hedges around points have faces with del flag the point won't be added to the output list
if del_edge_flag and del_face_flag:
raise ValueError('Not sure that both del flags can do the job')
used = set()
point_index = dict()
sv_verts = []
if not del_face_flag and not del_edge_flag:
for hedge in dcel_mesh.hedges:
if hedge in used:
continue
point_index[hedge.origin] = len(sv_verts)
sv_verts.append(hedge.origin.co)
for h in hedge.ccw_hedges:
used.add(h)
elif del_face_flag:
for hedge in dcel_mesh.hedges:
point_usage = False
if hedge in used:
continue
for h in hedge.ccw_hedges:
used.add(h)
if not h.face.is_unbounded and del_face_flag not in h.face.flags:
point_usage = True
if point_usage:
point_index[hedge.origin] = len(sv_verts)
sv_verts.append(hedge.origin.co)
else:
for hedge in dcel_mesh.hedges:
point_usage = False
if hedge in used:
continue
for h in hedge.ccw_hedges:
used.add(h)
if del_edge_flag not in h.flags:
point_usage = True
if point_usage:
point_index[hedge.origin] = len(sv_verts)
sv_verts.append(hedge.origin.co)
return sv_verts, point_index
def generate_sv_edges(dcel_mesh, point_index, del_flag=None):
sv_edges = []
used = set()
for hedge in dcel_mesh.hedges:
if hedge in used:
continue
if del_flag and del_flag in hedge.flags:
continue
used.add(hedge)
used.add(hedge.twin)
sv_edges.append((point_index[hedge.origin], point_index[hedge.twin.origin]))
return sv_edges
def generate_sv_faces(dcel_mesh, point_index, only_select=False, del_flag=None):
# This part of function creates faces in SV format
# It ignores boundless super face
sv_faces = []
for i, face in enumerate(dcel_mesh.faces):
if face.inners and face.outer:
'Face ({}) has inner components! Sverchok can not show polygons with holes.'.format(i)
if not face.outer or del_flag in face.flags:
continue
if only_select and not face.select:
continue
sv_faces.append([point_index[hedge.origin] for hedge in face.outer.loop_hedges])
return sv_faces
Functions
def generate_dcel_mesh(mesh, verts, faces, face_selection=None, face_flag=None, face_data=None, new_mesh=False)
-
Expand source code
def generate_dcel_mesh(mesh, verts, faces, face_selection=None, face_flag=None, face_data=None, new_mesh=False): # todo: self intersection polygons? double repeated polygons??? # face_data = {name of data: [value 1, val2, .., value n]} - number of values should be equal to number of faces # I'm not going use zip longest or something else with face mask and face data inputs # Because I think it can lead to unexpected behaviour of algorithms, which will be difficult to debug # If mesh is None all will be bind to new instent if face_selection and len(face_selection) != len(faces): raise IndexError("Length of face_mask({}) input should be equal to" " length of input faces({})".format(len(face_selection), len(faces))) if face_flag and len(face_flag) != len(faces): raise IndexError("Length of face_flag({}) input should be equal to" " length of input faces({})".format(len(face_flag), len(faces))) if face_data and any([len(val) != len(faces) for val in face_data.values()]): bad_key, length = [(key, len(val)) for key, val in face_data.items() if len(val) != len(faces)][0] raise IndexError("Face data should be a dictionary." "Each value should be a list with length equal to length of input faces" "At list with key({}) length of input list({}) is not equal to " "length of input faces({})".format(bad_key, length, len(faces))) if new_mesh: mesh = type(mesh)(mesh.accuracy) # can bring trouble with isinstance, not sure half_edges_list = dict() len_added_points = len(mesh.points) mesh.points.extend([mesh.Point(mesh, co) for co in verts]) # Generate outer faces and there hedges # face_data_iter is tricky iterator for distributing custom properties among faces, example: # d = {'a': [1, 2], 'b': [3, 4]} # >>> for names, values in zip(cycle([d.keys()]), zip(*d.values())): # ... print('Next Face') # ... for n, v in zip(names, values): # ... print(n, v) # ... # Next Face # a 1 # b 3 # Next Face # a 2 # b 4 face_data_iter = zip(cycle([face_data.keys()]), zip(*face_data.values())) if face_data else cycle([None]) for face, fm, ff, fd in zip(faces, face_selection or cycle([False]), face_flag or cycle([None]), face_data_iter): face = face if is_ccw_polygon([verts[i] for i in face]) else face[::-1] f = mesh.Face(mesh) f.select = bool(fm) if ff: f.flags.add(ff) if fd: for property_name, value in zip(*fd): f.sv_data[property_name] = value loop = [] for i in range(len(face)): origin_i = face[i] next_i = face[(i + 1) % len(face)] half_edge = mesh.HalfEdge(mesh, mesh.points[origin_i + len_added_points], f) mesh.points[origin_i + len_added_points].hedge = half_edge # this should be overrode several times loop.append(half_edge) half_edges_list[(origin_i, next_i)] = half_edge for i in range(len(face)): loop[i].last = loop[(i - 1) % len(face)] loop[i].next = loop[(i + 1) % len(face)] f.outer = loop[0] mesh.faces.append(f) mesh.hedges.extend(list(half_edges_list.values())) # to twin hedges and create hedges of unbounded face outer_half_edges = dict() for key in half_edges_list: half_edge = half_edges_list[key] if key[::-1] in half_edges_list: half_edge.twin = half_edges_list[key[::-1]] half_edges_list[key[::-1]].twin = half_edge else: outer_edge = mesh.HalfEdge(mesh, mesh.points[key[1] + len_added_points]) outer_edge.face = mesh.unbounded half_edge.twin = outer_edge outer_edge.twin = half_edge if key[::-1] in outer_half_edges: raise Exception("It looks like input mesh has adjacent faces with only one common point" "Handle such meshes does not implemented yet.") outer_half_edges[key[::-1]] = outer_edge mesh.hedges.extend(list(outer_half_edges.values())) # link hedges of unbounded face in loops for key in outer_half_edges: outer_edge = outer_half_edges[key] next_edge = outer_edge.twin count = 0 while next_edge: next_edge = next_edge.last.twin if next_edge.face.is_unbounded: break count += 1 if count > len(half_edges_list): raise RecursionError("The hedge ({}) can't find next neighbour".format(outer_edge)) outer_edge.next = next_edge next_edge.last = outer_edge # link unbounded face to loops of edges of unbounded face used = set() for outer_hedge in outer_half_edges.values(): if outer_hedge in used: continue mesh.unbounded.inners.append(outer_hedge) [used.add(hedge) for hedge in outer_hedge.loop_hedges] return mesh
def generate_sv_edges(dcel_mesh, point_index, del_flag=None)
-
Expand source code
def generate_sv_edges(dcel_mesh, point_index, del_flag=None): sv_edges = [] used = set() for hedge in dcel_mesh.hedges: if hedge in used: continue if del_flag and del_flag in hedge.flags: continue used.add(hedge) used.add(hedge.twin) sv_edges.append((point_index[hedge.origin], point_index[hedge.twin.origin])) return sv_edges
def generate_sv_faces(dcel_mesh, point_index, only_select=False, del_flag=None)
-
Expand source code
def generate_sv_faces(dcel_mesh, point_index, only_select=False, del_flag=None): # This part of function creates faces in SV format # It ignores boundless super face sv_faces = [] for i, face in enumerate(dcel_mesh.faces): if face.inners and face.outer: 'Face ({}) has inner components! Sverchok can not show polygons with holes.'.format(i) if not face.outer or del_flag in face.flags: continue if only_select and not face.select: continue sv_faces.append([point_index[hedge.origin] for hedge in face.outer.loop_hedges]) return sv_faces
def generate_sv_points(dcel_mesh, del_edge_flag=None, del_face_flag=None)
-
Expand source code
def generate_sv_points(dcel_mesh, del_edge_flag=None, del_face_flag=None): # This function also takes in account faces which should be deleted # if all hedges around points have faces with del flag the point won't be added to the output list if del_edge_flag and del_face_flag: raise ValueError('Not sure that both del flags can do the job') used = set() point_index = dict() sv_verts = [] if not del_face_flag and not del_edge_flag: for hedge in dcel_mesh.hedges: if hedge in used: continue point_index[hedge.origin] = len(sv_verts) sv_verts.append(hedge.origin.co) for h in hedge.ccw_hedges: used.add(h) elif del_face_flag: for hedge in dcel_mesh.hedges: point_usage = False if hedge in used: continue for h in hedge.ccw_hedges: used.add(h) if not h.face.is_unbounded and del_face_flag not in h.face.flags: point_usage = True if point_usage: point_index[hedge.origin] = len(sv_verts) sv_verts.append(hedge.origin.co) else: for hedge in dcel_mesh.hedges: point_usage = False if hedge in used: continue for h in hedge.ccw_hedges: used.add(h) if del_edge_flag not in h.flags: point_usage = True if point_usage: point_index[hedge.origin] = len(sv_verts) sv_verts.append(hedge.origin.co) return sv_verts, point_index
Classes
class DCELMesh (accuracy=None)
-
Expand source code
class DCELMesh: Point = Point HalfEdge = HalfEdge Face = Face accuracy = 1e-5 def __init__(self, accuracy=None): self.points = [] self.hedges = [] self.faces = [] self.unbounded = self.Face(self) if accuracy: self.set_accuracy(accuracy) @classmethod def set_accuracy(cls, accuracy): # This value is using for comparing float figures if isinstance(accuracy, int): accuracy = 1 / 10 ** accuracy if not (1e-1 > accuracy > 1e-15): raise ValueError("Accuracy should between 1^-1 and 1^-15, {} value was given".format(accuracy)) cls.Point.accuracy = accuracy cls.HalfEdge.accuracy = accuracy cls.Face.accuracy = accuracy cls.accuracy = accuracy def from_sv_faces(self, verts, faces, face_selection=None, face_flag=None, face_data=None): # face_data = {name of data: [value 1, val2, .., value n]} - number of values should be equal to number of faces generate_dcel_mesh(self, verts, faces, face_selection, face_flag, face_data, False) def from_sv_edges(self, verts, edges): # Probably it is worth take in account such cases as: edge with 0 length at least # Interesting that this method makes next attribute of end of an edge linked to a twin edges = [edge for edge in edges if not all([almost_equal(co1, co2, self.accuracy) for co1, co2 in zip(verts[edge[0]], verts[edge[1]])])] points = [self.Point(self, co) for co in verts] self.points.extend(points) coincidence_hedges = [[] for _ in range(len(points))] # hedges coincident to points # Generate hedges for edge in edges: hedge1 = self.HalfEdge(self, points[edge[0]]) hedge2 = self.HalfEdge(self, points[edge[1]]) self.points[edge[0]].hedge = hedge1 # this should be overrode several times but looks okay self.points[edge[1]].hedge = hedge2 hedge1.twin = hedge2 hedge2.twin = hedge1 coincidence_hedges[edge[0]].append(hedge1) coincidence_hedges[edge[1]].append(hedge2) self.hedges.extend([hedge1, hedge2]) # Link hedges around all points for hedges in coincidence_hedges: hedges.sort(key=lambda hedge: hedge.slop) for i in range(len(hedges)): i_next = (i + 1) % len(hedges) hedges[i].last = hedges[i_next].twin hedges[i_next].twin.next = hedges[i] def generate_faces_from_hedges(self): # Generate face list from half edge list # Tail edges will be dissolving # Left component of hedges is taken in account # build outer faces and detect inner faces (holes) # if face is not ccw and there is no left neighbour it is boundless super face # if there is left neighbour the face should be stored with only inner component, # outer component will be find further # will detect tails first used = set() # type: Set[HalfEdge] tail_key = 'tail' rebuild = False # if there are tails some points and half edges can be loosed for hedge in self.hedges: if hedge in used: continue # https://github.com/nortikin/sverchok/pull/2623#issuecomment-546570210 used_in_loop = set() # type: Set[HalfEdge] for loop_hedge in hedge.loop_hedges: if loop_hedge.twin not in used_in_loop: used_in_loop.add(loop_hedge) else: # this is tail, useless, for del method but not only loop_hedge.flags.add(tail_key) loop_hedge.twin.flags.add(tail_key) rebuild = True faces = [] # type: List[Face] min_hedges = set() # type: Set[HalfEdge] # all detected leftmost half edges of every loop, not tails inner_hedges = [] # type: List[List[HalfEdge]] # multiple loops can be produced be desolving tails algorithm # relink half edges # after this process it will be possible to get from tail to loop but impossible from loop to tail # actually this process lead to losing information which it could be useful # dissolving algorithm can create several loops from one but they are related with each other: # - if at least one of them is outer loop so the rest are inner components of this one # - or if they all are inner so they either belong to boundless face or are inside some another face # Also left attribute can have links to tails what make the situation more complicated used.clear() for hedge in self.hedges: if hedge in used: continue if tail_key in hedge.flags: # avoid start form tails continue # Start handling a loop loop_hedges = [] # type: List[HalfEdge] for loop_hedge in hedge.loop_hedges: loop_hedges.append(loop_hedge) # links can be changed only when sub loop is left if tail_key in loop_hedge.flags and tail_key not in loop_hedge.last.flags: # this case about when previous step was from sub loop to tail last_hedge = loop_hedge.last # origin of next hedge is in place where tail connects with a face for cw_hedge in loop_hedge.cw_hedges: # Try to find last normal half edge for next normal half edge if id(cw_hedge) != id(loop_hedge) and tail_key not in cw_hedge.flags: # check either there are other tails in the point next_hedge = cw_hedge break next_hedge.last = last_hedge last_hedge.next = next_hedge # detect new sub loops, figure out weather loop is ccw or cw # after code above loop_hedges attribute returns new sub loops according start hedge new_outer = None # type: Union[None, HalfEdge] new_inners = [] # type: List[HalfEdge] for loop_hedge in loop_hedges: if tail_key in loop_hedge.flags: used.add(loop_hedge) # just ignore tail continue elif loop_hedge in used: # avoid reconsidering sub loop continue else: # the start edge for sub loop is found # mark all hedges of the sub loop for avoiding them later [used.add(hedge) for hedge in loop_hedge.loop_hedges] min_hedge = min([hedge for hedge in loop_hedge.loop_hedges], key=lambda he: (he.origin.co[x], he.origin.co[y])) min_hedges.add(min_hedge) # avoiding extra calculation later _is_ccw = is_ccw_polygon(most_lefts=[min_hedge.last.origin.co, min_hedge.origin.co, min_hedge.next.origin.co], accuracy=self.accuracy) if not _is_ccw: new_inners.append(min_hedge) elif _is_ccw and new_outer is not None: raise ValueError("During dissolving edges algorithm only one ccw face can be created") else: new_outer = loop_hedge # handle case when after dissolving tails there are at list one outer face if new_outer: face = self.Face(self) face.outer = new_outer faces.append(face) for h in new_outer.loop_hedges: h.face = face for start_hedge in new_inners: face.inners.append(start_hedge) for h in start_hedge.loop_hedges: h.face = face # case when only inners loops was found # if left neighbour is None this mean that the inner face is a hole of boundless face in either way # if left is not None it is impossible to say which face inner faces belongs at this stage # if at list one left neighbour of inner start half edge is None then # all inner loops belong to boundless face elif new_inners: # new inners should be also check because after dissolving half edges some loops can produce nothing belong_to_boundless = any([start_hedge.left is None for start_hedge in new_inners]) if belong_to_boundless: for start_hedge in new_inners: self.unbounded.inners.append(start_hedge) for loop_hedge in start_hedge.loop_hedges: loop_hedge.face = self.unbounded else: # it impossible to say to which face the inner loops belong at this stage # it is also possible that they belong to boundless face inner_hedges.append(new_inners) used.clear() # only for start half edges which are leftmost half edges # This part about holes detection for start_hedges in inner_hedges: # check first probably some of the loops already was assigned to a face # it is possible if some disjoint loop lies to the right of one sub loop face # in this case during ahndling the loop sub loop also will be assigned to face assigned_face = None # type: Union[None, Face] for start_hedge in start_hedges: if start_hedge.face and start_hedge.face.outer: assigned_face = start_hedge.face # this can be weather boundless face or outer face break # this means that hedge loops can belongs only one face if assigned_face: for start_hedge in start_hedges: if start_hedge not in used: used.add(start_hedge) assigned_face.inners.append(start_hedge) # repeat of half edges should be avoided for hedge in start_hedge.loop_hedges: hedge.face = assigned_face # Well, we have bad luck and no one loop was already marked in given sequence # initialisation of walk to leftward direction should be done # choose a hedge for start, does not matter which from the set left_hedges = [start_hedges[0]] # type: List[HalfEdge] # list of start hedges of every inner loop detected count = 0 while not left_hedges[-1].left or not left_hedges[-1].left.face or not left_hedges[-1].left.face.outer: # At first check can be next jump done if not left_hedges[-1].left: break # First of all try to find next loop start_loop = None # type: Union[None, HalfEdge] # It is necessary to know what is coming next, whether it tail half edge or normal half edge if tail_key in left_hedges[-1].left.flags: # it looks that iterate over tail half edges is not a good idea # it is possible to get into endless loop, # when leftmost point of a loop has left attribute with tail # which leis right from left side of the loop and joined to it # it will be better to make next jump immediately # but if one of ccw half edges of tail has outer face it means it is boundary face # or if it is inner component then next hole is found and should be iterated # or if twin of one of ccw half edges of tail ahs outer face it means we also het into next hole jump = True # True if boundary face or next hole in ccw hedges was not found for ccw_hedge in left_hedges[-1].left.ccw_hedges: if ccw_hedge.face and ccw_hedge.face.outer: # the boundary face is found and should be linked to last left half edge # it will be probably better to relink left half edge to half edge with boundary face left_hedges[-1].left = ccw_hedge # probably not vary nice to do jump = Face break # todo check the end of while elif ccw_hedge.face and ccw_hedge.face.inners: # new next hole is found start_loop = ccw_hedge jump = False break elif ccw_hedge.twin.face: # this also mean that next hole is found start_loop = ccw_hedge jump = False break if jump: left_hedges.append(left_hedges[-1].left) # start_tail = left_hedges[-1].left # tail_hedges = [start_tail] # next_tail = start_tail # while True: # if tail_key in next_tail.next.flags: # # everything okay we are still in tail loop # next_tail = next_tail.next # tail_hedges.append(next_tail) # else: # # we step out from tail loop, switch to another side # # so it is possible move further in normal way # start_loop = next_tail.next # break # if id(next_tail) == id(start_tail): # # this is the end of the loop # break # count += 1 # if count > len(self.hedges): # raise RecursionError('Tail loop can not find the end of its tail') # # We found tail loop it means inner face without twin outer face # min_tail_hedge = min(tail_hedges, key=lambda he: (he.origin.co[x], he.origin.co[y])) # left_hedges.append(min_tail_hedge) # next left half edge was found else: # we are in a normal loop start_loop = left_hedges[-1].left if start_loop: # this means we have jumped to a normal loop or via tail joined to normal loop # will find leftmost half edge for hedge in start_loop.loop_hedges: if hedge in min_hedges: left_hedges.append(hedge) break count += 1 if count > len(self.hedges): raise RecursionError('Hedge of hole can not find outer face') # all left half edges was found and last left half edge keeps information about boundary face # interesting thing about left hedges is the list can include tails which are useless in face creating # set boundary face if not left_hedges[-1].left: face = self.unbounded else: face = left_hedges[-1].left.face # make links between boundary face and inner loops for start_hedge in left_hedges: if tail_key in start_hedge.flags: continue if start_hedge in used: continue face.inners.append(start_hedge) used.add(start_hedge) # this should be enough for hedge in start_hedge.loop_hedges: hedge.face = face # all sub loops also can be assigned to founded face for start_hedge in start_hedges: if start_hedge in used: continue face.inners.append(start_hedge) used.add(start_hedge) for hedge in start_hedge.loop_hedges: hedge.face = face self.faces = faces if rebuild: self.del_loose_hedges(tail_key) def to_sv_mesh(self, edges=True, faces=True, only_select=False, del_edge_flag=None, del_face_flag=None): # all elements of mesh should have correct links # will create only selected faces if only_select is True sv_points, point_index = generate_sv_points(self, del_edge_flag=del_edge_flag, del_face_flag=del_face_flag) if edges and not faces: sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag) return sv_points, sv_edges elif faces and not edges: sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag) return sv_points, sv_faces else: sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag) sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag) return sv_points, sv_edges, sv_faces def del_loose_hedges(self, flag=None): # flag means that half edges with the value will be deleted if flag: hedges = [] for hedge in self.hedges: if flag in hedge.flags: hedge.mesh = None else: hedges.append(hedge) self.hedges = hedges else: self.hedges = [hedge for hedge in self.hedges if hedge.mesh] used = set() points = [] for hedge in self.hedges: if hedge.origin not in used: used.add(hedge.origin) points.append(hedge.origin) hedge.origin.hedge = hedge # point can have link to not existing half edge self.points = points def del_face(self, face): # does not remove face from self.faces, only switch link from face to unbounded face # not sure that this is good idea, probably should not be used # this can lead to wrong topology, difficult to predict if face.is_unbounded: raise ValueError('Unbounded face can not be deleted') for start_hedge in chain([face.outer] if face.outer else [], face.inners): self.unbounded.inners.append(start_hedge) for hedge in start_hedge.loop_hedges: hedge.face = self.unbounded face.mesh = None
Subclasses
Class variables
var Face
var HalfEdge
var Point
var accuracy
Static methods
def set_accuracy(accuracy)
-
Expand source code
@classmethod def set_accuracy(cls, accuracy): # This value is using for comparing float figures if isinstance(accuracy, int): accuracy = 1 / 10 ** accuracy if not (1e-1 > accuracy > 1e-15): raise ValueError("Accuracy should between 1^-1 and 1^-15, {} value was given".format(accuracy)) cls.Point.accuracy = accuracy cls.HalfEdge.accuracy = accuracy cls.Face.accuracy = accuracy cls.accuracy = accuracy
Methods
def del_face(self, face)
-
Expand source code
def del_face(self, face): # does not remove face from self.faces, only switch link from face to unbounded face # not sure that this is good idea, probably should not be used # this can lead to wrong topology, difficult to predict if face.is_unbounded: raise ValueError('Unbounded face can not be deleted') for start_hedge in chain([face.outer] if face.outer else [], face.inners): self.unbounded.inners.append(start_hedge) for hedge in start_hedge.loop_hedges: hedge.face = self.unbounded face.mesh = None
def del_loose_hedges(self, flag=None)
-
Expand source code
def del_loose_hedges(self, flag=None): # flag means that half edges with the value will be deleted if flag: hedges = [] for hedge in self.hedges: if flag in hedge.flags: hedge.mesh = None else: hedges.append(hedge) self.hedges = hedges else: self.hedges = [hedge for hedge in self.hedges if hedge.mesh] used = set() points = [] for hedge in self.hedges: if hedge.origin not in used: used.add(hedge.origin) points.append(hedge.origin) hedge.origin.hedge = hedge # point can have link to not existing half edge self.points = points
def from_sv_edges(self, verts, edges)
-
Expand source code
def from_sv_edges(self, verts, edges): # Probably it is worth take in account such cases as: edge with 0 length at least # Interesting that this method makes next attribute of end of an edge linked to a twin edges = [edge for edge in edges if not all([almost_equal(co1, co2, self.accuracy) for co1, co2 in zip(verts[edge[0]], verts[edge[1]])])] points = [self.Point(self, co) for co in verts] self.points.extend(points) coincidence_hedges = [[] for _ in range(len(points))] # hedges coincident to points # Generate hedges for edge in edges: hedge1 = self.HalfEdge(self, points[edge[0]]) hedge2 = self.HalfEdge(self, points[edge[1]]) self.points[edge[0]].hedge = hedge1 # this should be overrode several times but looks okay self.points[edge[1]].hedge = hedge2 hedge1.twin = hedge2 hedge2.twin = hedge1 coincidence_hedges[edge[0]].append(hedge1) coincidence_hedges[edge[1]].append(hedge2) self.hedges.extend([hedge1, hedge2]) # Link hedges around all points for hedges in coincidence_hedges: hedges.sort(key=lambda hedge: hedge.slop) for i in range(len(hedges)): i_next = (i + 1) % len(hedges) hedges[i].last = hedges[i_next].twin hedges[i_next].twin.next = hedges[i]
def from_sv_faces(self, verts, faces, face_selection=None, face_flag=None, face_data=None)
-
Expand source code
def from_sv_faces(self, verts, faces, face_selection=None, face_flag=None, face_data=None): # face_data = {name of data: [value 1, val2, .., value n]} - number of values should be equal to number of faces generate_dcel_mesh(self, verts, faces, face_selection, face_flag, face_data, False)
def generate_faces_from_hedges(self)
-
Expand source code
def generate_faces_from_hedges(self): # Generate face list from half edge list # Tail edges will be dissolving # Left component of hedges is taken in account # build outer faces and detect inner faces (holes) # if face is not ccw and there is no left neighbour it is boundless super face # if there is left neighbour the face should be stored with only inner component, # outer component will be find further # will detect tails first used = set() # type: Set[HalfEdge] tail_key = 'tail' rebuild = False # if there are tails some points and half edges can be loosed for hedge in self.hedges: if hedge in used: continue # https://github.com/nortikin/sverchok/pull/2623#issuecomment-546570210 used_in_loop = set() # type: Set[HalfEdge] for loop_hedge in hedge.loop_hedges: if loop_hedge.twin not in used_in_loop: used_in_loop.add(loop_hedge) else: # this is tail, useless, for del method but not only loop_hedge.flags.add(tail_key) loop_hedge.twin.flags.add(tail_key) rebuild = True faces = [] # type: List[Face] min_hedges = set() # type: Set[HalfEdge] # all detected leftmost half edges of every loop, not tails inner_hedges = [] # type: List[List[HalfEdge]] # multiple loops can be produced be desolving tails algorithm # relink half edges # after this process it will be possible to get from tail to loop but impossible from loop to tail # actually this process lead to losing information which it could be useful # dissolving algorithm can create several loops from one but they are related with each other: # - if at least one of them is outer loop so the rest are inner components of this one # - or if they all are inner so they either belong to boundless face or are inside some another face # Also left attribute can have links to tails what make the situation more complicated used.clear() for hedge in self.hedges: if hedge in used: continue if tail_key in hedge.flags: # avoid start form tails continue # Start handling a loop loop_hedges = [] # type: List[HalfEdge] for loop_hedge in hedge.loop_hedges: loop_hedges.append(loop_hedge) # links can be changed only when sub loop is left if tail_key in loop_hedge.flags and tail_key not in loop_hedge.last.flags: # this case about when previous step was from sub loop to tail last_hedge = loop_hedge.last # origin of next hedge is in place where tail connects with a face for cw_hedge in loop_hedge.cw_hedges: # Try to find last normal half edge for next normal half edge if id(cw_hedge) != id(loop_hedge) and tail_key not in cw_hedge.flags: # check either there are other tails in the point next_hedge = cw_hedge break next_hedge.last = last_hedge last_hedge.next = next_hedge # detect new sub loops, figure out weather loop is ccw or cw # after code above loop_hedges attribute returns new sub loops according start hedge new_outer = None # type: Union[None, HalfEdge] new_inners = [] # type: List[HalfEdge] for loop_hedge in loop_hedges: if tail_key in loop_hedge.flags: used.add(loop_hedge) # just ignore tail continue elif loop_hedge in used: # avoid reconsidering sub loop continue else: # the start edge for sub loop is found # mark all hedges of the sub loop for avoiding them later [used.add(hedge) for hedge in loop_hedge.loop_hedges] min_hedge = min([hedge for hedge in loop_hedge.loop_hedges], key=lambda he: (he.origin.co[x], he.origin.co[y])) min_hedges.add(min_hedge) # avoiding extra calculation later _is_ccw = is_ccw_polygon(most_lefts=[min_hedge.last.origin.co, min_hedge.origin.co, min_hedge.next.origin.co], accuracy=self.accuracy) if not _is_ccw: new_inners.append(min_hedge) elif _is_ccw and new_outer is not None: raise ValueError("During dissolving edges algorithm only one ccw face can be created") else: new_outer = loop_hedge # handle case when after dissolving tails there are at list one outer face if new_outer: face = self.Face(self) face.outer = new_outer faces.append(face) for h in new_outer.loop_hedges: h.face = face for start_hedge in new_inners: face.inners.append(start_hedge) for h in start_hedge.loop_hedges: h.face = face # case when only inners loops was found # if left neighbour is None this mean that the inner face is a hole of boundless face in either way # if left is not None it is impossible to say which face inner faces belongs at this stage # if at list one left neighbour of inner start half edge is None then # all inner loops belong to boundless face elif new_inners: # new inners should be also check because after dissolving half edges some loops can produce nothing belong_to_boundless = any([start_hedge.left is None for start_hedge in new_inners]) if belong_to_boundless: for start_hedge in new_inners: self.unbounded.inners.append(start_hedge) for loop_hedge in start_hedge.loop_hedges: loop_hedge.face = self.unbounded else: # it impossible to say to which face the inner loops belong at this stage # it is also possible that they belong to boundless face inner_hedges.append(new_inners) used.clear() # only for start half edges which are leftmost half edges # This part about holes detection for start_hedges in inner_hedges: # check first probably some of the loops already was assigned to a face # it is possible if some disjoint loop lies to the right of one sub loop face # in this case during ahndling the loop sub loop also will be assigned to face assigned_face = None # type: Union[None, Face] for start_hedge in start_hedges: if start_hedge.face and start_hedge.face.outer: assigned_face = start_hedge.face # this can be weather boundless face or outer face break # this means that hedge loops can belongs only one face if assigned_face: for start_hedge in start_hedges: if start_hedge not in used: used.add(start_hedge) assigned_face.inners.append(start_hedge) # repeat of half edges should be avoided for hedge in start_hedge.loop_hedges: hedge.face = assigned_face # Well, we have bad luck and no one loop was already marked in given sequence # initialisation of walk to leftward direction should be done # choose a hedge for start, does not matter which from the set left_hedges = [start_hedges[0]] # type: List[HalfEdge] # list of start hedges of every inner loop detected count = 0 while not left_hedges[-1].left or not left_hedges[-1].left.face or not left_hedges[-1].left.face.outer: # At first check can be next jump done if not left_hedges[-1].left: break # First of all try to find next loop start_loop = None # type: Union[None, HalfEdge] # It is necessary to know what is coming next, whether it tail half edge or normal half edge if tail_key in left_hedges[-1].left.flags: # it looks that iterate over tail half edges is not a good idea # it is possible to get into endless loop, # when leftmost point of a loop has left attribute with tail # which leis right from left side of the loop and joined to it # it will be better to make next jump immediately # but if one of ccw half edges of tail has outer face it means it is boundary face # or if it is inner component then next hole is found and should be iterated # or if twin of one of ccw half edges of tail ahs outer face it means we also het into next hole jump = True # True if boundary face or next hole in ccw hedges was not found for ccw_hedge in left_hedges[-1].left.ccw_hedges: if ccw_hedge.face and ccw_hedge.face.outer: # the boundary face is found and should be linked to last left half edge # it will be probably better to relink left half edge to half edge with boundary face left_hedges[-1].left = ccw_hedge # probably not vary nice to do jump = Face break # todo check the end of while elif ccw_hedge.face and ccw_hedge.face.inners: # new next hole is found start_loop = ccw_hedge jump = False break elif ccw_hedge.twin.face: # this also mean that next hole is found start_loop = ccw_hedge jump = False break if jump: left_hedges.append(left_hedges[-1].left) # start_tail = left_hedges[-1].left # tail_hedges = [start_tail] # next_tail = start_tail # while True: # if tail_key in next_tail.next.flags: # # everything okay we are still in tail loop # next_tail = next_tail.next # tail_hedges.append(next_tail) # else: # # we step out from tail loop, switch to another side # # so it is possible move further in normal way # start_loop = next_tail.next # break # if id(next_tail) == id(start_tail): # # this is the end of the loop # break # count += 1 # if count > len(self.hedges): # raise RecursionError('Tail loop can not find the end of its tail') # # We found tail loop it means inner face without twin outer face # min_tail_hedge = min(tail_hedges, key=lambda he: (he.origin.co[x], he.origin.co[y])) # left_hedges.append(min_tail_hedge) # next left half edge was found else: # we are in a normal loop start_loop = left_hedges[-1].left if start_loop: # this means we have jumped to a normal loop or via tail joined to normal loop # will find leftmost half edge for hedge in start_loop.loop_hedges: if hedge in min_hedges: left_hedges.append(hedge) break count += 1 if count > len(self.hedges): raise RecursionError('Hedge of hole can not find outer face') # all left half edges was found and last left half edge keeps information about boundary face # interesting thing about left hedges is the list can include tails which are useless in face creating # set boundary face if not left_hedges[-1].left: face = self.unbounded else: face = left_hedges[-1].left.face # make links between boundary face and inner loops for start_hedge in left_hedges: if tail_key in start_hedge.flags: continue if start_hedge in used: continue face.inners.append(start_hedge) used.add(start_hedge) # this should be enough for hedge in start_hedge.loop_hedges: hedge.face = face # all sub loops also can be assigned to founded face for start_hedge in start_hedges: if start_hedge in used: continue face.inners.append(start_hedge) used.add(start_hedge) for hedge in start_hedge.loop_hedges: hedge.face = face self.faces = faces if rebuild: self.del_loose_hedges(tail_key)
def to_sv_mesh(self, edges=True, faces=True, only_select=False, del_edge_flag=None, del_face_flag=None)
-
Expand source code
def to_sv_mesh(self, edges=True, faces=True, only_select=False, del_edge_flag=None, del_face_flag=None): # all elements of mesh should have correct links # will create only selected faces if only_select is True sv_points, point_index = generate_sv_points(self, del_edge_flag=del_edge_flag, del_face_flag=del_face_flag) if edges and not faces: sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag) return sv_points, sv_edges elif faces and not edges: sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag) return sv_points, sv_faces else: sv_edges = generate_sv_edges(self, point_index, del_flag=del_edge_flag) sv_faces = generate_sv_faces(self, point_index, only_select, del_face_flag) return sv_points, sv_edges, sv_faces
class Face (mesh)
-
Expand source code
class Face: accuracy = 1e-5 def __init__(self, mesh): self.mesh = mesh self._outer = None # hedge of boundary loop self._inners = [] # hedges of hole loops self.select = False # actually should be careful with this parameter, some algorithm can use it or not self.flags = set() # For any value which an algorithm would like to keep with object, only add or remove self.sv_data = dict() # for any data which we would like to keep with face def __str__(self): return 'Face' @property def is_unbounded(self): # returns true if face is boundless - includes all other faces of a mesh return not self.outer @property def outer(self): return self._outer @outer.setter def outer(self, value): self.check_mesh() if not isinstance(value, self.mesh.HalfEdge): raise ValueError("HalfEdge type of object only can be set to outer attribute, " "({}) was given".format(type(value))) self._outer = value @property def inners(self): # it would be nice to check whether all element of a list are correct, don't know how to do return self._inners @inners.setter def inners(self, value): self.check_mesh() if not isinstance(value, list) and not isinstance(value[0], self.mesh.HalfEdge): raise ValueError("List of HalfEdge(s) only can be set to inners attribute, " "({}) was given".format(type(value))) self._inners = value def insert_holes(self, sv_verts, sv_faces, face_selection=None, face_data=None): # not sure super useful, holes should not intersect with the face self.check_mesh() hole_mesh = generate_dcel_mesh(self.mesh, sv_verts, sv_faces, face_selection, face_data=face_data, new_mesh=True) self.inners.extend(hole_mesh.unbounded.inners) [setattr(obj, 'mesh', self.mesh) for obj in chain(hole_mesh.points, hole_mesh.hedges, hole_mesh.faces)] self.mesh.points.extend(hole_mesh.points) self.mesh.hedges.extend(hole_mesh.hedges) self.mesh.faces.extend(hole_mesh.faces) for start_hedge in hole_mesh.unbounded.inners: for hedge in start_hedge.loop_hedges: hedge.face = self def check_mesh(self): # Ensure that the face belongs to a mesh if not self.mesh: raise AttributeError("This attribute is not available till the face does have link to a mesh")
Class variables
var accuracy
Instance variables
var inners
-
Expand source code
@property def inners(self): # it would be nice to check whether all element of a list are correct, don't know how to do return self._inners
var is_unbounded
-
Expand source code
@property def is_unbounded(self): # returns true if face is boundless - includes all other faces of a mesh return not self.outer
var outer
-
Expand source code
@property def outer(self): return self._outer
Methods
def check_mesh(self)
-
Expand source code
def check_mesh(self): # Ensure that the face belongs to a mesh if not self.mesh: raise AttributeError("This attribute is not available till the face does have link to a mesh")
def insert_holes(self, sv_verts, sv_faces, face_selection=None, face_data=None)
-
Expand source code
def insert_holes(self, sv_verts, sv_faces, face_selection=None, face_data=None): # not sure super useful, holes should not intersect with the face self.check_mesh() hole_mesh = generate_dcel_mesh(self.mesh, sv_verts, sv_faces, face_selection, face_data=face_data, new_mesh=True) self.inners.extend(hole_mesh.unbounded.inners) [setattr(obj, 'mesh', self.mesh) for obj in chain(hole_mesh.points, hole_mesh.hedges, hole_mesh.faces)] self.mesh.points.extend(hole_mesh.points) self.mesh.hedges.extend(hole_mesh.hedges) self.mesh.faces.extend(hole_mesh.faces) for start_hedge in hole_mesh.unbounded.inners: for hedge in start_hedge.loop_hedges: hedge.face = self
class HalfEdge (mesh, point, face=None)
-
Expand source code
class HalfEdge: accuracy = 1e-5 def __init__(self, mesh, point, face=None): self.mesh = mesh # can be just None but in this case some method weren't be available self.origin = point self.face = face self.twin = None self.next = None self.last = None self.left = None # Information about nearest left neighbour for hole detection, intersection algorithm user self.flags = set() # For any value which an algorithm would like to keep with object, only add or remove self._slop = None # Should be recalculated if origin to origin of twin are changed def __str__(self): return "Hedge" @property def ccw_hedges(self): # returns hedges originated in one point if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.last.twin counter = 0 while next_edge != self: yield next_edge next_edge = next_edge.last.twin counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self)) @property def cw_hedges(self): # returns hedges originated in one point if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.twin.next counter = 0 while next_edge != self: yield next_edge next_edge = next_edge.twin.next counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self)) @property def loop_hedges(self): # returns hedges bounding face if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.next counter = 0 while id(next_edge) != id(self): yield next_edge try: next_edge = next_edge.next except AttributeError: raise AttributeError(' Some of half edges has incomplete data (does not have link to next half edge)') counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self)) @property def slop(self): """ Returns dot product of direction of half edges and -X direction in ccw order in such way Angle 45 from -X direction in ccw order returns 0.707 Angle 90 from -X direction in ccw order returns 1.0 Angle 180 from -X direction in ccw order returns 2.0 Angle 360 or 0 from -X direction in ccw order returns 4.0 :return: float """ if self._slop: pass elif self.twin._slop: self._slop = (self.twin._slop + 2) % 4 if self.twin._slop != 2 else 4 # corner case for horizontal elif almost_equal(self.origin.co[y], self.twin.origin.co[y], self.accuracy): # is horizontal if is_more(self.origin.co[x], self.twin.origin.co[x], self.accuracy): self._slop = 4.0 else: self._slop = 2.0 else: direction = (self.twin.origin - self.origin).normalize() product = dot_product(direction.co, (1, 0)) self._slop = product + 1 if direction.co[y] < 0 else 3 - product return self._slop
Subclasses
Class variables
var accuracy
Instance variables
var ccw_hedges
-
Expand source code
@property def ccw_hedges(self): # returns hedges originated in one point if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.last.twin counter = 0 while next_edge != self: yield next_edge next_edge = next_edge.last.twin counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self))
var cw_hedges
-
Expand source code
@property def cw_hedges(self): # returns hedges originated in one point if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.twin.next counter = 0 while next_edge != self: yield next_edge next_edge = next_edge.twin.next counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self))
var loop_hedges
-
Expand source code
@property def loop_hedges(self): # returns hedges bounding face if not self.mesh: raise AttributeError("This method doesn't work with hedges({}) without link to a mesh." "Besides, mesh object should have proper number of half edges " "in hedges list".format(self)) yield self next_edge = self.next counter = 0 while id(next_edge) != id(self): yield next_edge try: next_edge = next_edge.next except AttributeError: raise AttributeError(' Some of half edges has incomplete data (does not have link to next half edge)') counter += 1 if counter > len(self.mesh.hedges): raise RecursionError('Hedge - {} does not have a loop'.format(self))
var slop
-
Returns dot product of direction of half edges and -X direction in ccw order in such way Angle 45 from -X direction in ccw order returns 0.707 Angle 90 from -X direction in ccw order returns 1.0 Angle 180 from -X direction in ccw order returns 2.0 Angle 360 or 0 from -X direction in ccw order returns 4.0 :return: float
Expand source code
@property def slop(self): """ Returns dot product of direction of half edges and -X direction in ccw order in such way Angle 45 from -X direction in ccw order returns 0.707 Angle 90 from -X direction in ccw order returns 1.0 Angle 180 from -X direction in ccw order returns 2.0 Angle 360 or 0 from -X direction in ccw order returns 4.0 :return: float """ if self._slop: pass elif self.twin._slop: self._slop = (self.twin._slop + 2) % 4 if self.twin._slop != 2 else 4 # corner case for horizontal elif almost_equal(self.origin.co[y], self.twin.origin.co[y], self.accuracy): # is horizontal if is_more(self.origin.co[x], self.twin.origin.co[x], self.accuracy): self._slop = 4.0 else: self._slop = 2.0 else: direction = (self.twin.origin - self.origin).normalize() product = dot_product(direction.co, (1, 0)) self._slop = product + 1 if direction.co[y] < 0 else 3 - product return self._slop
class Point (mesh, co)
-
Expand source code
class Point: accuracy = 1e-5 def __init__(self, mesh, co): self.mesh = mesh self.co = co self.hedge = None def __str__(self): return "Point" def __add__(self, other): # should returns object with class of inputs object # will be an error if class of input object has other init arguments return self.__class__(None, tuple(co1 + co2 for co1, co2 in zip(self.co, other.co))) def __sub__(self, other): # should returns object with class of inputs object # will be an error if class of input object has other init arguments return self.__class__(None, tuple(co1 - co2 for co1, co2 in zip(self.co, other.co))) def __mul__(self, other): # returns dot product or multiplication vector to scalar if isinstance(other, self.__class__): return dot_product(self.co, other.co) else: return self.__class__(None, [co * other for co in self.co]) def length(self): # returns length as vector return sum([co ** 2 for co in self.co]) ** 0.5 def normalize(self): # making the length of the vector 1.0 mem_len = self.length() self.co = (self.co[0] / mem_len, self.co[1] / mem_len, self.co[2] / mem_len) return self def cross_product(self, other): return self.__class__(None, cross_product(self.co, other.co))
Subclasses
Class variables
var accuracy
Methods
def cross_product(self, other)
-
Expand source code
def cross_product(self, other): return self.__class__(None, cross_product(self.co, other.co))
def length(self)
-
Expand source code
def length(self): # returns length as vector return sum([co ** 2 for co in self.co]) ** 0.5
def normalize(self)
-
Expand source code
def normalize(self): # making the length of the vector 1.0 mem_len = self.length() self.co = (self.co[0] / mem_len, self.co[1] / mem_len, self.co[2] / mem_len) return self