Last active
April 14, 2022 13:10
-
-
Save Dragorn421/4dcc7fe91d298b1c6061559b22d9f527 to your computer and use it in GitHub Desktop.
Normal editor from Blend4Web, working standalone https://www.blend4web.com
This file contains 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
# Copyright (C) 2014-2017 Triumph LLC | |
# | |
# 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 3 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, see <http://www.gnu.org/licenses/>. | |
# Original idea from the Recalc Vertex Normals add-on by adsn | |
# The original Blend4Web vertex_normals.py file can be found at: | |
# https://github.com/TriumphLLC/Blend4Web/blob/master/addons/blend4web/vertex_normals.py | |
# or as part of the full distribution, or Blender Add-on, on the download page: | |
# https://www.blend4web.com/en/downloads/ | |
# features: https://www.blend4web.com/en/community/article/131/ | |
# List of changes | |
# Made standalone: | |
# remove translator feature | |
# register classes individually, instead of relying on a single register_module | |
# Panel B4W_VertexNormalsUI: | |
# moved to the "Shading / UVs" tab | |
# added link to Blend4Web website | |
# indicate addon source of panel | |
# Copy and edit bl_info from Blend4Web's __init__ | |
bl_info = { | |
"name": "Normal editor from Blend4Web", | |
"author": "Blend4Web Development Team", | |
"version": (18, 5, 0), | |
"blender": (2, 79, 0), | |
"location": '"Shading / UVs" tab', | |
"description": "Normal editor from Blend4Web, working standalone", | |
"warning": "", | |
"wiki_url": "https://www.blend4web.com/doc/en/normal_editor.html", | |
"category": "Mesh" | |
} | |
added_by_info = 'This is copied from Blend4Web and works standalone.' | |
import bpy | |
import mathutils | |
import bgl | |
from bpy.types import Panel | |
from rna_prop_ui import PropertyPanel | |
from mathutils import (Vector, Quaternion, Matrix) | |
import copy | |
from collections import namedtuple | |
from math import sqrt | |
from math import radians | |
# from blend4web/translator.py (under the same license as this file) | |
_ = lambda string : string | |
p_ = lambda string, context : string | |
global b4w_vertex_to_loops_map | |
b4w_vertex_to_loops_map = {} | |
global b4w_loops_normals | |
b4w_loops_normals = [] | |
global b4w_loops_normals_select | |
b4w_loops_normals_select = [] | |
global helper_handle | |
helper_handle = None | |
global b4w_vn_customnormal_updated | |
b4w_vn_customnormal_updated = False | |
class RotationHelperParams: | |
is_active = False | |
local_space = False | |
constraint = None | |
typed_number = None | |
c_world = Vector() | |
mouse_local = Vector() | |
mouse_world = Vector() | |
AXES_MAP = {"X": Vector((1,0,0)), | |
"Y": Vector((0,1,0)), | |
"Z": Vector((0,0,1))} | |
global modal_operator_helper_params | |
modal_operator_helper_params = RotationHelperParams() | |
def set_vertex_normal(index, value): | |
(x,y,z) = value | |
l = sqrt(x*x + y*y + z*z) | |
if l!= 0: # case when "Copy" not pressed yet | |
newvalue = (x/l, y/l, z/l) | |
if index in b4w_vertex_to_loops_map: | |
for i in b4w_vertex_to_loops_map[index]: | |
b4w_loops_normals[i] = newvalue | |
def resize_array_if_need(array, length, el = (0,0,0)): | |
if len(array) != length: | |
array.clear() | |
for i in range(length): | |
array.append(el) | |
return True | |
else: | |
return False | |
def load_loops_normals(ob): | |
loops_normals = [] | |
vertex_to_loops_map = {} | |
ob.data.calc_normals_split() | |
i = 0 | |
resize_array_if_need(loops_normals, len(ob.data.loops)) | |
for i in range(len(ob.data.loops)): | |
l = ob.data.loops[i] | |
if not l.vertex_index in vertex_to_loops_map: | |
vertex_to_loops_map[l.vertex_index] = [] | |
vertex_to_loops_map[l.vertex_index].append(i) | |
loops_normals[i] = (l.normal.x, l.normal.y, l.normal.z) | |
return (loops_normals, vertex_to_loops_map) | |
def load_loops_normals_into_global_cache(ob): | |
global b4w_loops_normals | |
global b4w_vertex_to_loops_map | |
b4w_loops_normals, b4w_vertex_to_loops_map = load_loops_normals(ob) | |
resize_array_if_need(b4w_loops_normals_select, len(b4w_loops_normals)) | |
def check_b4w_obj_prop(context): | |
if 'b4w_select' not in context.active_object: | |
context.active_object['b4w_select'] = 0 | |
if 'b4w_select_vertex' not in context.active_object: | |
context.active_object['b4w_select_vertex'] = 0 | |
def prepare(context): | |
check_b4w_obj_prop(context) | |
load_loops_normals_into_global_cache(context.active_object) | |
class B4W_ShapeKeysNormal(bpy.types.PropertyGroup): | |
normal = bpy.props.FloatVectorProperty(name=_("Normal"), subtype="NONE", | |
unit="NONE", size=3) | |
def b4w_select(self, context): | |
b4w_split = context.window_manager.b4w_split | |
obj = context.active_object | |
size = 0 | |
bpy.ops.object.mode_set(mode="OBJECT") | |
bpy.ops.object.mode_set(mode="EDIT") | |
prepare(context) | |
sel_vert = find_single_selected_vertex(obj) | |
if sel_vert != -1: | |
context.active_object['b4w_select_vertex'] = sel_vert | |
b4w_select_vertex = context.active_object['b4w_select_vertex'] | |
array = [] | |
if not b4w_split: | |
size = len(context.active_object.data.vertices) | |
array = context.active_object.data.vertices | |
else: | |
size = len(b4w_vertex_to_loops_map[b4w_select_vertex]) | |
array = b4w_vertex_to_loops_map[b4w_select_vertex] | |
obj['b4w_select'] = obj['b4w_select'] % size | |
b4w_select = obj['b4w_select'] | |
if not b4w_split: | |
obj['b4w_select_vertex'] = obj['b4w_select'] | |
bpy.ops.object.mode_set(mode='OBJECT') | |
for i in range(size): | |
select = i == b4w_select | |
if not b4w_split: | |
array[i].select = copy.copy(select) | |
else: | |
b4w_loops_normals_select[array[i]] = copy.copy(select) | |
if select: | |
global b4w_vn_customnormal_updated | |
b4w_vn_customnormal_updated = True | |
context.window_manager.b4w_vn_customnormal1 = copy.copy( | |
b4w_loops_normals[array[i]]) | |
b4w_vn_customnormal_updated = False | |
bpy.ops.object.mode_set(mode='EDIT') | |
class B4W_VertexNormalsUI(bpy.types.Panel): | |
# draw UI buttons | |
bl_idname = "B4W_VIEW3D_PT_normal_editor" | |
bl_label = _('Normal Editor') | |
bl_space_type = 'VIEW_3D' | |
bl_region_type = 'TOOLS' | |
bl_category = "Shading / UVs" | |
def __init__(self): | |
pass | |
@classmethod | |
def poll(self, context): | |
try: | |
ob = context.active_object | |
if ob.type == 'MESH': | |
return True | |
else: | |
return False | |
except AttributeError: | |
return False | |
def draw(self, context): | |
is_edit_mode = context.active_object.mode == 'EDIT' | |
layout = self.layout | |
layout.operator('wm.url_open', text='Blend4Web website', icon='INFO').url = 'https://www.blend4web.com' | |
layout.separator() | |
row = layout.row(align=True) | |
row.prop(context.active_object.data, 'use_auto_smooth', | |
text=_('Activate'), toggle=True, icon='MOD_NORMALEDIT') | |
layout.label(text="Settings:") | |
# draw normals | |
row = layout.row(align=True) | |
row.active = context.active_object.data.use_auto_smooth | |
row.prop(context.active_object.data, 'show_normal_loop', text=_('Show Normals'), | |
toggle=True, icon='LOOPSEL') | |
row.enabled = is_edit_mode | |
row.prop(bpy.context.scene.tool_settings, 'normal_size', text=_('Size')) | |
row.enabled = is_edit_mode | |
# Split normals | |
row = layout.row() | |
row.prop(context.window_manager, 'b4w_split', text = _('Split Mode'), | |
toggle=True) | |
row.enabled = is_edit_mode | |
row.active = context.active_object.data.use_auto_smooth | |
# selection tools | |
# selected single vertex index | |
row.prop(context.active_object, 'b4w_select', text=_('Index')) | |
# Direct editing | |
layout.label(text="Direct Edit:") | |
row = layout.row() | |
row.active = context.active_object.data.use_auto_smooth | |
row.enabled = is_edit_mode | |
col = row.column() | |
col.operator('object.b4w_normal_rotate', text = p_('Rotate', "Operator"), icon='MAN_ROT') | |
col = row.column() | |
col.operator('object.b4w_normal_scale', text = p_('Scale', "Operator"), icon='MAN_SCALE') | |
row = layout.row() | |
row.active = context.active_object.data.use_auto_smooth | |
row.prop(context.window_manager, 'b4w_normal_edit_mode', expand=True) | |
if context.window_manager.b4w_normal_edit_mode == "ABSOLUTE": | |
# manipulate normals | |
row = layout.row() | |
row.active = context.active_object.data.use_auto_smooth | |
row.column().prop(context.window_manager, 'b4w_vn_customnormal2', | |
expand=True, text='') | |
row.enabled = is_edit_mode | |
# ball | |
row.prop(context.window_manager, 'b4w_vn_customnormal1', text='') | |
row.enabled = is_edit_mode | |
else: | |
row = layout.row(align=True) | |
row.column(align=True).prop(context.window_manager, 'b4w_customnormal_offset', text='', expand=True) | |
row = layout.row(align=True) | |
row.operator("object.b4w_apply_offset", text="Sub", icon='ZOOMOUT').sign = -1 | |
row.operator("object.b4w_apply_offset", text="Add", icon='ZOOMIN').sign = 1 | |
# Transform operators | |
layout.label(text="Transform:") | |
# average | |
row = layout.row(align=True) | |
row.enabled = is_edit_mode | |
row.active = context.active_object.data.use_auto_smooth | |
row.operator('object.b4w_smooth_normals', text = p_('Average', "Operator")) | |
# restore/smooth | |
row = layout.row(align=True) | |
row.active = context.active_object.data.use_auto_smooth | |
row.operator('object.b4w_restore_normals', text = p_('Restore', "Operator")) | |
row.enabled = is_edit_mode | |
# Alignment operators | |
layout.label(text="Align:") | |
row = layout.row(align=True) | |
row.active = context.active_object.data.use_auto_smooth | |
row.operator('object.cursor_align_vertex_normals', text = p_('3D Cursor', "Operator"), icon='CURSOR') | |
row.operator('object.axis_align_vertex_normals', text = p_('Axis', "Operator"), icon='MANIPUL') | |
row.operator('object.face_vertex_normals', text = p_('Face', "Operator"), icon='FACESEL') | |
row.enabled = not context.window_manager.b4w_split and is_edit_mode | |
# Transfer tools and operators | |
layout.label(text="Transfer:") | |
row = layout.row(align=True) | |
row.active = context.active_object.data.use_auto_smooth | |
row.operator('object.copy_normal', text = p_('Copy', "Operator"), icon='COPYDOWN') | |
row.operator('object.paste_normal', text = p_('Paste', "Operator"), icon='PASTEDOWN') | |
row.enabled = is_edit_mode | |
row = layout.row(align=True) | |
row.active = context.active_object.data.use_auto_smooth | |
if context.window_manager.b4w_copy_normal_method == "MATCHED": | |
row.operator('object.copy_normals_from_mesh', text = p_('Copy From Mesh', "Operator")) | |
else: | |
row.operator('b4w.approx_normals_from_mesh', text = p_('Copy From Mesh', "Operator")) | |
row.prop(context.window_manager, 'b4w_copy_normal_method', text='') | |
row.enabled = not is_edit_mode | |
layout.separator() | |
layout.label(text=added_by_info, icon='INFO') | |
def lerp_to_vertex_loop_normal(index, value, t): | |
n = b4w_loops_normals[index] | |
n = value.lerp(n, 1-t).normalized() | |
b4w_loops_normals[index] = (n.x, n.y, n.z) | |
def lerp_to_vertex_normal(index, value, t): | |
if index in b4w_vertex_to_loops_map: | |
loops = b4w_vertex_to_loops_map[index] | |
for j in loops: | |
n = b4w_loops_normals[j] | |
n = value.lerp(n, 1-t).normalized() | |
b4w_loops_normals[j] = (n.x, n.y, n.z) | |
class B4W_CursorAlignVertexNormals(bpy.types.Operator): | |
bl_idname = 'object.cursor_align_vertex_normals' | |
bl_label = p_('Vertex Normal Cursor', "Operator") | |
bl_description = _('Align selected verts pointing away from 3d cursor') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
towards = bpy.props.BoolProperty( | |
name = "Towards", | |
description = _("Point normals towards the 3D cursor"), | |
default = 0 | |
) | |
factor = bpy.props.FloatProperty( | |
name = "Factor", | |
description = _("Factor"), | |
subtype = 'FACTOR', | |
min = 0, | |
max = 1, | |
default = 1 | |
) | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode='OBJECT') | |
bpy.ops.object.mode_set(mode='EDIT') | |
prepare(context) | |
obj = context.active_object | |
vert_index = len(bpy.context.active_object.data.vertices) | |
obmat = context.active_object.matrix_world | |
vec1 = obmat.inverted() * context.scene.cursor_location | |
bpy.ops.object.mode_set(mode='OBJECT') | |
for i in range(vert_index): | |
if context.active_object.data.vertices[i].select == True: | |
vec2 = obj.data.vertices[i].co | |
newnormal = vec1 - vec2 if self.towards else vec2 - vec1 | |
newnormal = newnormal.normalized() | |
lerp_to_vertex_normal(i, newnormal, self.factor) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
obj.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
context.area.tag_redraw() | |
return {'FINISHED'} | |
class B4W_AxisAlignVertexNormals(bpy.types.Operator): | |
# align selected verts to selected axis | |
bl_idname = 'object.axis_align_vertex_normals' | |
bl_label = p_('Vertex Normal Axis', "Operator") | |
bl_description = _('Selected verts to Z axis') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
towards = bpy.props.BoolProperty( | |
name = "Towards", | |
description = _("Point normals towards the 3D cursor"), | |
default = True | |
) | |
axis = bpy.props.EnumProperty( | |
name = "Axis", | |
description = _("Alignment axis"), | |
items = [ | |
("X", _("X"), _("Align verts to positive X axis"), 0), | |
("Y", _("Y"), _("Align verts to positive Y axis"), 1), | |
("Z", _("Z"), _("Align verts to positive Z axis"), 2), | |
], | |
default = "Z" | |
) | |
space = bpy.props.EnumProperty( | |
name = "Space", | |
description = _("Transform Space"), | |
items = [ | |
("Global", _("Global"), _("Use global transform space"), 0), | |
("Local", _("Local"), _("Use local transform space"), 1) | |
], | |
default = "Global" | |
) | |
factor = bpy.props.FloatProperty( | |
name = "Factor", | |
description = _("Factor"), | |
subtype = 'FACTOR', | |
min = 0, | |
max = 1, | |
default = 1 | |
) | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode='OBJECT') | |
bpy.ops.object.mode_set(mode='EDIT') | |
prepare(context) | |
obj = context.active_object | |
num_verts = len(bpy.context.active_object.data.vertices) | |
align_vector = copy.copy(AXES_MAP[self.axis]) | |
if not self.towards: | |
align_vector = -align_vector | |
if self.space == 'Global': | |
align_vector = obj.rotation_euler.to_matrix().inverted() * align_vector | |
align_vector.normalize() | |
bpy.ops.object.mode_set(mode='OBJECT') | |
for i in range(num_verts): | |
if context.active_object.data.vertices[i].select == True: | |
lerp_to_vertex_normal(i, align_vector, self.factor) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
obj.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
context.area.tag_redraw() | |
return {'FINISHED'} | |
class B4W_FaceVertexNormals(bpy.types.Operator): | |
# face orientation | |
bl_idname = 'object.face_vertex_normals' | |
bl_label = p_('Vertex Normal Face', "Operator") | |
bl_description = _('Copy face normal') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
smooth_shared = bpy.props.BoolProperty( | |
name = "Smooth Shared", | |
description = _("Smooth shared normals of shared vertices"), | |
default = True | |
) | |
factor = bpy.props.FloatProperty( | |
name = "Factor", | |
description = _("Factor"), | |
subtype = 'FACTOR', | |
min = 0, | |
max = 1, | |
default = 1 | |
) | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode='OBJECT') | |
bpy.ops.object.mode_set(mode='EDIT') | |
prepare(context) | |
obj = context.active_object | |
mesh = obj.data | |
bpy.ops.object.mode_set(mode='OBJECT') | |
sel_indices = [] | |
for i in range(len(mesh.vertices)): | |
if mesh.vertices[i].select == True: | |
sel_indices.append(i) | |
if len(sel_indices) < 3: | |
self.report({'INFO'}, _('Please select at least 3 vertices')) | |
return {"FINISHED"} | |
sel_polys = [] | |
for poly in mesh.polygons: | |
if poly.select == True: | |
sel_polys.append(poly) | |
if self.smooth_shared: | |
for i in sel_indices: | |
n = Vector((0,0,0)) | |
for poly in sel_polys: | |
if i in poly.vertices: | |
n = n + poly.normal | |
n.normalize() | |
lerp_to_vertex_normal(i, n, self.factor) | |
else: | |
for poly in sel_polys: | |
n = poly.normal | |
for loop_i in poly.loop_indices: | |
lerp_to_vertex_loop_normal(loop_i, n, self.factor) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
obj.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
context.area.tag_redraw() | |
return {'FINISHED'} | |
class B4W_CopyNormalsFromMesh(bpy.types.Operator): | |
# copy normals from another mesh if |v1 - v2| -> 0 | |
bl_idname = 'object.copy_normals_from_mesh' | |
bl_label = p_("B4W Copy Normals From Mesh (Matched)", "Operator") | |
bl_description = _('Copy normals from the selected to the active mesh' + | |
' for selected vertices') | |
bl_options = {"REGISTER", "UNDO", "INTERNAL"} | |
threshold = bpy.props.FloatProperty( | |
name = "Threshold", | |
description = _("Threshold"), | |
min = 0.00001, | |
max = 10, | |
step = 0.01, | |
precision = 4, | |
default = 0.001 | |
) | |
factor = bpy.props.FloatProperty( | |
name = "Factor", | |
description = _("Factor"), | |
subtype = 'FACTOR', | |
min = 0, | |
max = 1, | |
default = 1 | |
) | |
def execute(self, context): | |
prepare(context) | |
norm_edited = False | |
if len(bpy.context.selected_objects) != 2: | |
print('Wrong selection') | |
else: | |
obj_target = context.active_object | |
if context.selected_objects[0].name!=obj_target.name: | |
obj_source = context.selected_objects[0] | |
else: | |
obj_source = context.selected_objects[1] | |
verts_target = obj_target.data.vertices | |
verts_source = obj_source.data.vertices | |
src_loops_normals, src_vert_to_loops_map = load_loops_normals(obj_source) | |
verts_is_selected = False | |
for v in verts_target: | |
if v.select: | |
verts_is_selected = True | |
break | |
verts_target_count = len(verts_target) | |
for i in range(verts_target_count): | |
if not verts_is_selected or verts_target[i].select: | |
for j in range(len(verts_source)): | |
if (verts_target[i].co - verts_source[j].co).length < self.threshold: | |
n = Vector() | |
for l in src_vert_to_loops_map[j]: | |
n = n + Vector(src_loops_normals[l]) | |
n = n / len(src_vert_to_loops_map[j]) | |
lerp_to_vertex_normal(i, n, self.factor) | |
norm_edited = True | |
if norm_edited: | |
obj_target.data.normals_split_custom_set(b4w_loops_normals) | |
context.area.tag_redraw() | |
return {'FINISHED'} | |
class B4W_ApproxNormalsFromMesh(bpy.types.Operator): | |
# copy normals from the nearest vertices of another mesh | |
bl_idname = "b4w.approx_normals_from_mesh" | |
bl_label = p_("B4W Copy Normals From Mesh (Nearest)", "Operator") | |
bl_description = _("Approximate target mesh normals from source mesh") | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
factor = bpy.props.FloatProperty( | |
name = "Factor", | |
description = _("Factor"), | |
subtype = 'FACTOR', | |
min = 0, | |
max = 1, | |
default = 1 | |
) | |
def execute(self, context): | |
prepare(context) | |
if len(bpy.context.selected_objects) != 2: | |
print('Wrong selection') | |
self.report({'INFO'}, _('Please select 2 meshes')) | |
else: | |
self.approx_normals(context) | |
context.area.tag_redraw() | |
return {"FINISHED"} | |
def approx_normals(self, context): | |
obj_dst = context.active_object | |
if context.selected_objects[0].name!=obj_dst.name: | |
obj_src = context.selected_objects[0] | |
else: | |
obj_src = context.selected_objects[1] | |
mesh_src = obj_src.data | |
mesh_dst = obj_dst.data | |
src_loops_normals, src_vert_to_loops_map = load_loops_normals(obj_src) | |
dst_vert_list = mesh_dst.vertices | |
src_vert_list = mesh_src.vertices | |
matr_src = obj_src.matrix_world | |
matr_dst = obj_dst.matrix_world | |
# remember old vertices positions | |
old_dst_co_list = [0] * 3 * len(dst_vert_list) | |
old_src_co_list = [0] * 3 * len(src_vert_list) | |
for i in range(len(dst_vert_list)): | |
old_dst_co_list[i * 3] = dst_vert_list[i].co[0] | |
old_dst_co_list[i * 3 + 1] = dst_vert_list[i].co[1] | |
old_dst_co_list[i * 3 + 2] = dst_vert_list[i].co[2] | |
for i in range(len(src_vert_list)): | |
old_src_co_list[i * 3] = src_vert_list[i].co[0] | |
old_src_co_list[i * 3 + 1] = src_vert_list[i].co[1] | |
old_src_co_list[i * 3 + 2] = src_vert_list[i].co[2] | |
# transform vertices to world space | |
for vert_dst in dst_vert_list: | |
vert_dst.co = matr_dst * vert_dst.co | |
for vert_src in src_vert_list: | |
vert_src.co = matr_src * vert_src.co | |
# approximate normals | |
verts_is_selected = False | |
for v in dst_vert_list: | |
if v.select: | |
verts_is_selected = True | |
break | |
for vert_dst in dst_vert_list: | |
if not verts_is_selected or vert_dst.select == True: | |
min_distance = 1E10 | |
min_index = -1 | |
for vert_src in src_vert_list: | |
distance = sqrt(pow(vert_dst.co[0]-vert_src.co[0],2) \ | |
+ pow(vert_dst.co[1]-vert_src.co[1],2) | |
+ pow(vert_dst.co[2]-vert_src.co[2],2)) | |
if distance<min_distance: | |
if vert_src.index in src_vert_to_loops_map: # vertex must be connected | |
min_distance = distance | |
min_index = vert_src.index | |
n = Vector() | |
for l in src_vert_to_loops_map[min_index]: | |
n = n + Vector(src_loops_normals[l]) | |
n = n / len(src_vert_to_loops_map[min_index]) | |
n = matr_dst.to_quaternion().inverted() \ | |
* matr_src.to_quaternion() \ | |
* n | |
lerp_to_vertex_normal(vert_dst.index, n, self.factor) | |
# reset destination mesh's vertices positions | |
for vert_dst in dst_vert_list: | |
vert_dst.co[0] = old_dst_co_list[vert_dst.index * 3] | |
vert_dst.co[1] = old_dst_co_list[vert_dst.index * 3 + 1] | |
vert_dst.co[2] = old_dst_co_list[vert_dst.index * 3 + 2] | |
# reset source mesh's vertices positions | |
for vert_src in src_vert_list: | |
vert_src.co[0] = old_src_co_list[vert_src.index * 3] | |
vert_src.co[1] = old_src_co_list[vert_src.index * 3 + 1] | |
vert_src.co[2] = old_src_co_list[vert_src.index * 3 + 2] | |
obj_dst.data.normals_split_custom_set(b4w_loops_normals) | |
def get_selected_split_normal_idx(obj): | |
vert_index = len(obj.data.vertices) | |
for i in range(vert_index): | |
if obj.data.vertices[i].select == True: | |
obj["b4w_select_vertex"] = i | |
break | |
array = b4w_vertex_to_loops_map[obj["b4w_select_vertex"]] | |
return array[obj["b4w_select"]%len(array)] | |
# main function for update custom normals | |
# trans_param depends on trans_type | |
# if trans_type == rotate -> strans_param == rotation matrix, | |
# else trans_param is normal vector | |
def update_custom_normal(self, context, trans_param, trans_type = "set", | |
init_normals = None): | |
global b4w_loops_normals | |
# toggle mode for update selection | |
bpy.ops.object.mode_set(mode="OBJECT") | |
bpy.ops.object.mode_set(mode="EDIT") | |
check_b4w_obj_prop(context) | |
# forced to load normals always because smooth/flat shading can be changed | |
# no simple way to detect this change | |
load_loops_normals_into_global_cache(context.active_object) | |
# | |
normals_edited = False | |
obj = context.active_object | |
vert_index = len(obj.data.vertices) | |
if not context.window_manager.b4w_split: | |
for i in range(vert_index): | |
# selected verts align on custom normal | |
if obj.data.vertices[i].select == True: | |
normals_edited = True | |
if trans_type == "transform": | |
n = Vector(init_normals[i]) | |
n = trans_param * n | |
elif trans_type == "set": | |
n = copy.copy(trans_param) | |
n.normalize() | |
set_vertex_normal(i, (n.x, n.y, n.z)) | |
else: | |
ind = get_selected_split_normal_idx(obj) | |
if trans_type == "transform": | |
n = Vector(init_normals[ind]) | |
n = trans_param * n | |
elif trans_type == "set": | |
n = copy.copy(trans_param) | |
n.normalize() | |
b4w_loops_normals[ind] = (n.x, n.y, n.z) | |
normals_edited = True | |
if normals_edited: | |
bpy.ops.object.mode_set(mode="OBJECT") | |
obj.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode="EDIT") | |
def update_custom_normal1(self, context): | |
global b4w_vn_customnormal_updated | |
b4w_vn_customnormal_updated = b4w_vn_customnormal_updated + 1 | |
if b4w_vn_customnormal_updated > 1: | |
b4w_vn_customnormal_updated = 0 | |
return | |
normal = context.active_object.rotation_euler.to_matrix().inverted() * \ | |
context.window_manager.b4w_vn_customnormal1 | |
update_custom_normal(self, context, normal) | |
context.window_manager.b4w_vn_customnormal2 = copy.copy( | |
context.window_manager.b4w_vn_customnormal1) | |
def update_custom_normal2(self, context): | |
global b4w_vn_customnormal_updated | |
b4w_vn_customnormal_updated = b4w_vn_customnormal_updated + 1 | |
if b4w_vn_customnormal_updated > 1: | |
b4w_vn_customnormal_updated = 0 | |
return | |
normal = context.active_object.rotation_euler.to_matrix().inverted() * \ | |
context.window_manager.b4w_vn_customnormal2 | |
update_custom_normal(self, context, normal) | |
if context.window_manager.b4w_vn_customnormal2.length != 0: | |
context.window_manager.b4w_vn_customnormal1 = copy.copy( | |
context.window_manager.b4w_vn_customnormal2) | |
def find_single_selected_vertex(object): | |
vertices = object.data.vertices | |
found = 0 | |
last = -1 | |
for i in range(len(vertices)): | |
if vertices[i].select: | |
last = i | |
found += 1 | |
if found == 1: | |
return last | |
else: | |
return -1 | |
class B4W_CopyNormal(bpy.types.Operator): | |
# copy normal | |
bl_idname = 'object.copy_normal' | |
bl_label = p_('Copy Normal', "Operator") | |
bl_description = _('Copies normal from selected Vertex') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode="OBJECT") | |
bpy.ops.object.mode_set(mode="EDIT") | |
prepare(context) | |
obj = context.active_object | |
vert_index = len(context.active_object.data.vertices) | |
if not context.window_manager.b4w_split: | |
check = 0 | |
# inverse selection | |
for h in range(vert_index): | |
if context.active_object.data.vertices[h].select == True: | |
check += 1 | |
if check == 1: | |
for i in range(vert_index): | |
if context.active_object.data.vertices[i].select == True: | |
result = Vector() | |
for l in b4w_vertex_to_loops_map[i]: | |
result = result + Vector(b4w_loops_normals[l]) | |
result = result / len(b4w_vertex_to_loops_map[i]) | |
context.window_manager.b4w_vn_copynormal = result | |
else: | |
self.report({'INFO'}, _('Please select a single vertex')) | |
return {'FINISHED'} | |
else: | |
for i in range(vert_index): | |
if obj.data.vertices[i].select == True: | |
obj["b4w_select_vertex"] = i | |
break | |
array = b4w_vertex_to_loops_map[obj["b4w_select_vertex"]] | |
ind = array[obj["b4w_select"]%len(array)] | |
context.window_manager.b4w_vn_copynormal = Vector(b4w_loops_normals[ind]) | |
return {'FINISHED'} | |
class B4W_PasteNormal(bpy.types.Operator): | |
# paste normal | |
bl_idname = 'object.paste_normal' | |
bl_label = p_('Paste Normal', "Operator") | |
bl_description = _('Paste normal to selected Vertex') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode="OBJECT") | |
bpy.ops.object.mode_set(mode="EDIT") | |
obj = context.active_object | |
check_b4w_obj_prop(context) | |
load_loops_normals_into_global_cache(obj) | |
vert_index = len(context.active_object.data.vertices) | |
check = 0 | |
normals_edited = False | |
if not context.window_manager.b4w_split: | |
for h in range(vert_index): | |
if context.active_object.data.vertices[h].select == True: | |
check += 1 | |
if check >= 1: | |
for i in range(vert_index): | |
if context.active_object.data.vertices[i].select == True: | |
n = context.window_manager.b4w_vn_copynormal | |
set_vertex_normal(i, (n[0], n[1], n[2])) | |
normals_edited = True | |
else: | |
self.report({'INFO'}, _('Please select at least one vertex')) | |
else: | |
array = b4w_vertex_to_loops_map[obj["b4w_select_vertex"]] | |
ind = array[obj["b4w_select"]%len(array)] | |
n = context.window_manager.b4w_vn_copynormal | |
b4w_loops_normals[ind] = (n[0], n[1], n[2]) | |
normals_edited = True | |
if normals_edited: | |
bpy.ops.object.mode_set(mode="OBJECT") | |
obj.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode="EDIT") | |
return {'FINISHED'} | |
class B4W_ApplyOffset(bpy.types.Operator): | |
bl_idname = "object.b4w_apply_offset" | |
bl_label = p_("Apply offset", "Operator") | |
bl_description = _('Apply offset for selected normals') | |
bl_options = {"INTERNAL", "UNDO"} | |
sign = bpy.props.IntProperty( | |
name = "B4W: offset sign", | |
description = _("Offset sign"), | |
default = 1, | |
min=-1, | |
max=1, | |
) | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode='OBJECT') | |
bpy.ops.object.mode_set(mode='EDIT') | |
vertices = context.active_object.data.vertices | |
check_b4w_obj_prop(context) | |
load_loops_normals_into_global_cache(context.active_object) | |
context.active_object.data.calc_normals() | |
offset = context.active_object.rotation_euler.to_matrix().inverted() * context.window_manager.b4w_customnormal_offset | |
if context.window_manager.b4w_split: | |
idx = get_selected_split_normal_idx(context.active_object) | |
self.apply(idx, offset) | |
else: | |
for i in range(len(vertices)): | |
if vertices[i].select: | |
for l in b4w_vertex_to_loops_map[i]: | |
self.apply(l, offset) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
context.active_object.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
return{'FINISHED'} | |
def apply(self, idx, offset): | |
x, y, z = b4w_loops_normals[idx] | |
n = Vector((x + self.sign * offset.x, | |
y + self.sign * offset.y, | |
z + self.sign * offset.z)).normalized() | |
b4w_loops_normals[idx] = (n.x, n.y, n.z) | |
class B4W_RestoreNormals(bpy.types.Operator): | |
# clean up normal list | |
bl_idname = "object.b4w_restore_normals" | |
bl_label = p_("Restore Normals from vertices", "Operator") | |
bl_description = _('Restore normals from vertices') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
def execute(self, context): | |
bpy.ops.object.mode_set(mode='OBJECT') | |
bpy.ops.object.mode_set(mode='EDIT') | |
vertices = context.active_object.data.vertices | |
polygons = context.active_object.data.polygons | |
check_b4w_obj_prop(context) | |
load_loops_normals_into_global_cache(context.active_object) | |
context.active_object.data.calc_normals() | |
if not context.window_manager.b4w_split: | |
# copy normals from vertexes | |
for i in range(len(vertices)): | |
n = vertices[i].normal | |
if vertices[i].select == True: | |
set_vertex_normal(i, (n.x,n.y,n.z)) | |
else: | |
# copy normals from polygons | |
# bruteforce, so it is very slow | |
for i in range(len(vertices)): | |
if vertices[i].select: | |
for l in b4w_vertex_to_loops_map[i]: | |
for p in polygons: | |
for li in p.loop_indices: | |
if li == l: | |
n = p.normal | |
b4w_loops_normals[l] = (n.x, n.y, n.z) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
context.active_object.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
return{'FINISHED'} | |
class B4W_SmoothNormals(bpy.types.Operator): | |
# clean up normal list | |
bl_idname = "object.b4w_smooth_normals" | |
bl_label = p_("Average Normals", "Operator") | |
bl_description = _('Average normals') | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
def execute(self, context): | |
vertices = context.active_object.data.vertices | |
bpy.ops.object.mode_set(mode='EDIT') | |
bpy.ops.object.mode_set(mode='OBJECT') | |
check_b4w_obj_prop(context) | |
load_loops_normals_into_global_cache(context.active_object) | |
N = Vector() | |
for i in range(len(vertices)): | |
if vertices[i].select: | |
n = Vector() | |
for j in b4w_vertex_to_loops_map[i]: | |
n = n + Vector(b4w_loops_normals[j]) | |
n.normalize() | |
if context.window_manager.b4w_split: | |
set_vertex_normal(i, (n.x,n.y,n.z)) | |
else: | |
N += n | |
N.normalize() | |
if not context.window_manager.b4w_split: | |
for i in range(len(vertices)): | |
if vertices[i].select: | |
set_vertex_normal(i, (N.x, N.y, N.z)) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
context.active_object.data.normals_split_custom_set(b4w_loops_normals) | |
bpy.ops.object.mode_set(mode='EDIT') | |
return{'FINISHED'} | |
#------------ scale normal ------------ | |
class EditNormalModalOperator(): | |
number_strs =\ | |
[["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"], | |
["NUMPAD_0", "NUMPAD_1", "NUMPAD_2", "NUMPAD_3", "NUMPAD_4", "NUMPAD_5", "NUMPAD_6", "NUMPAD_7", "NUMPAD_8", "NUMPAD_9"]] | |
mouse_x = 0 | |
mouse_y = 0 | |
def event_str_to_int_str(self, number_str): | |
try: | |
try: | |
n = self.number_strs[0].index(number_str) | |
except ValueError: | |
n = None | |
if n is not None: | |
return str(n) | |
else: | |
return str(self.number_strs[1].index(number_str)) | |
except ValueError: | |
if "PERIOD" == number_str or "NUMPAD_PERIOD" == number_str: | |
return "." | |
return None | |
def calc_mouse_view(self, context): | |
a = None | |
for area in bpy.context.screen.areas: | |
if area.type == 'VIEW_3D': | |
for r in area.regions: | |
if r.type == 'WINDOW': | |
if (r.x <= self.mouse_x < (r.x+area.width) and | |
r.y <= self.mouse_y < (r.y + area.height)): | |
a = r | |
if a: | |
width = a.width | |
height = a.height | |
a_x = a.x | |
a_y = a.y | |
else: | |
return None | |
mouse = Vector(((self.mouse_x- a_x)/width*2 -1, | |
(self.mouse_y - a_y)/height*2 -1 , 0 )) | |
mouse = context.space_data.region_3d.window_matrix.inverted() * mouse | |
return mouse | |
def calc_eye_n(self, context): | |
eye_n = Vector(context.space_data.region_3d.view_matrix[2][:3]) | |
eye_co = (context.space_data.region_3d.view_location + | |
eye_n*context.space_data.region_3d.view_distance) | |
obimat = context.active_object.matrix_world.inverted() | |
c_local = obimat * modal_operator_helper_params.c_world | |
modal_operator_helper_params.c_local = c_local | |
eye_co_local = obimat * eye_co | |
n = eye_co_local - c_local | |
n.normalize() | |
return (n, c_local) | |
def calc_r(self, n, c_local, mouse_local_old): | |
# calculate projection of c_local on viewplane | |
c_pr = c_local + n * (n * | |
(modal_operator_helper_params.mouse_local - c_local)) | |
# calculate main vectors | |
r_old = mouse_local_old - c_pr | |
r = modal_operator_helper_params.mouse_local - c_pr | |
return (r_old, r) | |
def calc_mouse_world(self, context): | |
mouse = self.calc_mouse_view(context) | |
if mouse: | |
return context.space_data.region_3d.view_matrix.inverted() * mouse | |
else: | |
return None | |
def calc_mouse(self, context): | |
modal_operator_helper_params.mouse_world = self.calc_mouse_world(context) | |
if not modal_operator_helper_params.mouse_world: | |
return False | |
modal_operator_helper_params.mouse_local = context.active_object.matrix_world.inverted() *\ | |
modal_operator_helper_params.mouse_world | |
if not self.mouse_local_old: | |
self.mouse_local_old = modal_operator_helper_params.mouse_local | |
return True | |
def number_arr_to_int(self, number_str_arr): | |
number = "" | |
for i in range(1, len(number_str_arr)): | |
number += self.event_str_to_int_str(number_str_arr[i]) | |
result = float(number) * number_str_arr[0] | |
return result | |
def calc_typed_number(self): | |
if len(self.number_arr) > 1: | |
modal_operator_helper_params.typed_number = self.number_arr_to_int(self.number_arr) | |
else: | |
modal_operator_helper_params.typed_number = 0 | |
def modal(self, context, event): | |
def reset_params(): | |
modal_operator_helper_params.is_active = False | |
modal_operator_helper_params.constraint = None | |
modal_operator_helper_params.typed_number = None | |
modal_operator_helper_params.local_space = False | |
if event.type == "MOUSEMOVE": | |
self.mouse_x = event.mouse_x | |
self.mouse_y = event.mouse_y | |
if not modal_operator_helper_params.typed_number: | |
self.execute(context) | |
elif event.type in ["X", "Y", "Z"] and event.value == "PRESS": | |
if not modal_operator_helper_params.constraint == AXES_MAP[event.type]: | |
modal_operator_helper_params.constraint = AXES_MAP[event.type] | |
modal_operator_helper_params.local_space = False | |
else: | |
modal_operator_helper_params.local_space = True | |
context.area.tag_redraw() | |
elif event.type in ["LEFTMOUSE", "RET"]: # Confirm | |
reset_params() | |
context.area.tag_redraw() | |
return {'FINISHED'} | |
elif event.type in ["MINUS", "NUMPAD_MINUS"] and event.value == "PRESS": | |
if len(self.number_arr) == 0: | |
self.number_arr.append(-1) | |
elif event.type == "BACK_SPACE": | |
if event.value == "PRESS": | |
if len(self.number_arr): | |
self.number_arr.pop() | |
self.calc_typed_number() | |
self.execute(context) | |
elif self.event_str_to_int_str(event.type) is not None: | |
if event.value == "PRESS": | |
if len(self.number_arr) == 0: | |
self.number_arr.append(1) | |
self.number_arr.append(event.type) | |
self.calc_typed_number() | |
self.execute(context) | |
elif event.type in ('RIGHTMOUSE', 'ESC'): # Cancel | |
reset_params() | |
bpy.ops.object.mode_set(mode="OBJECT") | |
context.active_object.data.normals_split_custom_set( | |
self.init_loops_normals) | |
bpy.ops.object.mode_set(mode="EDIT") | |
load_loops_normals_into_global_cache(context.active_object) | |
context.area.tag_redraw() | |
return {'CANCELLED'} | |
return {'RUNNING_MODAL'} | |
def invoke(self, context, event): | |
# initialization | |
self.number_arr.clear() | |
if not context.active_object.data.use_auto_smooth: | |
return {'CANCELLED'} | |
global b4w_loops_normals | |
global b4w_vertex_to_loops_map | |
global modal_operator_helper_params | |
# toggle mode for update selection | |
bpy.ops.object.mode_set(mode="OBJECT") | |
bpy.ops.object.mode_set(mode="EDIT") | |
load_loops_normals_into_global_cache(context.active_object) | |
modal_operator_helper_params.is_active = True | |
self.mouse_x = event.mouse_x | |
self.mouse_y = event.mouse_y | |
# make a copy of all normals which will be as start data | |
self.init_loops_normals = copy.copy(b4w_loops_normals) | |
if not context.window_manager.b4w_split: | |
self.init_normals = [] | |
i = 0 | |
j = 0 | |
for v in context.active_object.data.vertices: | |
vert = Vector() | |
# check for floating vertices | |
if i in b4w_vertex_to_loops_map: | |
for j in b4w_vertex_to_loops_map[i]: | |
vert = vert + Vector(b4w_loops_normals[j]) | |
else: | |
vert = Vector() | |
vert = vert / (j+1) | |
i = i + 1 | |
self.init_normals.append(vert) | |
# calculate rotation center | |
modal_operator_helper_params.c_world = Vector((0,0,0)) | |
n = 0 | |
for v in context.active_object.data.vertices: | |
if v.select: | |
modal_operator_helper_params.c_world = modal_operator_helper_params.c_world + v.co | |
n = n + 1 | |
if context.window_manager.b4w_split: | |
break | |
if n > 0: | |
modal_operator_helper_params.c_world = (context.active_object.matrix_world * | |
(modal_operator_helper_params.c_world / n)) | |
modal_operator_helper_params.constraint = None | |
mouse_world = self.calc_mouse_world(context) | |
if mouse_world: | |
self.mouse_local_old = context.active_object.matrix_world.inverted() * mouse_world | |
else: | |
self.mouse_local_old = None | |
self.execute(context) | |
context.window_manager.modal_handler_add(self) | |
return {'RUNNING_MODAL'} | |
class OperatorScaleNormal(EditNormalModalOperator,bpy.types.Operator): | |
bl_idname = "object.b4w_normal_scale" | |
bl_label = p_("Scale Normal", "Operator") | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
number_arr = [] | |
def execute(self, context): | |
# here is main scale logic | |
global modal_operator_helper_params | |
if not self.calc_mouse(context): | |
return | |
n, c_local = self.calc_eye_n(context) | |
r_old, r = self.calc_r(n, c_local, self.mouse_local_old) | |
scale = 4 * (r - r_old) | |
# scaling can't be performed without constraint for normal | |
# setting up default constraint | |
if not modal_operator_helper_params.constraint: | |
modal_operator_helper_params.constraint = AXES_MAP["X"] | |
# correct scale in correspondence to constraint | |
if modal_operator_helper_params.constraint: | |
constraint = Vector(modal_operator_helper_params.constraint) | |
if modal_operator_helper_params.typed_number is not None: | |
scale = modal_operator_helper_params.typed_number * constraint | |
scale = Vector((1,1,1)) - (1 - scale * constraint) * constraint | |
scale_matrix = Matrix([(scale.x, 0, 0), | |
(0, scale.y, 0), | |
(0, 0, scale.z)]) | |
if not modal_operator_helper_params.local_space: | |
obmat = context.active_object.matrix_world.to_3x3() | |
obinvmat = obmat.inverted() | |
scale_matrix = obinvmat * scale_matrix * obmat | |
if context.window_manager.b4w_split: | |
update_custom_normal(self, context, scale_matrix, "transform", | |
self.init_loops_normals) | |
else: | |
update_custom_normal(self, context, scale_matrix, "transform", | |
self.init_normals) | |
#------------ rotate normal ----------- | |
class OperatorRotateNormal(EditNormalModalOperator, bpy.types.Operator): | |
bl_idname = "object.b4w_normal_rotate" | |
bl_label = p_("Rotate Normal", "Operator") | |
bl_options = {"INTERNAL", "REGISTER", "UNDO"} | |
number_arr = [] | |
def execute(self, context): | |
# here is main rotation logic | |
global modal_operator_helper_params | |
n, c_local = self.calc_eye_n(context) | |
if modal_operator_helper_params.typed_number: | |
if modal_operator_helper_params.constraint: | |
matrix = Quaternion(bpy.context.active_object.matrix_world.inverted() * | |
Vector(modal_operator_helper_params.constraint), | |
-radians(modal_operator_helper_params.typed_number)).to_matrix() | |
else: | |
matrix = Quaternion(n, -radians(modal_operator_helper_params.typed_number)).to_matrix() | |
else: | |
if not self.calc_mouse(context): | |
return | |
r_old, r = self.calc_r(n, c_local, self.mouse_local_old) | |
# calculate projection of main vectors on orthogonal to view vector plane | |
r_pr = r - n*r*n | |
r_old_pr = r_old - n*r_old*n | |
r_pr.normalize() | |
r_old_pr.normalize() | |
# calculate main rotation matrix | |
matrix = r_old_pr.rotation_difference(r_pr).to_matrix() | |
# correct rotation matrix in correspondence to constraint | |
if modal_operator_helper_params.constraint: | |
constraint = modal_operator_helper_params.constraint | |
if n*constraint < 0: | |
n = -n | |
matrix = n.rotation_difference(constraint).to_matrix() * matrix | |
if not modal_operator_helper_params.local_space: | |
obmat = context.active_object.matrix_world.to_3x3() | |
matrix = obmat.inverted() * matrix * obmat | |
if context.window_manager.b4w_split: | |
update_custom_normal(self, context, matrix, "transform", | |
self.init_loops_normals) | |
else: | |
update_custom_normal(self, context, matrix, "transform", | |
self.init_normals) | |
#------------- hotkey ------------- | |
def menu_func(self, context): | |
self.layout.operator(OperatorRotateNormal.bl_idname) | |
addon_keymaps = [] | |
def register_hotkey(): | |
bpy.types.VIEW3D_MT_object.append(menu_func) | |
# handle the keymap | |
wm = bpy.context.window_manager | |
# if running not in background | |
if wm.keyconfigs: | |
km = wm.keyconfigs.addon.keymaps.new(name='Mesh', space_type='EMPTY') | |
km.keymap_items.new(OperatorRotateNormal.bl_idname, 'R', 'PRESS', | |
ctrl=True, shift=True) | |
km.keymap_items.new(OperatorScaleNormal.bl_idname, 'S', 'PRESS', | |
ctrl=True, shift=True) | |
addon_keymaps.append(km) | |
def unregister_hotkey(): | |
bpy.types.VIEW3D_MT_object.remove(menu_func) | |
# handle the keymap | |
wm = bpy.context.window_manager | |
if wm.keyconfigs: | |
for km in addon_keymaps: | |
wm.keyconfigs.addon.keymaps.remove(km) | |
# clear the list | |
del addon_keymaps[:] | |
#------------ draw helpers --------- | |
def draw_axis(): | |
n = Vector(modal_operator_helper_params.constraint) | |
if modal_operator_helper_params.local_space: | |
v1 = bpy.context.active_object.matrix_world * (modal_operator_helper_params.c_local - n*25000) | |
v2 = bpy.context.active_object.matrix_world * (modal_operator_helper_params.c_local + n*25000) | |
else: | |
v1 = modal_operator_helper_params.c_world - n*25000 | |
v2 = modal_operator_helper_params.c_world + n*25000 | |
# if modal_operator_helper_params.local_space: | |
# v1 = bpy.context.active_object.location + bpy.context.active_object.matrix_world.inverted() * v1 | |
# v2 = bpy.context.active_object.location + bpy.context.active_object.matrix_world.inverted() * v2 | |
bgl.glColor4f(n.x, n.y, n.z, 0.5) | |
# draw line | |
bgl.glEnable(bgl.GL_BLEND) | |
bgl.glLineWidth(1) | |
bgl.glBegin(bgl.GL_LINES) | |
bgl.glVertex3f(v1.x,v1.y,v1.z) | |
bgl.glVertex3f(v2.x,v2.y,v2.z) | |
bgl.glEnd() | |
bgl.glDisable(bgl.GL_BLEND) | |
def draw_normal(context, vertexloc, vertexnorm, objscale, is_selected = True): | |
# draw normals in object mode | |
obj = context.active_object | |
color1, thick1 = (0.5, 1.0, 1.0, 1.0), 3 | |
# input in localspace | |
vertexloc = copy.copy(vertexloc) | |
vertexloc.resize_4d() | |
obmat = obj.matrix_world | |
r1 = obmat*vertexloc | |
r1.resize_3d() | |
del vertexloc | |
r2 = obj.rotation_euler.to_matrix() * Vector(vertexnorm) | |
r2 = r2* objscale | |
r2 = r2* context.scene.tool_settings.normal_size + r1 | |
bgl.glEnable(bgl.GL_BLEND) | |
bgl.glLineWidth(thick1) | |
# set colour | |
bgl.glColor4f(*color1) | |
# draw line | |
bgl.glBegin(bgl.GL_LINES) | |
bgl.glVertex3f(r1.x,r1.y,r1.z) | |
bgl.glVertex3f(r2.x,r2.y,r2.z) | |
bgl.glEnd() | |
bgl.glDisable(bgl.GL_BLEND) | |
def draw_stipple_line(): | |
v1 = modal_operator_helper_params.c_world | |
if modal_operator_helper_params.mouse_world: | |
v2 = modal_operator_helper_params.mouse_world | |
else: | |
v2 = Vector() | |
bgl.glEnable(bgl.GL_BLEND) | |
bgl.glEnable(bgl.GL_LINE_STIPPLE) | |
bgl.glLineWidth(1) | |
bgl.glLineStipple(3, 0xAAAA) | |
bgl.glDepthMask(bgl.GL_FALSE) | |
bgl.glDisable(bgl.GL_DEPTH_TEST) | |
bgl.glColor4f(0, 0, 0, 1) | |
bgl.glBegin(bgl.GL_LINES) | |
bgl.glVertex3f(v1.x,v1.y,v1.z) | |
bgl.glVertex3f(v2.x,v2.y,v2.z) | |
bgl.glEnd() | |
bgl.glEnable(bgl.GL_DEPTH_TEST) | |
bgl.glDepthMask(bgl.GL_TRUE) | |
bgl.glDisable(bgl.GL_LINE_STIPPLE) | |
bgl.glDisable(bgl.GL_BLEND) | |
def draw_helpers(): | |
context = bpy.context | |
obj = context.active_object | |
if (context.active_object != None and obj.type == 'MESH' and | |
context.active_object.mode == 'EDIT'): | |
line_width = bgl.Buffer(bgl.GL_FLOAT, 1) | |
bgl.glGetFloatv(bgl.GL_LINE_WIDTH, line_width) | |
if context.window_manager.b4w_split and "b4w_select_vertex" in obj: | |
vertex = obj.data.vertices | |
if not obj["b4w_select_vertex"] in b4w_vertex_to_loops_map: | |
return | |
array = b4w_vertex_to_loops_map[obj["b4w_select_vertex"]] | |
ind = array[obj["b4w_select"]%len(array)] | |
n = b4w_loops_normals[ind] | |
draw_normal(context, vertex[obj["b4w_select_vertex"]].co, n, 1) | |
if modal_operator_helper_params.is_active: | |
draw_stipple_line() | |
if modal_operator_helper_params.constraint: | |
draw_axis() | |
bgl.glLineWidth(line_width.to_list()[0]) | |
def draw_helper_callback_enable(): | |
global helper_handle | |
if helper_handle: | |
return | |
context = bpy.context | |
helper_handle = bpy.types.SpaceView3D.draw_handler_add(draw_helpers, (), | |
'WINDOW', 'POST_VIEW') | |
for window in context.window_manager.windows: | |
for area in window.screen.areas: | |
if area.type == 'VIEW_3D': | |
for region in area.regions: | |
if region.type == 'WINDOW': | |
region.tag_redraw() | |
def draw_helper_callback_disable(): | |
global helper_handle | |
if not helper_handle: | |
return | |
try: | |
bpy.types.SpaceView3D.draw_handler_remove(helper_handle, 'WINDOW') | |
except: | |
# already removed | |
pass | |
#----------------------------------- | |
def init_properties(): | |
bpy.types.Object.b4w_shape_keys_normals = bpy.props.CollectionProperty( | |
name=_("B4W: shape keys normal list"), | |
type=B4W_ShapeKeysNormal, | |
description=_("Shape keys normal list")) | |
bpy.types.WindowManager.b4w_split = bpy.props.BoolProperty( | |
name=_("B4W: edit split normals"), | |
default=False, | |
description=_("Edit split normals")) | |
bpy.types.Object.b4w_select = bpy.props.IntProperty( | |
name=_("B4W: selected normal"), | |
default=0, | |
update=b4w_select, | |
description=_("Index of selected normal")) | |
bpy.types.Object.b4w_select_vertex = bpy.props.IntProperty( | |
name=_("B4W: selected vertex"), | |
default=0, | |
description=_("Index of selected vertex")) | |
bpy.types.WindowManager.b4w_vn_copynormal = bpy.props.FloatVectorProperty( | |
name=_("B4W: vertex normal copy"), | |
default=(0.0, 0.0, 0.0), | |
description=_("Vertex normal copy")) | |
bpy.types.WindowManager.b4w_vn_customnormal1 = bpy.props.FloatVectorProperty( | |
name=_("B4W: custom vertex normal 1"), | |
default=(0.0, 0.0, 1.0), | |
subtype = 'DIRECTION', | |
update=update_custom_normal1, | |
description=_("Custom vertex normal")) | |
bpy.types.WindowManager.b4w_vn_customnormal2 = bpy.props.FloatVectorProperty( | |
name=_("B4W: custom vertex normal 2"), | |
default=(0.0, 0.0, 1.0), | |
subtype = 'TRANSLATION', | |
update=update_custom_normal2, | |
description=_("Custom vertex normal")) | |
bpy.types.WindowManager.b4w_customnormal_offset = bpy.props.FloatVectorProperty( | |
name=_("B4W: custom vertex normal offset"), | |
default=(0.0, 0.0, 0.0), | |
subtype = 'TRANSLATION', | |
description=_("Custom vertex normal offset")) | |
bpy.types.WindowManager.b4w_copy_normal_method = bpy.props.EnumProperty( | |
name = "B4W: method of copying normals", | |
description = _("Copy from vertices"), | |
items = [ | |
("MATCHED", _("Matched Vertices"), | |
_("Copy normals only from matched vertices of the source mesh"), 0), | |
("NEAREST", _("Nearest Vertices"), | |
_("Copy normals from nearest vertices of the source mesh"), 1), | |
# ("INTERPOLATED", "Interpolated Vertices", "", 2), # -> proposed | |
], | |
default = "NEAREST" | |
) | |
bpy.types.WindowManager.b4w_normal_edit_mode = bpy.props.EnumProperty( | |
name = "B4W: edit mode", | |
description = _("Normal Editor edit mode"), | |
items = [ | |
("ABSOLUTE", "Absolute", "", 1), | |
("OFFSET", "Offset", "", 2) | |
], | |
default = "ABSOLUTE" | |
) | |
# clear properties | |
def clear_properties(): | |
# actually window_manager props are not stored in blend file | |
# but keep this for future | |
props = ['b4w_vn_customnormal1', 'b4w_vn_customnormal2', | |
'b4w_vn_copynormal', 'b4w_customnormal_offset', 'b4w_normal_edit_mode'] | |
for p in props: | |
if bpy.context.window_manager.get(p) != None: | |
del bpy.context.window_manager[p] | |
try: | |
x = getattr(bpy.types.WindowManager, p) | |
del x | |
except: | |
pass | |
classes = ( | |
B4W_ShapeKeysNormal, | |
B4W_VertexNormalsUI, | |
B4W_CursorAlignVertexNormals, | |
B4W_AxisAlignVertexNormals, | |
B4W_FaceVertexNormals, | |
B4W_CopyNormalsFromMesh, | |
B4W_ApproxNormalsFromMesh, | |
B4W_CopyNormal, | |
B4W_PasteNormal, | |
B4W_ApplyOffset, | |
B4W_RestoreNormals, | |
B4W_SmoothNormals, | |
OperatorScaleNormal, | |
OperatorRotateNormal, | |
) | |
def register(): | |
for clazz in classes: | |
bpy.utils.register_class(clazz) | |
draw_helper_callback_enable() | |
register_hotkey() | |
init_properties() | |
def unregister(): | |
draw_helper_callback_disable() | |
unregister_hotkey() | |
clear_properties() | |
for clazz in reversed(classes): | |
bpy.utils.unregister_class(clazz) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Moved to https://github.com/Dragorn421/blender-normal-editor/