Extrude a concave, complex polygon in PyVista - python

I wish to take a concave and complex (containing holes) polygon and extrude it 'vertically' into a polyhedron, purely for visualisation. I begin with a shapely Polygon, like below:
poly = Polygon(
[(0,0), (10,0), (10,10), (5,8), (0,10), (1,7), (0,5), (1,3)],
holes=[
[(2,2),(4,2),(4,4),(2,4)],
[(6,6), (7,6), (6.5,6.5), (7,7), (6,7), (6.2,6.5)]])
which I correctly plot (reorientating the exterior coordinates to be clockwise, and the hole coordinates to be counterclockwise) in matplotlib as:
I then seek to render this polygon extruded out-of-the-page (along z), using PyVista. There are a few hurdles; PyVista doesn't directly support concave (nor complex) input to its PolyData type. So we first create an extrusion of simple (hole-free) concave polygons, as per this discussion.
def extrude_simple_polygon(xy, z0, z1):
# force counter-clockwise ordering, so PyVista interprets polygon correctly
xy = _reorient_coords(xy, clockwise=False)
# remove duplication of first & last vertex
xyz0 = [(x,y,z0) for x,y in xy]
if (xyz0[0] == xyz0[-1]):
xyz0.pop()
# explicitly set edge_source
base_vert = [len(xyz0)] + list(range(len(xyz0)))
base_data = pyvista.PolyData(xyz0, base_vert)
base_mesh = base_data.delaunay_2d(edge_source=base_data)
vol_mesh = base_mesh.extrude((0, 0, z1-z0), capping=True)
# force triangulation, so PyVista allows boolean_difference
return vol_mesh.triangulate()
Observe this works when extruding the outer polygon and each of its internal polygons in-turn:
extrude_simple_polygon(list(poly.exterior.coords), 0, 5).plot()
extrude_simple_polygon(list(poly.interiors[0].coords), 0, 5).plot()
extrude_simple_polygon(list(poly.interiors[1].coords), 0, 5).plot()
I reasoned that to create an extrusion of the original complex polygon, I could compute the boolean_difference. Alas, the result of
outer_vol = extrude_simple_polygon(list(poly.exterior.coords), 0, 5)
for hole in poly.interiors:
hole_vol = extrude_simple_polygon(list(hole.coords), 0, 5)
outer_vol = outer_vol.boolean_difference(hole_vol)
outer_vol.plot()
is erroneous:
The doc advises to inspect the normals via plot_normals, revealing that all extruded volumes have inward-pointing (or else, unexpected) normals:
The extrude doc mentions nothing of the extruded surface normals nor the original object (in this case, a polygon) orientation.
We could be forgiven for expecting our polygons must be clockwise, so we set clockwise=True in the first line of extrude_simple_polygon and try again. Alas, PolyData now misinterprets our base polygon; calling base_mesh.plot() reveals (what should look like our original blue outer polygon):
with extrusion
Does PyVista always expect counter-clockwise polygons?
Why does extrude create volumes with inward-pointing surface normals?
How can I correct the extruded surface normals?
Otherwise, how can I make PyVista correctly visualise what should be an incredibly simply-extruded concave complex polygon??

You're very close. What you have to do is use a single call to delaunay_2d() with all three polygons (i.e. the enclosing one and the two holes) as edge source (loop source?). It's also important to have faces (rather than lines) from each polygon; this is what makes it possible to enforce the holeyness of the holes.
Here's a complete example for your input (where I manually flipped the orientation of the holes; you seem to have a _reorient_coords() helper that you should use instead):
import pyvista as pv
# coordinates of enclosing polygon
poly_points = [
(0, 0), (10, 0), (10, 10), (5, 8), (0, 10), (1, 7), (0, 5), (1, 3),
]
# hole point order hard-coded here; use your _reorient_coords() function
holes = [
[(2, 2), (4, 2), (4, 4), (2, 4)][::-1],
[(6, 6), (7, 6), (6.5, 6.5), (7, 7), (6, 7), (6.2, 6.5)][::-1],
]
z0, z1 = 0.0, 5.0
def is_clockwise(xy):
value = 0
for i in range(len(xy)):
x1, y1 = xy[i]
x2, y2 = xy[(i+1)%len(coords)]
value += (x2-x1)*(y2+y1)
return (value > 0)
def reorient_coords(xy, clockwise):
if is_clockwise(xy) == clockwise:
return xy
return xy[::-1]
def points_2d_to_poly(xy, z, clockwise):
"""Convert a sequence of 2d coordinates to a polydata with a polygon."""
# ensure vertices are currently ordered without repeats
xy = reorient_coords(xy, clockwise)
if xy[0] == xy[-1]:
xy = xy[:-1]
xyz = [(x,y,z) for x,y in xy]
faces = [len(xyz), *range(len(xyz))]
data = pv.PolyData(xyz, faces=faces)
return data
# bounding polygon
polygon = points_2d_to_poly(poly_points, z0)
# add all holes
for hole_points in holes:
polygon += points_2d_to_poly(hole_points, z0)
# triangulate poly with all three subpolygons supplying edges
# (relative face orientation is critical here)
polygon_with_holes = polygon.delaunay_2d(edge_source=polygon)
# extrude
holey_solid = polygon_with_holes.extrude((0, 0, z1 - z0), capping=True)
holey_solid.plot()
Here's the top view of the polygon pre-extrusion:
plotter = pv.Plotter()
plotter.add_mesh(polygon_with_holes, show_edges=True, color='cyan')
plotter.view_xy()
plotter.show()

Related

PyBox2d: Change vertices of polygon during simulation

Currently I define a polygon in my simulation as follows
from Box2D.examples.framework import (Framework, main)
from Box2D import (b2CircleShape, b2EdgeShape, b2FixtureDef, b2PolygonShape)
vertices = [(-1, -1), (-1, 1), (1, 1), (1, -1)]
self.polygon = self.world.CreateDynamicBody(
bullet=True,
position=(1, 1),
fixtures=b2FixtureDef(shape=b2PolygonShape(vertices=vertices),
density=1),
linearVelocity=(1, 1))
I want to change the size of the polygon while the simulation is running. I tried the following
self.polygon.fixtures[0] = b2FixtureDef(shape=b2PolygonShape(vertices=new_vertices),
density=self.density)
which has no effect.
Do I have to delete the object to change the vertices? Or is there a way to change the vertices during the simulation?

Graph isomorphism with constraints on the edges using networkx

I would like to define my own isomorphism of two graphs. I want to check if two graphs are isomorphic given that each edge has some attribute --- basically the order of placing each edge. I wonder if one can use the method:
networkx.is_isomorphic(G1,G2, edge_match=some_callable)
somehow by defining function some_callable().
For example, the following graphs are isomorphic, because you can relabel the nodes to obtain one from another.
Namely, relabel [2<->3].
But, the following graphs are not isomorphic.
There is no way to obtain one from another by re-labeling the nodes.
Here you go. This is exactly what the edge_match option is for doing. I'll create 3 graphs the first two are isomorphic (even though the weights have different names --- I've set the comparison function to account for that). The third is not isomorphic.
import networkx as nx
G1 = nx.Graph()
G1.add_weighted_edges_from([(0,1,0), (0,2,1), (0,3,2)], weight = 'aardvark')
G2 = nx.Graph()
G2.add_weighted_edges_from([(0,1,0), (0,2,2), (0,3,1)], weight = 'baboon')
G3 = nx.Graph()
G3.add_weighted_edges_from([(0,1,0), (0,2,2), (0,3,2)], weight = 'baboon')
def comparison(D1, D2):
#for an edge u,v in first graph and x,y in second graph
#this tests if the attribute 'aardvark' of edge u,v is the
#same as the attribute 'baboon' of edge x,y.
return D1['aardvark'] == D2['baboon']
nx.is_isomorphic(G1, G2, edge_match = comparison)
> True
nx.is_isomorphic(G1, G3, edge_match = comparison)
> False
Here answer the problem specifically in the question, with the very same graphs. Note that I'm using the networkx.MultiGraph and consider some 'ordering' in placing those edges.
import networkx as nx
G1,G2,G3,G4=nx.MultiGraph(),nx.MultiGraph(),nx.MultiGraph(),nx.MultiGraph()
G1.add_weighted_edges_from([(0, 1, 0), (0, 2, 1), (0, 3, 2)], weight='ordering')
G2.add_weighted_edges_from([(0, 1, 0), (0, 3, 1), (0, 2, 2)], weight='ordering')
G3.add_weighted_edges_from([(0, 1, 0), (0, 1, 1), (2, 3, 2)], weight='ordering')
G4.add_weighted_edges_from([(0, 1, 0), (2, 3, 1), (0, 1, 2)], weight='ordering')
def comparison(D1,D2):
return D1[0]['ordering'] == D2[0]['ordering']
nx.is_isomorphic(G1,G2, edge_match=comparison)
>True
nx.is_isomorphic(G3,G4, edge_match=comparison)
>False

Order of vertex for drawing quad

I need to draw n planes in 3D space. The quads are planes created from two points, and with an algorithm I get 4 vertex for drawing the quad. The problem I have is that the order of the vertices affect to the result, obviously. Here it is what I mean:
But when the plane is horizontal instead of vertical:
I can imagine two possible solutions. Using triangles and combining them or ordering the vertex properly. I dont know how to do the second idea. I have tried using triangles but I get the same problem.
# self.planos = [('A', (500, 500, 10), (-500, 500, 10), (-500, -500, 10), (500, -500, 10))] for horizontal
# self.planos = [('A', (-500, 10, 500), (500, 10, 500), (-500, 10, -500), (500, 10, -500))] for vertical
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDepthMask(GL_FALSE)
glBegin(GL_QUADS)
glColor(0.5, 0.5, 0.1, 0.5)
for i in range(len(self.planos)):
glVertex(self.planos[i][1][0], self.planos[i][1][2], self.planos[i][1][1])
glVertex(self.planos[i][2][0], self.planos[i][2][2], self.planos[i][2][1])
glVertex(self.planos[i][3][0], self.planos[i][3][2], self.planos[i][3][1])
glVertex(self.planos[i][4][0], self.planos[i][4][2], self.planos[i][4][1])
glEnd()
glDepthMask(GL_TRUE)
glDisable(GL_BLEND)
Intersection code for getting four vertex to draw planes:
In init method:
#Vertices of the cube
self.v = (Point3D(500, 500, 500), Point3D(-500, 500, 500), Point3D(-500, -500, 500),
Point3D(500, -500, 500), Point3D(500, 500, -500), Point3D(-500, 500, -500),
Point3D(-500, -500, -500), Point3D(500, -500, -500))
# Edges of the cube
self.a = (Segment3D(self.v[0], self.v[1]), Segment3D(self.v[1], self.v[2]),
Segment3D(self.v[2], self.v[3]), Segment3D(self.v[3], self.v[0]),
Segment3D(self.v[0], self.v[4]), Segment3D(self.v[1], self.v[5]),
Segment3D(self.v[2], self.v[6]), Segment3D(self.v[3], self.v[7]),
Segment3D(self.v[4], self.v[5]), Segment3D(self.v[5], self.v[6]),
Segment3D(self.v[6], self.v[7]), Segment3D(self.v[7], self.v[4]))
# Algorithm for getting 4 points
def plano_limites(self, point1, point2, point3):
plano = Plane(Point3D(point1), Point3D(point2), Point3D(point3))
good = []
for i in range(12):
a = intersection(plano, self.a[i]) # Sympy intersection
if a:
good.append(a[0])
return good
First of all, be aware that your intersection can result in more or less than four vertices. But since the region will always be convex, you can simply draw it with a triangle fan.
To sort your vertices, you need the normal n of the plane and the centroid c of all the vertices v_i:
c = 1/(number of vertices) * (v_1 + v_2 + v3 + ...)
Then, we need a coordinate system whose z-axis is the normal. For this, we can simply define an arbitrary other direction vector d and define x = normalize(cross(d, normal)), y = cross(normal, x). For cases where d conincides with normal, we need an alternative d.
We can then calculate a representative angle of any vertex in this coordinate system:
angle_i = atan2(dot(x, v_i - c), dot(y, v_i - c))
Sort by this angle and you are done.
Thanks to Nico's answer I was able to learn how to do this for myself but I thought it would be useful to do a full write up as it still took me further learning to understand what was going on here.
We essentiality need to find an x- and y-axis where the z-axis is aligned with the normal, we do this by taking the cross product of an arbitrary vector other. If the other vector coincides with the normal we will need to use a different vector. Two vectors coincide if their dot product is equal to 1.
By using the other vector of (0, 1, 0) (or (0, 0, -1) in the case they coincide) in a right-hand coordinate system we will produce easier to understand x- and y-axis vectors. This does not matter for the sake of the algorithm but I found this was the most crucial part initially missing in my own understanding.
By using the other vector of (0, 1, 0) when the normal of the plane is (0, 0, 1) the x-axis will be (1, 0, 0) and the y-axis will be (0, 1, 0).
By using the other vector of (0, 0, -1) when the normal of the plane is (0, 1, 0) the x-axis will be (1, 0, 0) and the y-axis will be (-1, 0, 0).
This can easily be modeled by using your right hand and turning your finger that is the normal to point up to positive z-axis (towards yourself).
def order_on_plane(vertices, normal):
# Find the centroid of the vertices
centroid = (0, 0, 0)
for v in vertices:
centroid = add(centroid, v)
centroid = scale(centroid, 1 / len(vertices))
# Determine the 'other' vector, used to create the axis vectors
other = (0, 1, 0)
# If the other vector coincides with the normal vector, we need to use a different other
if math.fabs(math.fabs(dot(other, normal)) - 1.0) < 0.0001:
other = (0, 0, -1)
# Create the axis vectors
x_axis = normalize(cross(other, normal))
y_axis = normalize(cross(normal, x_axis))
# Sort by angle as a vector from the centroid
angles = []
for v in vertices:
vector = sub(v, centroid)
x_pos = dot(vector, x_axis)
y_pos = dot(vector, y_axis)
# y_pos is passed in first for east counter clockwise convention
angle = math.atan2(y_pos, x_pos)
angles.append(angle)
# Sort vertices by angle
vertices = sorted(zip(angles, vertices), key=lambda x: x[0])
vertices = list(map(lambda x: x[1], vertices))
return vertices

Flip coordinates as if they were a grid

I have been at it for a long time but can't finish what I'm trying to do. I am trying to find a way to flip coordinates based on a list of objects and a list of their coordinates.
objects = [1, 2, 3, 4, 5]
grid_placement = [(0,0), (32, 0), (64, 0), (0, -32), (0, -64)]
I want to get the flipped axis coordinates of where they are
So the ideal flipped Y output would essentially be:
placement = [(0, -64), (32, -64), (64, -64), (0, -32), (0, 0)]
And the ideal flipped X output would be:
placement = [(64, 0), (32, -64), (0,0), (64, -32), (64, -64)]
So essentially if something is in the top right, it would be on the top left after flipping on X. Their index positions would remain the same in the list, but with the altered coordinates.
The code I have cobbled together works just fine for the Y axis, but I cannot get the X axis working. My brain is a little fried from messing with this for a few hours, any outside advice would be highly appreciated.
Here is my code (messy)
gridSize = 32
if axis == "x":
columns = defaultdict(dict)
for gridOffset in self.offsetList:
row = gridOffset[1] // gridSize
col = gridOffset[0] // gridSize
columns[col][gridOffset] = row
new_order = []
order = list(reversed(list(columns.keys())))
for col in order:
for offset in self.offsetList:
if offset in columns[col]:
new_order.append((col * gridSize, columns[col][offset] * gridSize))
elif axis == "y":
rows = defaultdict(dict)
for gridOffset in self.offsetList:
row = gridOffset[1] // gridSize
col = gridOffset[0] // gridSize
rows[row][gridOffset] = col
new_order = []
order = list(reversed(list(rows.keys())))
for offset in self.offsetList:
for row in order:
if offset in rows[row]:
new_order.append((rows[row][offset] * gridSize, row * -gridSize))
self.offsetList = new_order
Your examples suggest that you want to swap object coordinates rather than "flip" them. To actually flip on a Cartesian plane you would need a pivot point. If that point is (0,0) flipping would amount to inverting the sign of the X or Y coordinate you want to flip.
On the other hand if you want to swap coordinates between objects, use zip:
# vertical swap
grid_placement = [(F[0],B[1]) for F,B in zip(grid_placement, grid_placement[::-1])
# horizontal swap
grid_placement = [(F[1],B[0]) for F,B in zip(grid_placement, grid_placement[::-1])

How to determine corner/vertex cells in an arbitrary shape composed of grid cells

I am dealing with polygons composed of square tiles on a 2D grid. A polygon is simply stored as a list of tuples, with each tuple representing the coordinates of a tile. The polygons are always contiguous and have no holes.
What I want to be able to do is determine which of the tiles represent vertices along the border of the polygon, such that later I could trace between each one to produce the polygon's border, or determine the distance between two consecutive vertices to find the length of a side, etc.
Here is an example of a polygon (a 5x4 rectangle with a 3x2 rectangle subtracted from the top left, producing a backward 'L'):
polygon_tiles = [(3, 0), (4, 0), (3, 1), (4, 1), (0, 2), (1, 2), (2, 2), (3, 2),
(4, 2), (0, 3), (1, 3), (2, 3), (3, 3), (4, 3)]
Ideally the algorithm I am seeking would produce a result that looked like this:
polygon_verts = [(3, 0), (4, 0), (4, 3), (0, 3), (0, 2), (3, 2)]
with the vertices listed in order tracing around the border clockwise.
Just fiddling around with some test cases, this problem seems to be much more complicated than I would have thought, especially in weird circumstances like when a polygon has a 1-tile-wide extrusion (in this case one of the tiles might have to be stored as a vertex twice??).
I'm working in Python, but any insight is appreciated, even if it's in pseudocode.
Assuming your shape has no internal holes.
Find the topmost row. Pick the leftmost tile of this row. This guarantees we begin on a corner.
From this tile, attempt to go straight right If you can't, go straight downright, straight down, etc until you have picked a direction. This guarnatees we can trace a clockwise perimeter of the polygon
Continue to take steps in your chosen direction. After each step:
If the next step would be onto a tile, rotate counterclockwise and look again.
If the next step would be onto an empty space, rotate clockwise and look again.
Stop rotating once you have moved onto empty space and back onto a tile again.
If we rotated from the initial direction, we must be standing on a vertex. Mark it as such.
Mark every other tile you traverse as being part of the edge.
Keep walking the edge until you arrive at your initial tile. You may walk over tiles more than once in the case of 1 tile extrusions.
If this algorithm doesn't make sense in your head, try getting out some paper and following it by hand :)
This problem is a convex hull variation, for which e.g. the gift wrapping algorithm could be applied. The constraints of discrete coordinates and line directions lead to simplifications. Here is some python code that gives the desired answer (Patashu's answer is in the same spirit):
#!/usr/bin/python
import math
def neighbors(coord):
for dir in (1,0):
for delta in (-1,1):
yield (coord[0]+dir*delta, coord[1]+(1-dir)*delta)
def get_angle(dir1, dir2):
angle = math.acos(dir1[0] * dir2[0] + dir1[1] * dir2[1])
cross = dir1[1] * dir2[0] - dir1[0] * dir2[1]
if cross > 0:
angle = -angle
return angle
def trace(p):
if len(p) <= 1:
return p
# start at top left-most point
pt0 = min(p, key = lambda t: (t[1],t[0]))
dir = (0,-1)
pt = pt0
outline = [pt0]
while True:
pt_next = None
angle_next = 10 # dummy value to be replaced
dir_next = None
# find leftmost neighbor
for n in neighbors(pt):
if n in p:
dir2 = (n[0]-pt[0], n[1]-pt[1])
angle = get_angle(dir, dir2)
if angle < angle_next:
pt_next = n
angle_next = angle
dir_next = dir2
if angle_next != 0:
outline.append(pt_next)
else:
# previous point was unnecessary
outline[-1]=pt_next
if pt_next == pt0:
return outline[:-1]
pt = pt_next
dir = dir_next
polygon_tiles = [(3, 0), (4, 0), (3, 1), (4, 1), (0, 2), (1, 2), (2, 2), (3, 2),
(4, 2), (0, 3), (1, 3), (2, 3), (3, 3), (4, 3)]
outline = trace(polygon_tiles)
print(outline)
I would just calculate the slopes of the lines between the vertices
# Do sort stuff
vertices = []
for position, polygon in enumerate(polygon_tiles):
# look for IndexErrors
try:
polygon_tiles[position+1]
except IndexError:
break
try:
polygon_tiles[position+2]
except IndexError:
# Bad practice
position = position - 1
# calculate the slope of the line between of vertex 1 and vertex 2
s1 = (polygon_tiles[position+1][1] - polygon[1]) / (polygon_tiles[position+1][0] - polygon[0])
# calculate the slope of vertex 2 and vertex 3
s2 = (polygon_tiles[position+2][1] - polygon_tiles[position+1][1]) / (polygon_tiles[position+2][0] - polygon_tiles[position+1][0])
# if the slopes differ then you have a vertex
if d1 != d2:
vertices.append(polygon_tiles[position+1])

Categories