2

How can I find angles between the edges in a face, and then make a loop for all faces in an object.

I know I can set it up in a blender and just read it, but I'd like to send these angles to a text file. How to do it with Python

Kolkornik
  • 33
  • 3

3 Answers3

6

Project into 2D

The verts and edges are wound in order. Knowing the face normal can walk around the edges in a face, using the vector of the "incoming" edge, and vector of "outgoing" edge can calculate the internal angle as as outlined in Finding Internal Angles of a Polygon Blender winds its faces in a counter-clockwise direction.

IMO other answers fail to take this into account, see below

Blender has an angle_signed method for 2d vectors. To use here may require having an arbitrary "Greenwich" point as a reference.

  • If not colinear, The three verts of the 2 edges form a plane

  • The corner of 2 edges is the middle vert (b) Subtract this from other vert of each edge (new origin)

  • The cross product of the two edges is the axis of rotation (normal to plane)

  • Change the space to project (rotate) corner plane into XY plane to represent the corner edges as 2D xy vectors.

Test script. By way of point of difference have used an edit mode bmesh. In edit mode, select the faces you wish to see the internal angles of, then run script.

import bpy
from mathutils import Matrix, Vector
from bpy import context
from math import degrees, atan2, pi
import bmesh
# project into XY plane, 
up = Vector((0, 0, 1))

ob = context.object me = ob.data bm = bmesh.from_edit_mesh(me) def edge_angle(e1, e2, face_normal): b = set(e1.verts).intersection(e2.verts).pop() a = e1.other_vert(b).co - b.co c = e2.other_vert(b).co - b.co a.negate()
axis = a.cross(c).normalized() if axis.length < 1e-5: return pi # inline vert

if axis.dot(face_normal) &lt; 0:
    axis.negate()
M = axis.rotation_difference(up).to_matrix().to_4x4()  

a = (M @ a).xy.normalized()
c = (M @ c).xy.normalized()

return pi - atan2(a.cross(c), a.dot(c))

selected_faces = [f for f in bm.faces if f.select] for f in selected_faces: edges = f.edges[:] print("Face", f.index, "Edges:", [e.index for e in edges]) edges.append(f.edges[0])

for e1, e2 in zip(edges, edges[1:]):

    angle = edge_angle(e1, e2, f.normal)
    print(&quot;Edge Corner&quot;, e1.index, e2.index, &quot;Angle:&quot;, degrees(angle))

Test run on concave ngon

enter image description here 2x2 grid center vert dissolved. Bottom vert moved to origin making a 270 degree internal angle between edges 5 and 1

Face 0 Edges: [1, 6, 2, 7, 3, 4, 0, 5]
Edge Corner 1 6 Angle: 45.0
Edge Corner 6 2 Angle: 180.0
Edge Corner 2 7 Angle: 90.0
Edge Corner 7 3 Angle: 180.0
Edge Corner 3 4 Angle: 90.0
Edge Corner 4 0 Angle: 180.0
Edge Corner 0 5 Angle: 45.0
Edge Corner 5 1 Angle: 270.0

For file IO consult the python docs. (Or see other answers).

Re other answers, angles when run on ngon above

@lemon

[135.00000034162267, 0.0, 90.00000250447816, 0.0, 90.00000250447816, 0.0, 135.00000034162267, 90.00000250447816]

@MohammadHosseinJamshidi

[45.00000125223908, 90.00000250447816, 180.00000500895632, 90.00000250447816, 180.00000500895632, 90.00000250447816, 180.00000500895632, 45.00000125223908]
batFINGER
  • 84,216
  • 10
  • 108
  • 233
3

You can also (more directly):

import bpy
from mathutils import Vector
import csv

def get_face_angles(obj, poly): # Get vertices vertices = [obj.data.vertices[i] for i in poly.vertices] # Append first and second to loop back from the last ones vertices.append(vertices[0]) vertices.append(vertices[1]) # Get angle triplets triplets = [(v1, v2, v3) for v1, v2, v3 in zip(vertices, vertices[1:], vertices[2:])] # Keep elements and get angles return [(poly, v1, v2, v3, (v2.co - v1.co).angle(v3.co - v2.co)) for v1, v2, v3 in triplets]

def get_faces_angles(obj): # Loop over the polygons and get the results results = [] for p in obj.data.polygons: results.extend(get_face_angles(obj, p)) return results

def save_to_file(path, angles): with open(path, 'w', newline = "") as f: writer = csv.writer(f) for a in angles: #Five columns: face index, three vertex indices, angle writer.writerow([a[0].index, a[1].index, a[2].index, a[3].index, a[4]])

obj = bpy.context.object

angles = get_faces_angles(obj)

save_to_file("your file name", angles)

lemon
  • 60,295
  • 3
  • 66
  • 136
  • This is not what wanted. 1) It gives unknown triangles angles which share common vertices with polygon (if a polygon wont be single-planar, your triangles may not match with real model's triangles). 2) the question is about edges of polygons not triangles (for example a 6-gon or so)- in this way for polys with more than 3-edges you always calculate smaller angles – MohammadHossein Jamshidi Nov 27 '20 at 10:21
  • @MohammadHosseinJamshidi, I could be wrong but could you provide a concrete example? – lemon Nov 27 '20 at 10:28
  • I even learned something from your code so please accept my thanks. example: consider a flat 6-gon . move 1-vertex a little up. blender itself creates its own triangles. now if you connect every permutation of 3-vertices and calculate angles, you have (also calculated whats needed but) created triangles which are not even blender's triangles. they may not match. if there's a need for a picture please tell me. – MohammadHossein Jamshidi Nov 27 '20 at 10:39
  • @MohammadHosseinJamshidi, yes a picture will help maybe. But from a given face, vertices are counterclockwise (considering face normal orientation), and so edges are following this vertex path. – lemon Nov 27 '20 at 10:49
  • in this picture red triangles are blender's tris and yellow ones are what you calculate more. – MohammadHossein Jamshidi Nov 27 '20 at 11:06
  • 1
    @MohammadHosseinJamshidi, I'm very sorry but still don't get it. I may be wrong again but if this is one face (6gon) in Blender polygons model, I do not consider the upper segment in yellow. And I either don't consider the red ones. Maybe we'll need to continue in chat at some moment. – lemon Nov 27 '20 at 11:15
  • 1
    excuse me! I'm really sorry. I forgot that the loop is over those ordered tuples. – MohammadHossein Jamshidi Nov 27 '20 at 11:40
  • No problem. We agree now if I understand well? – lemon Nov 27 '20 at 12:02
  • thank you very much for all answers , I have to digest this :) – Kolkornik Nov 27 '20 at 16:58
0

use this for polygon-wise angles:

import bpy
import math

path = 'D:/01 Projects/edge_angles.txt' obj = bpy.context.object

def angle_between_edges(obj,poly,e1,e2): common_vert = set(e1).intersection(set(e2)) if common_vert==set(): return None v0 = list(common_vert)[0] v1 = list(set(e1).difference(common_vert))[0] v2 = list(set(e2).difference(common_vert))[0] vec1 = obj.data.vertices[v1].co -obj.data.vertices[v0].co vec2 = obj.data.vertices[v2].co -obj.data.vertices[v0].co angle = vec1.angle(vec2) angle = math.degrees(angle) return angle

def write_angles_in_file(obj,path): file = open(path,'w') for p_i,poly in enumerate(obj.data.polygons): for i,e1 in enumerate(poly.edge_keys): for j,e2 in enumerate(poly.edge_keys): if i<j: angle = angle_between_edges(obj,poly,e1,e2) if angle==None: continue line = str(p_i) + ' ' + str(e1) + ' ' + str(e2) + ' ' + str(angle) + '\n' file.write(line) file.close()

write_angles_in_file(obj,path)

and this one for all angles:

import bpy
import math

path = 'D:/01 Projects/edge_angles.txt' obj = bpy.context.object

def collect_all_edges(obj): all_edges = set() for poly in obj.data.polygons: all_edges.update(set(poly.edge_keys)) return list(all_edges)

def angle_between_edges(obj,e1,e2): v1 = obj.data.vertices[e1[0]].co - obj.data.vertices[e1[1]].co v2 = obj.data.vertices[e2[0]].co - obj.data.vertices[e2[1]].co # edge is not directed angle = min(v1.angle(v2),v1.angle(-v2)) return angle

def write_angles_in_file(obj,path): all_edges = collect_all_edges(obj) file = open(path,'w') for i,e1 in enumerate(all_edges): for j,e2 in enumerate(all_edges): if i<j: angle = angle_between_edges(obj,e1,e2) angle = math.degrees(angle) line = str(e1) + ' ' + str(e2) + ' ' + str(angle) + '\n' file.write(line) file.close()

write_angles_in_file(obj,path)

  • Using a set for collecting edges won't give the wanted result as you will loose the per face view when calculating angles. – lemon Nov 27 '20 at 07:20
  • I have added a code for calculating angles per face – MohammadHossein Jamshidi Nov 27 '20 at 08:21
  • this fragment for i,e1 in enumerate(poly.edge_keys): for j,e2 in enumerate(poly.edge_keys): if i<j: suggests for e.g. 10th edge, you will list 9 angles, between 1st and 10th edge, 2nd and 10th edge... and finally 9th and 10th edge. Now, when you compare edges, you check for common vertices, so that fixes it, but still, if 10th edge is the last edge and connects with the 1st edge, then you will list 2 angles instead of 1. Sure, the first edge counters it by having 0 edges satisfying i < j, but you end up with wrong ordering: https://i.imgur.com/xJ7FNS5.png – Markus von Broady May 11 '21 at 15:29
  • @MarkusvonBroady in polygonwise angle calculation a common vertex is considered to calculate the angle, and in second script, all of edges regardless of their order are considered. – MohammadHossein Jamshidi May 11 '21 at 15:47
  • @MarkusvonBroady it is also reflected in the image you have attached. if you see the edges share a common vertex. and the i<j condition just removes doubles and the ordering is considered by common vertex – MohammadHossein Jamshidi May 11 '21 at 15:54
  • @MohammadHosseinJamshidi so this ordering is by design? – Markus von Broady May 11 '21 at 15:57
  • @MarkusvonBroady It doesn't disturb calculation and the questioner didn't ask for it. In addition the ordering could be in several manners (order of vertices inside face, which itself has several cases) (order of vertex indices which can be so different) and more. so considering ordering (although it is simple,) is something else and could be done in a separate thread. – MohammadHossein Jamshidi May 11 '21 at 17:04