2
$\begingroup$

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

$\endgroup$
1
  • 1
    $\begingroup$ To clarify: As in the interior "edge angle" of an equilateral triangle is sixty degrees? $\endgroup$
    – batFINGER
    Commented Nov 26, 2020 at 17:45

3 Answers 3

6
$\begingroup$

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) < 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("Edge Corner", e1.index, e2.index, "Angle:", 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]
$\endgroup$
1
  • $\begingroup$ this answer is most helpful :) $\endgroup$
    – Kolkornik
    Commented Nov 30, 2020 at 13:56
3
$\begingroup$

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)
$\endgroup$
9
  • $\begingroup$ 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 $\endgroup$ Commented Nov 27, 2020 at 10:21
  • $\begingroup$ @MohammadHosseinJamshidi, I could be wrong but could you provide a concrete example? $\endgroup$
    – lemon
    Commented Nov 27, 2020 at 10:28
  • $\begingroup$ 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. $\endgroup$ Commented Nov 27, 2020 at 10:39
  • 1
    $\begingroup$ @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. $\endgroup$
    – lemon
    Commented Nov 27, 2020 at 11:15
  • 1
    $\begingroup$ excuse me! I'm really sorry. I forgot that the loop is over those ordered tuples. $\endgroup$ Commented Nov 27, 2020 at 11:40
0
$\begingroup$

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)
$\endgroup$
7
  • $\begingroup$ Using a set for collecting edges won't give the wanted result as you will loose the per face view when calculating angles. $\endgroup$
    – lemon
    Commented Nov 27, 2020 at 7:20
  • $\begingroup$ I have added a code for calculating angles per face $\endgroup$ Commented Nov 27, 2020 at 8:21
  • $\begingroup$ 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: i.imgur.com/xJ7FNS5.png $\endgroup$ Commented May 11, 2021 at 15:29
  • $\begingroup$ @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. $\endgroup$ Commented May 11, 2021 at 15:47
  • $\begingroup$ @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 $\endgroup$ Commented May 11, 2021 at 15:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .