Last active
December 13, 2021 07:49
-
-
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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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