mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-20 13:22:58 +00:00
mesh_tools: restore to release: T63750 9e99e90f08
This commit is contained in:
791
mesh_tools/mesh_offset_edges.py
Normal file
791
mesh_tools/mesh_offset_edges.py
Normal file
@ -0,0 +1,791 @@
|
||||
# ***** 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()
|
Reference in New Issue
Block a user