In [1]:
from collections import defaultdict
Each triangle is stored as a tuple of its vertices. Each vertex is labeled with a non negative integer.
A triangulation of a surface is stored as a simple graph. Each triangle in the triangulation is stored in the corresponding node in the graph. Two nodes in the graph are connected when the triangles stored inside them share a common edge.
Remark: a graph is cubic if a triangulated surface is 2D manifold without a boundary.
In [2]:
class Triangle:
"""
A triangle is represented as a list of its
vertices (labeled with natural numbers).
"""
def __init__(self, vertices, neighbours=None):
assert len(vertices) == 3, 'A triangle should have 3 vertices'
self.vertices = sorted(vertices)
self.neighbours = neighbours if neighbours is not None else set()
@property
def edges(self):
"""
Return the generator of edges 'in the positive direction'.
"""
v, n = self.vertices, len(self.vertices)
return ((v[i], v[(i + 1) % n]) for i in range(n))
def contains_edge(self, edge):
'''
Return True if the triangle contains the given edge.
'''
return set(edge) <= set(self.vertices)
def neighbour_edge(self, edge):
'''
Return the neighbouring triangle that shared the edge with out triangle.
If no such neighbouring triangle exists an Exception is raised.
'''
neighbour = [n for n in self.neighbours if n.contains_edge(edge)]
assert len(neighbour) == 1
return neighbour[0]
Example triangulations: a set of triangles that represents a torus and a projective plane.
In [3]:
torus_triangles = [
(0, 1, 6), (0, 4, 6), (1, 6, 7), (1, 2, 7), (2, 7, 4), (2, 0, 4), (4, 5, 8), (4, 6, 8), (6, 3, 8),
(6, 7, 3), (3, 7, 5), (7, 4, 5), (0, 1, 5), (1, 5, 8), (1, 2, 8), (2, 8, 3), (2, 0, 3), (0, 5, 3),
]
projective_plane_triangles = [
(1, 2, 7), (1, 7, 3), (2, 7, 5), (7, 3, 6), (2, 3, 5), (4, 5, 7),
(7, 4, 6), (2, 6, 3), (4, 5, 3), (4, 6, 2), (1, 3, 4), (4, 1, 2),
]
First we create a graph from the set of triangles. No ordered structure is computed here.
In [4]:
def create_triangulation(triangles):
'''
Create a triangulation from a set of triangles.
'''
n = max([max(triangle) for triangle in triangles]) + 1
triangles = [Triangle(triangle) for triangle in triangles]
# For each vertex compute the set of triangles containing the vertex.
vertex_triangles = defaultdict(set)
for triangle in triangles:
for vertex in triangle.vertices:
vertex_triangles[vertex].add(triangle)
# Make connection between neighbourhood triangles.
for triangle in triangles:
for (v1, v2) in triangle.edges:
neighbour = (vertex_triangles[v1] & vertex_triangles[v2]).difference((triangle,))
assert len(neighbour) == 1
triangle.neighbours.add(neighbour.pop())
# Each triangle should have exactly 3 neighbours (2D manifold without a boundary)
assert len(triangle.neighbours) == 3
return triangles
An oriented version of a triangle is represented as a tuple (triangle, orientation). In it we define helper functions enext and fnext.
In [5]:
class OrientedTriangle:
def __init__(self, triangle, orientation):
self.triangle = triangle
self.orientation = orientation
@property
def is_positively_oriented(self):
return self.orientation in [0, 1, 2]
@property
def get_leading_edge(self):
edge = list(self.triangle.edges)[self.orientation & 3]
return edge if self.is_positively_oriented else tuple(reversed(edge))
@property
def enext(self):
"""
Get the next orientation of the same type.
"""
orientation = (((self.orientation & 3) + 1) % 3) + (self.orientation & 4)
return OrientedTriangle(self.triangle, orientation)
@property
def fnext(self):
'''
Compute return the value of fnext for the oriented triangle.
'''
reversed_edge = tuple(reversed(self.get_leading_edge))
# Get the neighbour that shares the leading edge with our triangle.
neighbour = self.triangle.neighbour_edge(reversed_edge)
return OrientedTriangle(neighbour, 0).orient_triangle(reversed_edge)
def same_orientation(self, triangle):
return self.is_positively_oriented == triangle.is_positively_oriented
def orient_triangle(self, edge):
"""
Orient the triangle so that the given edge is its lead edge. Return self.
"""
es, re = list(self.triangle.edges), tuple(reversed(edge))
self.orientation = es.index(edge) if edge in es else es.index(re) + 4
return self
Finally: a function which checks whether a given triangulation is orientable.
In [6]:
def orientable(triangles):
'''
Is a triangulation orientable?
'''
return is_orientable(OrientedTriangle(create_triangulation(triangles)[0], 0))
def is_orientable(oriented_triangle):
triangle = oriented_triangle.triangle
chosen_orientation = getattr(triangle, 'orientation', None)
if chosen_orientation is None:
triangle.orientation = oriented_triangle.orientation
t1 = oriented_triangle.fnext
t2 = oriented_triangle.enext.fnext
t3 = oriented_triangle.enext.enext.fnext
return is_orientable(t1) and is_orientable(t2) and is_orientable(t3)
else:
return oriented_triangle.same_orientation(OrientedTriangle(triangle, chosen_orientation))
In [7]:
orientable(torus_triangles)
Out[7]:
In [8]:
orientable(projective_plane_triangles)
Out[8]: