mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-16 15:35:05 +00:00
792 lines
26 KiB
Python
792 lines
26 KiB
Python
# ***** BEGIN GPL LICENSE BLOCK *****
|
|
#
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ***** END GPL LICENCE BLOCK *****
|
|
|
|
bl_info = {
|
|
"name": "Offset Edges",
|
|
"author": "Hidesato Ikeya, Veezen fix 2.8 (temporary)",
|
|
#i tried edit newest version, but got some errors, works only on 0,2,6
|
|
"version": (0, 2, 6),
|
|
"blender": (2, 80, 0),
|
|
"location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges",
|
|
"description": "Offset Edges",
|
|
"warning": "",
|
|
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges",
|
|
"tracker_url": "",
|
|
"category": "Mesh"}
|
|
|
|
import math
|
|
from math import sin, cos, pi, copysign, radians
|
|
import bpy
|
|
from bpy_extras import view3d_utils
|
|
import bmesh
|
|
from mathutils import Vector
|
|
from time import perf_counter
|
|
|
|
X_UP = Vector((1.0, .0, .0))
|
|
Y_UP = Vector((.0, 1.0, .0))
|
|
Z_UP = Vector((.0, .0, 1.0))
|
|
ZERO_VEC = Vector((.0, .0, .0))
|
|
ANGLE_90 = pi / 2
|
|
ANGLE_180 = pi
|
|
ANGLE_360 = 2 * pi
|
|
|
|
|
|
def calc_loop_normal(verts, fallback=Z_UP):
|
|
# Calculate normal from verts using Newell's method.
|
|
normal = ZERO_VEC.copy()
|
|
|
|
if verts[0] is verts[-1]:
|
|
# Perfect loop
|
|
range_verts = range(1, len(verts))
|
|
else:
|
|
# Half loop
|
|
range_verts = range(0, len(verts))
|
|
|
|
for i in range_verts:
|
|
v1co, v2co = verts[i-1].co, verts[i].co
|
|
normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z)
|
|
normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x)
|
|
normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y)
|
|
|
|
if normal != ZERO_VEC:
|
|
normal.normalize()
|
|
else:
|
|
normal = fallback
|
|
|
|
return normal
|
|
|
|
def collect_edges(bm):
|
|
set_edges_orig = set()
|
|
for e in bm.edges:
|
|
if e.select:
|
|
co_faces_selected = 0
|
|
for f in e.link_faces:
|
|
if f.select:
|
|
co_faces_selected += 1
|
|
if co_faces_selected == 2:
|
|
break
|
|
else:
|
|
set_edges_orig.add(e)
|
|
|
|
if not set_edges_orig:
|
|
return None
|
|
|
|
return set_edges_orig
|
|
|
|
def collect_loops(set_edges_orig):
|
|
set_edges_copy = set_edges_orig.copy()
|
|
|
|
loops = [] # [v, e, v, e, ... , e, v]
|
|
while set_edges_copy:
|
|
edge_start = set_edges_copy.pop()
|
|
v_left, v_right = edge_start.verts
|
|
lp = [v_left, edge_start, v_right]
|
|
reverse = False
|
|
while True:
|
|
edge = None
|
|
for e in v_right.link_edges:
|
|
if e in set_edges_copy:
|
|
if edge:
|
|
# Overlap detected.
|
|
return None
|
|
edge = e
|
|
set_edges_copy.remove(e)
|
|
if edge:
|
|
v_right = edge.other_vert(v_right)
|
|
lp.extend((edge, v_right))
|
|
continue
|
|
else:
|
|
if v_right is v_left:
|
|
# Real loop.
|
|
loops.append(lp)
|
|
break
|
|
elif reverse is False:
|
|
# Right side of half loop.
|
|
# Reversing the loop to operate same procedure on the left side.
|
|
lp.reverse()
|
|
v_right, v_left = v_left, v_right
|
|
reverse = True
|
|
continue
|
|
else:
|
|
# Half loop, completed.
|
|
loops.append(lp)
|
|
break
|
|
return loops
|
|
|
|
def get_adj_ix(ix_start, vec_edges, half_loop):
|
|
# Get adjacent edge index, skipping zero length edges
|
|
len_edges = len(vec_edges)
|
|
if half_loop:
|
|
range_right = range(ix_start, len_edges)
|
|
range_left = range(ix_start-1, -1, -1)
|
|
else:
|
|
range_right = range(ix_start, ix_start+len_edges)
|
|
range_left = range(ix_start-1, ix_start-1-len_edges, -1)
|
|
|
|
ix_right = ix_left = None
|
|
for i in range_right:
|
|
# Right
|
|
i %= len_edges
|
|
if vec_edges[i] != ZERO_VEC:
|
|
ix_right = i
|
|
break
|
|
for i in range_left:
|
|
# Left
|
|
i %= len_edges
|
|
if vec_edges[i] != ZERO_VEC:
|
|
ix_left = i
|
|
break
|
|
if half_loop:
|
|
# If index of one side is None, assign another index.
|
|
if ix_right is None:
|
|
ix_right = ix_left
|
|
if ix_left is None:
|
|
ix_left = ix_right
|
|
|
|
return ix_right, ix_left
|
|
|
|
def get_adj_faces(edges):
|
|
adj_faces = []
|
|
for e in edges:
|
|
adj_f = None
|
|
co_adj = 0
|
|
for f in e.link_faces:
|
|
# Search an adjacent face.
|
|
# Selected face has precedance.
|
|
if not f.hide and f.normal != ZERO_VEC:
|
|
adj_exist = True
|
|
adj_f = f
|
|
co_adj += 1
|
|
if f.select:
|
|
adj_faces.append(adj_f)
|
|
break
|
|
else:
|
|
if co_adj == 1:
|
|
adj_faces.append(adj_f)
|
|
else:
|
|
adj_faces.append(None)
|
|
return adj_faces
|
|
|
|
|
|
def get_edge_rail(vert, set_edges_orig):
|
|
co_edges = co_edges_selected = 0
|
|
vec_inner = None
|
|
for e in vert.link_edges:
|
|
if (e not in set_edges_orig and
|
|
(e.select or (co_edges_selected == 0 and not e.hide))):
|
|
v_other = e.other_vert(vert)
|
|
vec = v_other.co - vert.co
|
|
if vec != ZERO_VEC:
|
|
vec_inner = vec
|
|
if e.select:
|
|
co_edges_selected += 1
|
|
if co_edges_selected == 2:
|
|
return None
|
|
else:
|
|
co_edges += 1
|
|
if co_edges_selected == 1:
|
|
vec_inner.normalize()
|
|
return vec_inner
|
|
elif co_edges == 1:
|
|
# No selected edges, one unselected edge.
|
|
vec_inner.normalize()
|
|
return vec_inner
|
|
else:
|
|
return None
|
|
|
|
def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l):
|
|
# Cross rail is a cross vector between normal_r and normal_l.
|
|
|
|
vec_cross = normal_r.cross(normal_l)
|
|
if vec_cross.dot(vec_tan) < .0:
|
|
vec_cross *= -1
|
|
cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l))
|
|
cos = vec_tan.dot(vec_cross)
|
|
if cos >= cos_min:
|
|
vec_cross.normalize()
|
|
return vec_cross
|
|
else:
|
|
return None
|
|
|
|
def move_verts(width, depth, verts, directions, geom_ex):
|
|
if geom_ex:
|
|
geom_s = geom_ex['side']
|
|
verts_ex = []
|
|
for v in verts:
|
|
for e in v.link_edges:
|
|
if e in geom_s:
|
|
verts_ex.append(e.other_vert(v))
|
|
break
|
|
#assert len(verts) == len(verts_ex)
|
|
verts = verts_ex
|
|
|
|
for v, (vec_width, vec_depth) in zip(verts, directions):
|
|
v.co += width * vec_width + depth * vec_depth
|
|
|
|
def extrude_edges(bm, edges_orig):
|
|
extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom']
|
|
n_edges = n_faces = len(edges_orig)
|
|
n_verts = len(extruded) - n_edges - n_faces
|
|
|
|
geom = dict()
|
|
geom['verts'] = verts = set(extruded[:n_verts])
|
|
geom['edges'] = edges = set(extruded[n_verts:n_verts + n_edges])
|
|
geom['faces'] = set(extruded[n_verts + n_edges:])
|
|
geom['side'] = set(e for v in verts for e in v.link_edges if e not in edges)
|
|
|
|
return geom
|
|
|
|
def clean(bm, mode, edges_orig, geom_ex=None):
|
|
for f in bm.faces:
|
|
f.select = False
|
|
if geom_ex:
|
|
for e in geom_ex['edges']:
|
|
e.select = True
|
|
if mode == 'offset':
|
|
lis_geom = list(geom_ex['side']) + list(geom_ex['faces'])
|
|
bmesh.ops.delete(bm, geom=lis_geom, context='EDGES')
|
|
else:
|
|
for e in edges_orig:
|
|
e.select = True
|
|
|
|
def collect_mirror_planes(edit_object):
|
|
mirror_planes = []
|
|
eob_mat_inv = edit_object.matrix_world.inverted()
|
|
|
|
|
|
for m in edit_object.modifiers:
|
|
if (m.type == 'MIRROR' and m.use_mirror_merge):
|
|
merge_limit = m.merge_threshold
|
|
if not m.mirror_object:
|
|
loc = ZERO_VEC
|
|
norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP
|
|
else:
|
|
mirror_mat_local = eob_mat_inv @ m.mirror_object.matrix_world
|
|
loc = mirror_mat_local.to_translation()
|
|
norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated()
|
|
norm_x = norm_x.to_3d().normalized()
|
|
norm_y = norm_y.to_3d().normalized()
|
|
norm_z = norm_z.to_3d().normalized()
|
|
if m.use_axis[0]:
|
|
mirror_planes.append((loc, norm_x, merge_limit))
|
|
if m.use_axis[1]:
|
|
mirror_planes.append((loc, norm_y, merge_limit))
|
|
if m.use_axis[2]:
|
|
mirror_planes.append((loc, norm_z, merge_limit))
|
|
return mirror_planes
|
|
|
|
def get_vert_mirror_pairs(set_edges_orig, mirror_planes):
|
|
if mirror_planes:
|
|
set_edges_copy = set_edges_orig.copy()
|
|
vert_mirror_pairs = dict()
|
|
for e in set_edges_orig:
|
|
v1, v2 = e.verts
|
|
for mp in mirror_planes:
|
|
p_co, p_norm, mlimit = mp
|
|
v1_dist = abs(p_norm.dot(v1.co - p_co))
|
|
v2_dist = abs(p_norm.dot(v2.co - p_co))
|
|
if v1_dist <= mlimit:
|
|
# v1 is on a mirror plane.
|
|
vert_mirror_pairs[v1] = mp
|
|
if v2_dist <= mlimit:
|
|
# v2 is on a mirror plane.
|
|
vert_mirror_pairs[v2] = mp
|
|
if v1_dist <= mlimit and v2_dist <= mlimit:
|
|
# This edge is on a mirror_plane, so should not be offsetted.
|
|
set_edges_copy.remove(e)
|
|
return vert_mirror_pairs, set_edges_copy
|
|
else:
|
|
return None, set_edges_orig
|
|
|
|
def get_mirror_rail(mirror_plane, vec_up):
|
|
p_norm = mirror_plane[1]
|
|
mirror_rail = vec_up.cross(p_norm)
|
|
if mirror_rail != ZERO_VEC:
|
|
mirror_rail.normalize()
|
|
# Project vec_up to mirror_plane
|
|
vec_up = vec_up - vec_up.project(p_norm)
|
|
vec_up.normalize()
|
|
return mirror_rail, vec_up
|
|
else:
|
|
return None, vec_up
|
|
|
|
def reorder_loop(verts, edges, lp_normal, adj_faces):
|
|
for i, adj_f in enumerate(adj_faces):
|
|
if adj_f is None:
|
|
continue
|
|
v1, v2 = verts[i], verts[i+1]
|
|
e = edges[i]
|
|
fv = tuple(adj_f.verts)
|
|
if fv[fv.index(v1)-1] is v2:
|
|
# Align loop direction
|
|
verts.reverse()
|
|
edges.reverse()
|
|
adj_faces.reverse()
|
|
if lp_normal.dot(adj_f.normal) < .0:
|
|
lp_normal *= -1
|
|
break
|
|
else:
|
|
# All elements in adj_faces are None
|
|
for v in verts:
|
|
if v.normal != ZERO_VEC:
|
|
if lp_normal.dot(v.normal) < .0:
|
|
verts.reverse()
|
|
edges.reverse()
|
|
lp_normal *= -1
|
|
break
|
|
|
|
return verts, edges, lp_normal, adj_faces
|
|
|
|
def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options):
|
|
opt_follow_face = options['follow_face']
|
|
opt_edge_rail = options['edge_rail']
|
|
opt_er_only_end = options['edge_rail_only_end']
|
|
opt_threshold = options['threshold']
|
|
|
|
verts, edges = lp[::2], lp[1::2]
|
|
set_edges = set(edges)
|
|
lp_normal = calc_loop_normal(verts, fallback=normal_fallback)
|
|
|
|
##### Loop order might be changed below.
|
|
if lp_normal.dot(vec_upward) < .0:
|
|
# Make this loop's normal towards vec_upward.
|
|
verts.reverse()
|
|
edges.reverse()
|
|
lp_normal *= -1
|
|
|
|
if opt_follow_face:
|
|
adj_faces = get_adj_faces(edges)
|
|
verts, edges, lp_normal, adj_faces = \
|
|
reorder_loop(verts, edges, lp_normal, adj_faces)
|
|
else:
|
|
adj_faces = (None, ) * len(edges)
|
|
##### Loop order might be changed above.
|
|
|
|
vec_edges = tuple((e.other_vert(v).co - v.co).normalized()
|
|
for v, e in zip(verts, edges))
|
|
|
|
if verts[0] is verts[-1]:
|
|
# Real loop. Popping last vertex.
|
|
verts.pop()
|
|
HALF_LOOP = False
|
|
else:
|
|
# Half loop
|
|
HALF_LOOP = True
|
|
|
|
len_verts = len(verts)
|
|
directions = []
|
|
for i in range(len_verts):
|
|
vert = verts[i]
|
|
ix_right, ix_left = i, i-1
|
|
|
|
VERT_END = False
|
|
if HALF_LOOP:
|
|
if i == 0:
|
|
# First vert
|
|
ix_left = ix_right
|
|
VERT_END = True
|
|
elif i == len_verts - 1:
|
|
# Last vert
|
|
ix_right = ix_left
|
|
VERT_END = True
|
|
|
|
edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left]
|
|
face_right, face_left = adj_faces[ix_right], adj_faces[ix_left]
|
|
|
|
norm_right = face_right.normal if face_right else lp_normal
|
|
norm_left = face_left.normal if face_left else lp_normal
|
|
if norm_right.angle(norm_left) > opt_threshold:
|
|
# Two faces are not flat.
|
|
two_normals = True
|
|
else:
|
|
two_normals = False
|
|
|
|
tan_right = edge_right.cross(norm_right).normalized()
|
|
tan_left = edge_left.cross(norm_left).normalized()
|
|
tan_avr = (tan_right + tan_left).normalized()
|
|
norm_avr = (norm_right + norm_left).normalized()
|
|
|
|
rail = None
|
|
if two_normals or opt_edge_rail:
|
|
# Get edge rail.
|
|
# edge rail is a vector of an inner edge.
|
|
if two_normals or (not opt_er_only_end) or VERT_END:
|
|
rail = get_edge_rail(vert, set_edges)
|
|
if vert_mirror_pairs and VERT_END:
|
|
if vert in vert_mirror_pairs:
|
|
rail, norm_avr = \
|
|
get_mirror_rail(vert_mirror_pairs[vert], norm_avr)
|
|
if (not rail) and two_normals:
|
|
# Get cross rail.
|
|
# Cross rail is a cross vector between norm_right and norm_left.
|
|
rail = get_cross_rail(
|
|
tan_avr, edge_right, edge_left, norm_right, norm_left)
|
|
if rail:
|
|
dot = tan_avr.dot(rail)
|
|
if dot > .0:
|
|
tan_avr = rail
|
|
elif dot < .0:
|
|
tan_avr = -rail
|
|
|
|
vec_plane = norm_avr.cross(tan_avr)
|
|
e_dot_p_r = edge_right.dot(vec_plane)
|
|
e_dot_p_l = edge_left.dot(vec_plane)
|
|
if e_dot_p_r or e_dot_p_l:
|
|
if e_dot_p_r > e_dot_p_l:
|
|
vec_edge, e_dot_p = edge_right, e_dot_p_r
|
|
else:
|
|
vec_edge, e_dot_p = edge_left, e_dot_p_l
|
|
|
|
vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized()
|
|
# Make vec_tan perpendicular to vec_edge
|
|
vec_up = vec_tan.cross(vec_edge)
|
|
|
|
vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge
|
|
vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge
|
|
else:
|
|
vec_width = tan_avr
|
|
vec_depth = norm_avr
|
|
|
|
directions.append((vec_width, vec_depth))
|
|
|
|
return verts, directions
|
|
|
|
def use_cashes(self, context):
|
|
self.caches_valid = True
|
|
|
|
angle_presets = {'0°': 0,
|
|
'15°': radians(15),
|
|
'30°': radians(30),
|
|
'45°': radians(45),
|
|
'60°': radians(60),
|
|
'75°': radians(75),
|
|
'90°': radians(90),}
|
|
def assign_angle_presets(self, context):
|
|
use_cashes(self, context)
|
|
self.angle = angle_presets[self.angle_presets]
|
|
|
|
class OffsetEdges(bpy.types.Operator):
|
|
"""Offset Edges."""
|
|
bl_idname = "mesh.offset_edges"
|
|
bl_label = "Offset Edges"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
geometry_mode: bpy.props.EnumProperty(
|
|
items=[('offset', "Offset", "Offset edges"),
|
|
('extrude', "Extrude", "Extrude edges"),
|
|
('move', "Move", "Move selected edges")],
|
|
name="Geometory mode", default='offset',
|
|
update=use_cashes)
|
|
width: bpy.props.FloatProperty(
|
|
name="Width", default=.2, precision=4, step=1, update=use_cashes)
|
|
flip_width: bpy.props.BoolProperty(
|
|
name="Flip Width", default=False,
|
|
description="Flip width direction", update=use_cashes)
|
|
depth: bpy.props.FloatProperty(
|
|
name="Depth", default=.0, precision=4, step=1, update=use_cashes)
|
|
flip_depth: bpy.props.BoolProperty(
|
|
name="Flip Depth", default=False,
|
|
description="Flip depth direction", update=use_cashes)
|
|
depth_mode: bpy.props.EnumProperty(
|
|
items=[('angle', "Angle", "Angle"),
|
|
('depth', "Depth", "Depth")],
|
|
name="Depth mode", default='angle', update=use_cashes)
|
|
angle: bpy.props.FloatProperty(
|
|
name="Angle", default=0, precision=3, step=.1,
|
|
min=-2*pi, max=2*pi, subtype='ANGLE',
|
|
description="Angle", update=use_cashes)
|
|
flip_angle: bpy.props.BoolProperty(
|
|
name="Flip Angle", default=False,
|
|
description="Flip Angle", update=use_cashes)
|
|
follow_face: bpy.props.BoolProperty(
|
|
name="Follow Face", default=False,
|
|
description="Offset along faces around")
|
|
mirror_modifier: bpy.props.BoolProperty(
|
|
name="Mirror Modifier", default=False,
|
|
description="Take into account of Mirror modifier")
|
|
edge_rail: bpy.props.BoolProperty(
|
|
name="Edge Rail", default=False,
|
|
description="Align vertices along inner edges")
|
|
edge_rail_only_end: bpy.props.BoolProperty(
|
|
name="Edge Rail Only End", default=False,
|
|
description="Apply edge rail to end verts only")
|
|
threshold: bpy.props.FloatProperty(
|
|
name="Flat Face Threshold", default=radians(0.05), precision=5,
|
|
step=1.0e-4, subtype='ANGLE',
|
|
description="If difference of angle between two adjacent faces is "
|
|
"below this value, those faces are regarded as flat.",
|
|
options={'HIDDEN'})
|
|
caches_valid: bpy.props.BoolProperty(
|
|
name="Caches Valid", default=False,
|
|
options={'HIDDEN'})
|
|
angle_presets: bpy.props.EnumProperty(
|
|
items=[('0°', "0°", "0°"),
|
|
('15°', "15°", "15°"),
|
|
('30°', "30°", "30°"),
|
|
('45°', "45°", "45°"),
|
|
('60°', "60°", "60°"),
|
|
('75°', "75°", "75°"),
|
|
('90°', "90°", "90°"), ],
|
|
name="Angle Presets", default='0°',
|
|
update=assign_angle_presets)
|
|
|
|
_cache_offset_infos = None
|
|
_cache_edges_orig_ixs = None
|
|
|
|
@classmethod
|
|
def poll(self, context):
|
|
return context.mode == 'EDIT_MESH'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.prop(self, 'geometry_mode', text="")
|
|
#layout.prop(self, 'geometry_mode', expand=True)
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(self, 'width')
|
|
row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)
|
|
|
|
layout.prop(self, 'depth_mode', expand=True)
|
|
if self.depth_mode == 'angle':
|
|
d_mode = 'angle'
|
|
flip = 'flip_angle'
|
|
else:
|
|
d_mode = 'depth'
|
|
flip = 'flip_depth'
|
|
row = layout.row(align=True)
|
|
row.prop(self, d_mode)
|
|
row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True)
|
|
if self.depth_mode == 'angle':
|
|
layout.prop(self, 'angle_presets', text="Presets", expand=True)
|
|
|
|
layout.separator()
|
|
|
|
layout.prop(self, 'follow_face')
|
|
|
|
row = layout.row()
|
|
row.prop(self, 'edge_rail')
|
|
if self.edge_rail:
|
|
row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)
|
|
|
|
layout.prop(self, 'mirror_modifier')
|
|
|
|
#layout.operator('mesh.offset_edges', text='Repeat')
|
|
|
|
if self.follow_face:
|
|
layout.separator()
|
|
layout.prop(self, 'threshold', text='Threshold')
|
|
|
|
|
|
def get_offset_infos(self, bm, edit_object):
|
|
if self.caches_valid and self._cache_offset_infos is not None:
|
|
# Return None, indicating to use cache.
|
|
return None, None
|
|
|
|
time = perf_counter()
|
|
|
|
set_edges_orig = collect_edges(bm)
|
|
if set_edges_orig is None:
|
|
self.report({'WARNING'},
|
|
"No edges selected.")
|
|
return False, False
|
|
|
|
if self.mirror_modifier:
|
|
mirror_planes = collect_mirror_planes(edit_object)
|
|
vert_mirror_pairs, set_edges = \
|
|
get_vert_mirror_pairs(set_edges_orig, mirror_planes)
|
|
|
|
if set_edges:
|
|
set_edges_orig = set_edges
|
|
else:
|
|
#self.report({'WARNING'},
|
|
# "All selected edges are on mirror planes.")
|
|
vert_mirror_pairs = None
|
|
else:
|
|
vert_mirror_pairs = None
|
|
|
|
loops = collect_loops(set_edges_orig)
|
|
if loops is None:
|
|
self.report({'WARNING'},
|
|
"Overlap detected. Select non-overlap edge loops")
|
|
return False, False
|
|
|
|
vec_upward = (X_UP + Y_UP + Z_UP).normalized()
|
|
# vec_upward is used to unify loop normals when follow_face is off.
|
|
normal_fallback = Z_UP
|
|
#normal_fallback = Vector(context.region_data.view_matrix[2][:3])
|
|
# normal_fallback is used when loop normal cannot be calculated.
|
|
|
|
follow_face = self.follow_face
|
|
edge_rail = self.edge_rail
|
|
er_only_end = self.edge_rail_only_end
|
|
threshold = self.threshold
|
|
|
|
offset_infos = []
|
|
for lp in loops:
|
|
verts, directions = get_directions(
|
|
lp, vec_upward, normal_fallback, vert_mirror_pairs,
|
|
follow_face=follow_face, edge_rail=edge_rail,
|
|
edge_rail_only_end=er_only_end,
|
|
threshold=threshold)
|
|
if verts:
|
|
offset_infos.append((verts, directions))
|
|
|
|
# Saving caches.
|
|
self._cache_offset_infos = _cache_offset_infos = []
|
|
for verts, directions in offset_infos:
|
|
v_ixs = tuple(v.index for v in verts)
|
|
_cache_offset_infos.append((v_ixs, directions))
|
|
self._cache_edges_orig_ixs = tuple(e.index for e in set_edges_orig)
|
|
|
|
print("Preparing OffsetEdges: ", perf_counter() - time)
|
|
|
|
return offset_infos, set_edges_orig
|
|
|
|
def do_offset_and_free(self, bm, me, offset_infos=None, set_edges_orig=None):
|
|
# If offset_infos is None, use caches.
|
|
# Makes caches invalid after offset.
|
|
|
|
#time = perf_counter()
|
|
|
|
if offset_infos is None:
|
|
# using cache
|
|
bmverts = tuple(bm.verts)
|
|
bmedges = tuple(bm.edges)
|
|
edges_orig = [bmedges[ix] for ix in self._cache_edges_orig_ixs]
|
|
verts_directions = []
|
|
for ix_vs, directions in self._cache_offset_infos:
|
|
verts = tuple(bmverts[ix] for ix in ix_vs)
|
|
verts_directions.append((verts, directions))
|
|
else:
|
|
verts_directions = offset_infos
|
|
edges_orig = list(set_edges_orig)
|
|
|
|
if self.depth_mode == 'angle':
|
|
w = self.width if not self.flip_width else -self.width
|
|
angle = self.angle if not self.flip_angle else -self.angle
|
|
width = w * cos(angle)
|
|
depth = w * sin(angle)
|
|
else:
|
|
width = self.width if not self.flip_width else -self.width
|
|
depth = self.depth if not self.flip_depth else -self.depth
|
|
|
|
# Extrude
|
|
if self.geometry_mode == 'move':
|
|
geom_ex = None
|
|
else:
|
|
geom_ex = extrude_edges(bm, edges_orig)
|
|
|
|
for verts, directions in verts_directions:
|
|
move_verts(width, depth, verts, directions, geom_ex)
|
|
|
|
clean(bm, self.geometry_mode, edges_orig, geom_ex)
|
|
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
bm.to_mesh(me)
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
bm.free()
|
|
self.caches_valid = False # Make caches invalid.
|
|
|
|
#print("OffsetEdges offset: ", perf_counter() - time)
|
|
|
|
def execute(self, context):
|
|
# In edit mode
|
|
edit_object = context.edit_object
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
|
|
me = edit_object.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
|
|
offset_infos, edges_orig = self.get_offset_infos(bm, edit_object)
|
|
if offset_infos is False:
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
return {'CANCELLED'}
|
|
|
|
self.do_offset_and_free(bm, me, offset_infos, edges_orig)
|
|
|
|
return {'FINISHED'}
|
|
|
|
def restore_original_and_free(self, context):
|
|
self.caches_valid = False # Make caches invalid.
|
|
context.area.header_text_set()
|
|
|
|
me = context.edit_object.data
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
self._bm_orig.to_mesh(me)
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
|
|
self._bm_orig.free()
|
|
context.area.header_text_set()
|
|
|
|
def invoke(self, context, event):
|
|
# In edit mode
|
|
edit_object = context.edit_object
|
|
me = edit_object.data
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
for p in me.polygons:
|
|
if p.select:
|
|
self.follow_face = True
|
|
break
|
|
|
|
self.caches_valid = False
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
return self.execute(context)
|
|
|
|
class OffsetEdgesMenu(bpy.types.Menu):
|
|
bl_idname = "VIEW3D_MT_edit_mesh_offset_edges"
|
|
bl_label = "Offset Edges"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator_context = 'INVOKE_DEFAULT'
|
|
|
|
off = layout.operator('mesh.offset_edges', text='Offset')
|
|
off.geometry_mode = 'offset'
|
|
|
|
ext = layout.operator('mesh.offset_edges', text='Extrude')
|
|
ext.geometry_mode = 'extrude'
|
|
|
|
mov = layout.operator('mesh.offset_edges', text='Move')
|
|
mov.geometry_mode = 'move'
|
|
|
|
classes = (
|
|
OffsetEdges,
|
|
OffsetEdgesMenu,
|
|
)
|
|
|
|
def draw_item(self, context):
|
|
self.layout.menu("VIEW3D_MT_edit_mesh_offset_edges")
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.VIEW3D_MT_edit_mesh_edges.prepend(draw_item)
|
|
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
register()
|