Skip to content

Instantly share code, notes, and snippets.

Last active December 13, 2021 07:49
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
What would you like to do?
Offset a loop of edges. Probably only works in the simple case where the loop is convex and the internal ordering of the vertices is "nice".
# Offset a loop of edges. Probably only works in the simple case where the loop
# is convex and the internal ordering of the vertices is "nice".
# Usage:
# 1. In edit mode, select a loop of vertices. The vertices should be roughly on
# the same plane.
# 2. Scroll to the bottom of this file, and change the offset distance in the
# offset function call. The distance can be negative.
# 3. Press "Run Script".
import typing
from typing import Set
from import Collection
import bpy
import bmesh
import mathutils
def normalVec(v0: mathutils.Vector, v1: mathutils.Vector,
v2: mathutils.Vector) -> mathutils.Vector:
"""Return the normal vector of the plane formed by three points v0, v1,
and v2. The ordering is v1 -> v0 -> v2."""
return (v1 - v0).cross(v0 - v2).normalized()
class Queue(object):
def __init__(self): = []
def enqueue(self, x):
def dequeue(self):
def head(self):
def empty(self):
return len( == 0
def __contains__(self, x):
return x in
class Offsetter(object):
def __init__(self):
self.object = bpy.context.selected_objects[0] =
def getSelectedVertices(self) -> Set[bmesh.types.BMVert]:
return set(v for v in if
def getNeighbors(v: bmesh.types.BMVert) -> Set[bmesh.types.BMVert]:
"""Return all vertices that share edge with vertex `v`."""
return set(edge.other_vert(v) for edge in v.link_edges)
def offsetDir(v: mathutils.Vector, neighbor1: mathutils.Vector,
neighbor2: mathutils.Vector,
center: mathutils.Vector) -> mathutils.Vector:
normal1 = normalVec(neighbor1, v, center)
offset_dir1 = (neighbor1 - v).cross(normal1).normalized()
normal2 = normalVec(neighbor2, v, center)
offset_dir2 = (neighbor2 - v).cross(normal2).normalized()
return ((offset_dir1 + offset_dir2) * 0.5).normalized()
def selectVertices(self, vs: Collection[bmesh.types.BMVert]): = {"VERT",}
for v in vs:
def offset(self, distance):
verts = self.getSelectedVertices()
print("Selected {} vertices".format(len(verts)))
center = sum(( for v in verts),
start=mathutils.Vector((0.0, 0.0, 0.0))) / len(verts)
verts_to_go = Queue()
relation = dict()
count = 0
while not verts_to_go.empty():
vert = verts_to_go.head()
neighbors = tuple(v for v in self.getNeighbors(vert) if v in verts)
if len(neighbors) != 2:
raise RuntimeError("Selection is not a loop")
# Calculate an offset direction from the two neighbors. This part
# may be sensitive to the internal ordering of vertices in Blender.
# Suppose we have 4 vertices connected like v0--v1--v2--v3. When
# finding the normal vector of the plane on v1, we may use
# v0--v1--v2, in this order. But when finding the normal vector on
# v2, the ordering may be v3--v2--v1, and the resulting normal will
# be oppsite to the previous normal. This can be fixed if we order
# the 2 neighbors correctly in relation to the center coordinate.
# And it's not a difficult fix. But we are not doing that here.
direction = self.offsetDir(, neighbors[0].co, neighbors[1].co,
new_vert = + direction * distance)
count += 1, new_vert))
relation[vert] = new_vert
for neighbor in neighbors:
if neighbor in relation:[neighbor], new_vert)), neighbor, relation[neighbor],
# If this is just an "else", the last vertex would be offset
# twice for some reason.
elif neighbor not in verts_to_go:
print("Created {} vertices.".format(count))
for v in verts:
# See
o = Offsetter()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment