Skip to content

Instantly share code, notes, and snippets.

@MetroWind
Last active December 13, 2021 07:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MetroWind/5a2b14ce5c5e3d0c8029c8285560e8ac to your computer and use it in GitHub Desktop.
Save MetroWind/5a2b14ce5c5e3d0c8029c8285560e8ac to your computer and use it in GitHub Desktop.
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 collections.abc 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):
self.data = []
def enqueue(self, x):
self.data.append(x)
def dequeue(self):
del self.data[0]
def head(self):
return self.data[0]
def empty(self):
return len(self.data) == 0
def __contains__(self, x):
return x in self.data
class Offsetter(object):
def __init__(self):
self.object = bpy.context.selected_objects[0]
self.bm = bmesh.new()
self.bm.from_mesh(self.object.data)
def getSelectedVertices(self) -> Set[bmesh.types.BMVert]:
return set(v for v in self.bm.verts if v.select)
@staticmethod
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)
@staticmethod
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]):
self.bm.select_mode = {"VERT",}
for v in vs:
v.select_set(True)
def offset(self, distance):
verts = self.getSelectedVertices()
print("Selected {} vertices".format(len(verts)))
center = sum((v.co for v in verts),
start=mathutils.Vector((0.0, 0.0, 0.0))) / len(verts)
verts_to_go = Queue()
verts_to_go.enqueue(next(iter(verts)))
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(vert.co, neighbors[0].co, neighbors[1].co,
center)
new_vert = self.bm.verts.new(vert.co + direction * distance)
count += 1
self.bm.edges.new((vert, new_vert))
relation[vert] = new_vert
for neighbor in neighbors:
if neighbor in relation:
self.bm.edges.new((relation[neighbor], new_vert))
self.bm.faces.new((vert, neighbor, relation[neighbor],
new_vert))
# If this is just an "else", the last vertex would be offset
# twice for some reason.
elif neighbor not in verts_to_go:
verts_to_go.enqueue(neighbor)
verts_to_go.dequeue()
print("Created {} vertices.".format(count))
for v in verts:
v.select_set(False)
self.selectVertices(relation.values())
self.bm.to_mesh(self.object.data)
# See https://stackoverflow.com/questions/15429796/blender-scripting-indices-of-selected-vertices
bpy.ops.object.mode_set(mode='OBJECT')
o = Offsetter()
o.offset(-0.07)
bpy.ops.object.mode_set(mode='EDIT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment