mesh_tools: restore to release: T63750 9e99e90f08

This commit is contained in:
Brendon Murphy 2019-06-15 14:06:26 +10:00
parent c7b5ffcfd9
commit 682d48cefc
16 changed files with 9361 additions and 0 deletions

1165
mesh_tools/__init__.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,335 @@
# -*- coding: utf-8 -*-
# ##### 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 LICENSE BLOCK #####
# based completely on addon by zmj100
# added some distance limits to prevent overlap - max12345
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import (
FloatProperty,
IntProperty,
BoolProperty,
EnumProperty,
)
from math import (
sin, cos, tan,
degrees, radians,
)
from mathutils import Matrix
def edit_mode_out():
bpy.ops.object.mode_set(mode='OBJECT')
def edit_mode_in():
bpy.ops.object.mode_set(mode='EDIT')
def angle_rotation(rp, q, axis, angle):
# returns the vector made by the rotation of the vector q
# rp by angle around axis and then adds rp
return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp
def face_inset_fillet(bme, face_index_list, inset_amount, distance,
number_of_sides, out, radius, type_enum, kp):
list_del = []
for faceindex in face_index_list:
bme.faces.ensure_lookup_table()
# loops through the faces...
f = bme.faces[faceindex]
f.select_set(False)
list_del.append(f)
f.normal_update()
vertex_index_list = [v.index for v in f.verts]
dict_0 = {}
orientation_vertex_list = []
n = len(vertex_index_list)
for i in range(n):
# loops through the vertices
dict_0[i] = []
bme.verts.ensure_lookup_table()
p = (bme.verts[vertex_index_list[i]].co).copy()
p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
# copies some vert coordinates, always the 3 around i
dict_0[i].append(bme.verts[vertex_index_list[i]])
# appends the bmesh vert of the appropriate index to the dict
vec1 = p - p1
vec2 = p - p2
# vectors for the other corner points to the cornerpoint
# corresponding to i / p
angle = vec1.angle(vec2)
adj = inset_amount / tan(angle * 0.5)
h = (adj ** 2 + inset_amount ** 2) ** 0.5
if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
# if the corner is a straight line...
# I think this creates some new points...
if out is True:
val = ((f.normal).normalized() * inset_amount)
else:
val = -((f.normal).normalized() * inset_amount)
p6 = angle_rotation(p, p + val, vec1, radians(90))
else:
# if the corner is an actual corner
val = ((f.normal).normalized() * h)
if out is True:
# this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
p6 = angle_rotation(
p, p + val,
-(p - (vec2.normalized() * adj)),
-radians(90)
)
else:
p6 = angle_rotation(
p, p - val,
((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
-radians(90)
)
orientation_vertex_list.append(p6)
new_inner_face = []
orientation_vertex_list_length = len(orientation_vertex_list)
ovll = orientation_vertex_list_length
for j in range(ovll):
q = orientation_vertex_list[j]
q1 = orientation_vertex_list[(j - 1) % ovll]
q2 = orientation_vertex_list[(j + 1) % ovll]
# again, these are just vectors between somewhat displaced corner vertices
vec1_ = q - q1
vec2_ = q - q2
ang_ = vec1_.angle(vec2_)
# the angle between them
if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
# again... if it's really a line...
v = bme.verts.new(q)
new_inner_face.append(v)
dict_0[j].append(v)
else:
# s.a.
if radius is False:
h_ = distance * (1 / cos(ang_ * 0.5))
d = distance
elif radius is True:
h_ = distance / sin(ang_ * 0.5)
d = distance / tan(ang_ * 0.5)
# max(d) is vec1_.magnitude * 0.5
# or vec2_.magnitude * 0.5 respectively
# only functional difference v
if d > vec1_.magnitude * 0.5:
d = vec1_.magnitude * 0.5
if d > vec2_.magnitude * 0.5:
d = vec2_.magnitude * 0.5
# only functional difference ^
q3 = q - (vec1_.normalized() * d)
q4 = q - (vec2_.normalized() * d)
# these are new verts somewhat offset from the corners
rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
# reference point inside the curvature
axis_ = vec1_.cross(vec2_)
# this should really be just the face normal
vec3_ = rp_ - q3
vec4_ = rp_ - q4
rot_ang = vec3_.angle(vec4_)
cornerverts = []
for o in range(number_of_sides + 1):
# this calculates the actual new vertices
q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
v = bme.verts.new(q5)
# creates new bmesh vertices from it
bme.verts.index_update()
dict_0[j].append(v)
cornerverts.append(v)
cornerverts.reverse()
new_inner_face.extend(cornerverts)
if out is False:
f = bme.faces.new(new_inner_face)
f.select_set(True)
elif out is True and kp is True:
f = bme.faces.new(new_inner_face)
f.select_set(True)
n2_ = len(dict_0)
# these are the new side faces, those that don't depend on cornertype
for o in range(n2_):
list_a = dict_0[o]
list_b = dict_0[(o + 1) % n2_]
bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
bme.faces.index_update()
# cornertype 1 - ngon faces
if type_enum == 'opt0':
for k in dict_0:
if len(dict_0[k]) > 2:
bme.faces.new(dict_0[k])
bme.faces.index_update()
# cornertype 2 - triangulated faces
if type_enum == 'opt1':
for k_ in dict_0:
q_ = dict_0[k_][0]
dict_0[k_].pop(0)
n3_ = len(dict_0[k_])
for kk in range(n3_ - 1):
bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
bme.faces.index_update()
del_ = [bme.faces.remove(f) for f in list_del]
if del_:
del del_
# Operator
class MESH_OT_face_inset_fillet(Operator):
bl_idname = "mesh.face_inset_fillet"
bl_label = "Face Inset Fillet"
bl_description = ("Inset selected and Fillet (make round) the corners \n"
"of the newly created Faces")
bl_options = {"REGISTER", "UNDO"}
# inset amount
inset_amount: FloatProperty(
name="Inset amount",
description="Define the size of the Inset relative to the selection",
default=0.04,
min=0, max=100.0,
step=1,
precision=3
)
# number of sides
number_of_sides: IntProperty(
name="Number of sides",
description="Define the roundness of the corners by specifying\n"
"the subdivision count",
default=4,
min=1, max=100,
step=1
)
distance: FloatProperty(
name="",
description="Use distance or radius for corners' size calculation",
default=0.04,
min=0.00001, max=100.0,
step=1,
precision=3
)
out: BoolProperty(
name="Outside",
description="Inset the Faces outwards in relation to the selection\n"
"Note: depending on the geometry, can give unsatisfactory results",
default=False
)
radius: BoolProperty(
name="Radius",
description="Use radius for corners' size calculation",
default=False
)
type_enum: EnumProperty(
items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
('opt1', "Triangle", "Triangulate corners")),
name="Corner Type",
default="opt0"
)
kp: BoolProperty(
name="Keep faces",
description="Do not delete the inside Faces\n"
"Only available if the Out option is checked",
default=False
)
def draw(self, context):
layout = self.layout
layout.label(text="Corner Type:")
row = layout.row()
row.prop(self, "type_enum", text="")
row = layout.row(align=True)
row.prop(self, "out")
if self.out is True:
row.prop(self, "kp")
row = layout.row()
row.prop(self, "inset_amount")
row = layout.row()
row.prop(self, "number_of_sides")
row = layout.row()
row.prop(self, "radius")
row = layout.row()
dist_rad = "Radius" if self.radius else "Distance"
row.prop(self, "distance", text=dist_rad)
def execute(self, context):
# this really just prepares everything for the main function
inset_amount = self.inset_amount
number_of_sides = self.number_of_sides
distance = self.distance
out = self.out
radius = self.radius
type_enum = self.type_enum
kp = self.kp
edit_mode_out()
ob_act = context.active_object
bme = bmesh.new()
bme.from_mesh(ob_act.data)
# this
face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
if len(face_index_list) == 0:
self.report({'WARNING'},
"No suitable Face selection found. Operation cancelled")
edit_mode_in()
return {'CANCELLED'}
elif len(face_index_list) != 0:
face_inset_fillet(bme, face_index_list,
inset_amount, distance, number_of_sides,
out, radius, type_enum, kp)
bme.to_mesh(ob_act.data)
edit_mode_in()
return {'FINISHED'}

View File

@ -0,0 +1,241 @@
bl_info = {
"name" : "Cut Faces",
"author" : "Stanislav Blinov",
"version" : (1, 0, 0),
"blender" : (2, 80, 0),
"description" : "Cut Faces and Deselect Boundary operators",
"category" : "Mesh",}
import bpy
import bmesh
def bmesh_from_object(object):
mesh = object.data
if object.mode == 'EDIT':
bm = bmesh.from_edit_mesh(mesh)
else:
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def bmesh_release(bm, object):
mesh = object.data
bm.select_flush_mode()
if object.mode == 'EDIT':
bmesh.update_edit_mesh(mesh, True)
else:
bm.to_mesh(mesh)
bm.free()
def calc_face(face, keep_caps=True):
assert face.tag
def radial_loops(loop):
next = loop.link_loop_radial_next
while next != loop:
result, next = next, next.link_loop_radial_next
yield result
result = []
face.tag = False
selected = []
to_select = []
for loop in face.loops:
self_selected = False
# Iterate over selected adjacent faces
for radial_loop in filter(lambda l: l.face.select, radial_loops(loop)):
# Tag the edge if no other face done so already
if not loop.edge.tag:
loop.edge.tag = True
self_selected = True
adjacent_face = radial_loop.face
# Only walk adjacent face if current face tagged the edge
if adjacent_face.tag and self_selected:
result += calc_face(adjacent_face, keep_caps)
if loop.edge.tag:
(selected, to_select)[self_selected].append(loop)
for loop in to_select:
result.append(loop.edge)
selected.append(loop)
# Select opposite edge in quads
if keep_caps and len(selected) == 1 and len(face.verts) == 4:
result.append(selected[0].link_loop_next.link_loop_next.edge)
return result
def get_edge_rings(bm, keep_caps=True):
def tag_face(face):
if face.select:
face.tag = True
for edge in face.edges: edge.tag = False
return face.select
# fetch selected faces while setting up tags
selected_faces = [ f for f in bm.faces if tag_face(f) ]
edges = []
try:
# generate a list of edges to select:
# traversing only tagged faces, since calc_face can walk and untag islands
for face in filter(lambda f: f.tag, selected_faces): edges += calc_face(face, keep_caps)
finally:
# housekeeping: clear tags
for face in selected_faces:
face.tag = False
for edge in face.edges: edge.tag = False
return edges
class MESH_xOT_deselect_boundary(bpy.types.Operator):
"""Deselect boundary edges of selected faces"""
bl_idname = "mesh.ext_deselect_boundary"
bl_label = "Deselect Boundary"
bl_options = {'REGISTER', 'UNDO'}
keep_cap_edges: bpy.props.BoolProperty(
name = "Keep Cap Edges",
description = "Keep quad strip cap edges selected",
default = False)
@classmethod
def poll(cls, context):
active_object = context.active_object
return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
def execute(self, context):
object = context.active_object
bm = bmesh_from_object(object)
try:
edges = get_edge_rings(bm, keep_caps = self.keep_cap_edges)
if not edges:
self.report({'WARNING'}, "No suitable selection found")
return {'CANCELLED'}
bpy.ops.mesh.select_all(action='DESELECT')
bm.select_mode = {'EDGE'}
for edge in edges:
edge.select = True
context.tool_settings.mesh_select_mode[:] = False, True, False
finally:
bmesh_release(bm, object)
return {'FINISHED'}
class MESH_xOT_cut_faces(bpy.types.Operator):
"""Cut selected faces, connecting through their adjacent edges"""
bl_idname = "mesh.ext_cut_faces"
bl_label = "Cut Faces"
bl_options = {'REGISTER', 'UNDO'}
# from bmesh_operators.h
INNERVERT = 0
PATH = 1
FAN = 2
STRAIGHT_CUT = 3
num_cuts: bpy.props.IntProperty(
name = "Number of Cuts",
default = 1,
min = 1,
max = 100,
subtype = 'UNSIGNED')
use_single_edge: bpy.props.BoolProperty(
name = "Quad/Tri Mode",
description = "Cut boundary faces",
default = False)
corner_type: bpy.props.EnumProperty(
items = [('INNER_VERT', "Inner Vert", ""),
('PATH', "Path", ""),
('FAN', "Fan", ""),
('STRAIGHT_CUT', "Straight Cut", ""),],
name = "Quad Corner Type",
description = "How to subdivide quad corners",
default = 'STRAIGHT_CUT')
use_grid_fill: bpy.props.BoolProperty(
name = "Use Grid Fill",
description = "Fill fully enclosed faces with a grid",
default = True)
@classmethod
def poll(cls, context):
active_object = context.active_object
return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
def cut_edges(self, context):
object = context.active_object
bm = bmesh_from_object(object)
try:
edges = get_edge_rings(bm, keep_caps = True)
if not edges:
self.report({'WARNING'}, "No suitable selection found")
return False
result = bmesh.ops.subdivide_edges(
bm,
edges = edges,
cuts = int(self.num_cuts),
use_grid_fill = bool(self.use_grid_fill),
use_single_edge = bool(self.use_single_edge),
quad_corner_type = str(self.corner_type))
bpy.ops.mesh.select_all(action='DESELECT')
bm.select_mode = {'EDGE'}
inner = result['geom_inner']
for edge in filter(lambda e: isinstance(e, bmesh.types.BMEdge), inner):
edge.select = True
finally:
bmesh_release(bm, object)
return True
def execute(self, context):
if not self.cut_edges(context):
return {'CANCELLED'}
context.tool_settings.mesh_select_mode[:] = False, True, False
# Try to select all possible loops
bpy.ops.mesh.loop_multi_select(ring=False)
return {'FINISHED'}
def menu_deselect_boundary(self, context):
self.layout.operator(MESH_xOT_deselect_boundary.bl_idname)
def menu_cut_faces(self, context):
self.layout.operator(MESH_xOT_cut_faces.bl_idname)
def register():
bpy.utils.register_class(MESH_xOT_deselect_boundary)
bpy.utils.register_class(MESH_xOT_cut_faces)
if __name__ != "__main__":
bpy.types.VIEW3D_MT_select_edit_mesh.append(menu_deselect_boundary)
bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu_cut_faces)
def unregister():
bpy.utils.unregister_class(MESH_xOT_deselect_boundary)
bpy.utils.unregister_class(MESH_xOT_cut_faces)
if __name__ != "__main__":
bpy.types.VIEW3D_MT_select_edit_mesh.remove(menu_deselect_boundary)
bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu_cut_faces)
if __name__ == "__main__":
register()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,384 @@
# ##### 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; version 2
# of the License.
#
# 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 LICENSE BLOCK #####
# based upon the functionality of Mesh to wall by luxuy_BlenderCN
# thanks to meta-androcto
bl_info = {
"name": "Edge Floor Plan",
"author": "lijenstina",
"version": (0, 2),
"blender": (2, 78, 0),
"location": "View3D > EditMode > Mesh",
"description": "Make a Floor Plan from Edges",
"wiki_url": "",
"category": "Mesh"}
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
FloatVectorProperty,
IntProperty,
)
# Handle error notifications
def error_handlers(self, error, reports="ERROR"):
if self and reports:
self.report({'WARNING'}, reports + " (See Console for more info)")
print("\n[mesh.edges_floor_plan]\nError: {}\n".format(error))
class MESH_OT_edges_floor_plan(Operator):
bl_idname = "mesh.edges_floor_plan"
bl_label = "Edges Floor Plan"
bl_description = "Top View, Extrude Flat Along Edges"
bl_options = {'REGISTER', 'UNDO'}
wid: FloatProperty(
name="Wall width:",
description="Set the width of the generated walls\n",
default=0.1,
min=0.001, max=30000
)
depth: FloatProperty(
name="Inner height:",
description="Set the height of the inner wall edges",
default=0.0,
min=0, max=10
)
connect_ends: BoolProperty(
name="Connect Ends",
description="Connect the ends of the boundary Edge loops",
default=False
)
repeat_cleanup: IntProperty(
name="Recursive Prepare",
description="Number of times that the preparation phase runs\n"
"at the start of the script\n"
"If parts of the mesh are not modified, increase this value",
min=1, max=20,
default=1
)
fill_items = [
('EDGE_NET', "Edge Net",
"Edge Net Method for mesh preparation - Initial Fill\n"
"The filled in faces will be Inset individually\n"
"Supports simple 3D objects"),
('SINGLE_FACE', "Single Face",
"Single Face Method for mesh preparation - Initial Fill\n"
"The produced face will be Triangulated before Inset Region\n"
"Good for edges forming a circle, avoid 3D objects"),
('SOLIDIFY', "Solidify",
"Extrude and Solidify Method\n"
"Useful for complex meshes, however works best on flat surfaces\n"
"as the extrude direction has to be defined")
]
fill_type: EnumProperty(
name="Fill Type",
items=fill_items,
description="Choose the method for creating geometry",
default='SOLIDIFY'
)
keep_faces: BoolProperty(
name="Keep Faces",
description="Keep or not the fill faces\n"
"Can depend on Remove Ngons state",
default=False
)
tri_faces: BoolProperty(
name="Triangulate Faces",
description="Triangulate the created fill faces\n"
"Sometimes can lead to unsatisfactory results",
default=False
)
initial_extrude: FloatVectorProperty(
name="Initial Extrude",
description="",
default=(0.0, 0.0, 0.1),
min=-20.0, max=20.0,
subtype='XYZ',
precision=3,
size=3
)
remove_ngons: BoolProperty(
name="Remove Ngons",
description="Keep or not the Ngon Faces\n"
"Note about limitations:\n"
"Sometimes the kept Faces could be Ngons\n"
"Removing the Ngons can lead to no geometry created",
default=True
)
offset: FloatProperty(
name="Wall Offset:",
description="Set the offset for the Solidify modifier",
default=0.0,
min=-1.0, max=1.0
)
only_rim: BoolProperty(
name="Rim Only",
description="Solidify Fill Rim only option",
default=False
)
@classmethod
def poll(cls, context):
ob = context.active_object
return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
def check_edge(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
obj = bpy.context.object
me_check = obj.data
if len(me_check.edges) < 1:
return False
return True
@staticmethod
def ensure(bm):
if bm:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
def solidify_mod(self, context, ob, wid, offset, only_rim):
try:
mods = ob.modifiers.new(
name="_Mesh_Solidify_Wall", type='SOLIDIFY'
)
mods.thickness = wid
mods.use_quality_normals = True
mods.offset = offset
mods.use_even_offset = True
mods.use_rim = True
mods.use_rim_only = only_rim
mods.show_on_cage = True
bpy.ops.object.modifier_apply(
modifier="_Mesh_Solidify_Wall"
)
except Exception as e:
error_handlers(self, e,
reports="Adding a Solidify Modifier failed")
pass
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="Choose Method:", icon="NONE")
box.prop(self, "fill_type")
col = box.column(align=True)
if self.fill_type == 'EDGE_NET':
col.prop(self, "repeat_cleanup")
col.prop(self, "remove_ngons", toggle=True)
elif self.fill_type == 'SOLIDIFY':
col.prop(self, "offset", slider=True)
col.prop(self, "initial_extrude")
else:
col.prop(self, "remove_ngons", toggle=True)
col.prop(self, "tri_faces", toggle=True)
box = layout.box()
box.label(text="Settings:", icon="NONE")
col = box.column(align=True)
col.prop(self, "wid")
if self.fill_type != 'SOLIDIFY':
col.prop(self, "depth")
col.prop(self, "connect_ends", toggle=True)
col.prop(self, "keep_faces", toggle=True)
else:
col.prop(self, "only_rim", toggle=True)
def execute(self, context):
if not self.check_edge(context):
self.report({'WARNING'},
"Operation Cancelled. Needs a Mesh with at least one edge")
return {'CANCELLED'}
wid = self.wid * 0.1
depth = self.depth * 0.1
offset = self.offset * 0.1
store_selection_mode = context.tool_settings.mesh_select_mode
# Note: the remove_doubles called after bmesh creation would make
# blender crash with certain meshes - keep it in mind for the future
bpy.ops.mesh.remove_doubles(threshold=0.003)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
ob = bpy.context.object
me = ob.data
bm = bmesh.from_edit_mesh(me)
bmesh.ops.delete(bm, geom=bm.faces, context='FACES_ONLY')
self.ensure(bm)
context.tool_settings.mesh_select_mode = (False, True, False)
original_edges = [edge.index for edge in bm.edges]
original_verts = [vert.index for vert in bm.verts]
self.ensure(bm)
bpy.ops.mesh.select_all(action='DESELECT')
if self.fill_type == 'EDGE_NET':
for i in range(self.repeat_cleanup):
bmesh.ops.edgenet_prepare(bm, edges=bm.edges)
self.ensure(bm)
bmesh.ops.edgenet_fill(bm, edges=bm.edges, mat_nr=0, use_smooth=True, sides=0)
self.ensure(bm)
if self.remove_ngons:
ngons = [face for face in bm.faces if len(face.edges) > 4]
self.ensure(bm)
bmesh.ops.delete(bm, geom=ngons, context='FACES') # 5 - delete faces
del ngons
self.ensure(bm)
elif self.fill_type == 'SOLIDIFY':
for vert in bm.verts:
vert.normal_update()
self.ensure(bm)
bmesh.ops.extrude_edge_only(
bm, edges=bm.edges, use_select_history=False
)
self.ensure(bm)
verts_extrude = [vert for vert in bm.verts if vert.index in original_verts]
self.ensure(bm)
bmesh.ops.translate(
bm,
verts=verts_extrude,
vec=(self.initial_extrude)
)
self.ensure(bm)
del verts_extrude
self.ensure(bm)
for edge in bm.edges:
if edge.is_boundary:
edge.select = True
bm = bmesh.update_edit_mesh(ob.data, 1, 1)
bpy.ops.object.mode_set(mode='OBJECT')
self.solidify_mod(context, ob, wid, offset, self.only_rim)
bpy.ops.object.mode_set(mode='EDIT')
context.tool_settings.mesh_select_mode = store_selection_mode
return {'FINISHED'}
else:
bm.faces.new(bm.verts)
self.ensure(bm)
if self.tri_faces:
bmesh.ops.triangle_fill(
bm, use_beauty=True, use_dissolve=False, edges=bm.edges
)
self.ensure(bm)
if self.remove_ngons and self.fill_type != 'EDGE_NET':
ngons = [face for face in bm.faces if len(face.edges) > 4]
self.ensure(bm)
bmesh.ops.delete(bm, geom=ngons, context='FACES') # 5 - delete faces
del ngons
self.ensure(bm)
del_boundary = [edge for edge in bm.edges if edge.index not in original_edges]
self.ensure(bm)
del original_edges
self.ensure(bm)
if self.fill_type == 'EDGE_NET':
extrude_inner = bmesh.ops.inset_individual(
bm, faces=bm.faces, thickness=wid, depth=depth,
use_even_offset=True, use_interpolate=False,
use_relative_offset=False
)
else:
extrude_inner = bmesh.ops.inset_region(
bm, faces=bm.faces, faces_exclude=[], use_boundary=True,
use_even_offset=True, use_interpolate=False,
use_relative_offset=False, use_edge_rail=False,
thickness=wid, depth=depth, use_outset=False
)
self.ensure(bm)
del_faces = [faces for faces in bm.faces if faces not in extrude_inner["faces"]]
self.ensure(bm)
del extrude_inner
self.ensure(bm)
if not self.keep_faces:
bmesh.ops.delete(bm, geom=del_faces, context='FACES') # 5 delete faces
del del_faces
self.ensure(bm)
face_del = set()
for face in bm.faces:
for edge in del_boundary:
if isinstance(edge, bmesh.types.BMEdge):
if edge in face.edges:
face_del.add(face)
self.ensure(bm)
face_del = list(face_del)
self.ensure(bm)
del del_boundary
self.ensure(bm)
if not self.connect_ends:
bmesh.ops.delete(bm, geom=face_del, context='FACES')
self.ensure(bm)
del face_del
self.ensure(bm)
for edge in bm.edges:
if edge.is_boundary:
edge.select = True
bm = bmesh.update_edit_mesh(ob.data, 1, 1)
context.tool_settings.mesh_select_mode = store_selection_mode
return {'FINISHED'}
def register():
bpy.utils.register_class(MESH_OT_edges_floor_plan)
def unregister():
bpy.utils.unregister_class(MESH_OT_edges_floor_plan)
if __name__ == "__main__":
register()

View File

@ -0,0 +1,341 @@
# gpl author: Giuseppe De Marco [BlenderLab] inspired by NirenYang
bl_info = {
"name": "Set edges length",
"description": "Edges length",
"author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang",
"version": (0, 1, 0),
"blender": (2, 71, 0),
"location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
"warning": "",
"wiki_url": "",
"category": "Mesh",
}
import bpy
import bmesh
from mathutils import Vector
from bpy.types import Operator
from bpy.props import (
FloatProperty,
EnumProperty,
)
# GLOBALS
edge_length_debug = False
_error_message = "Please select at least one edge to fill select history"
_error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead"
# Note : Refactor - removed all the operators apart from LengthSet
# and merged the other ones as options of length (lijenstina)
def get_edge_vector(edge):
verts = (edge.verts[0].co, edge.verts[1].co)
vector = verts[1] - verts[0]
return vector
def get_selected(bmesh_obj, geometry_type):
# geometry type should be edges, verts or faces
selected = []
for i in getattr(bmesh_obj, geometry_type):
if i.select:
selected.append(i)
return tuple(selected)
def get_center_vector(verts):
# verts = [Vector((x,y,z)), Vector((x,y,z))]
center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.),
((verts[1][1] + verts[0][1]) / 2.),
((verts[1][2] + verts[0][2]) / 2.)))
return center_vector
class LengthSet(Operator):
bl_idname = "object.mesh_edge_length_set"
bl_label = "Set edge length"
bl_description = ("Change one selected edge length by a specified target,\n"
"existing length and different modes\n"
"Note: works only with Edges that not share a vertex")
bl_options = {'REGISTER', 'UNDO'}
old_length: FloatProperty(
name="Original length",
options={'HIDDEN'},
)
set_length_type: EnumProperty(
items=[
('manual', "Manual",
"Input manually the desired Target Length"),
('existing', "Existing Length",
"Use existing geometry Edges' characteristics"),
],
name="Set Type of Input",
)
target_length: FloatProperty(
name="Target Length",
description="Input a value for an Edges Length target",
default=1.00,
unit='LENGTH',
precision=5
)
existing_length: EnumProperty(
items=[
('min', "Shortest",
"Set all to shortest Edge of selection"),
('max', "Longest",
"Set all to the longest Edge of selection"),
('average', "Average",
"Set all to the average Edge length of selection"),
('active', "Active",
"Set all to the active Edge's one\n"
"Needs a selection to be done in Edge Select mode"),
],
name="Existing length"
)
mode: EnumProperty(
items=[
('fixed', "Fixed", "Fixed"),
('increment', "Increment", "Increment"),
('decrement', "Decrement", "Decrement"),
],
name="Mode"
)
behaviour: EnumProperty(
items=[
('proportional', "Proportional",
"Move vertex locations proportionally to the center of the Edge"),
('clockwise', "Clockwise",
"Compute the Edges' vertex locations in a clockwise fashion"),
('unclockwise', "Counterclockwise",
"Compute the Edges' vertex locations in a counterclockwise fashion"),
],
name="Resize behavior"
)
originary_edge_length_dict = {}
edge_lengths = []
selected_edges = ()
@classmethod
def poll(cls, context):
return (context.edit_object and context.object.type == 'MESH')
def check(self, context):
return True
def draw(self, context):
layout = self.layout
layout.label(text="Original Active length is: {:.3f}".format(self.old_length))
layout.label(text="Input Mode:")
layout.prop(self, "set_length_type", expand=True)
if self.set_length_type == 'manual':
layout.prop(self, "target_length")
else:
layout.prop(self, "existing_length", text="")
layout.label(text="Mode:")
layout.prop(self, "mode", text="")
layout.label(text="Resize Behavior:")
layout.prop(self, "behaviour", text="")
def get_existing_edge_length(self, bm):
if self.existing_length != "active":
if self.existing_length == "min":
return min(self.edge_lengths)
if self.existing_length == "max":
return max(self.edge_lengths)
elif self.existing_length == "average":
return sum(self.edge_lengths) / float(len(self.selected_edges))
else:
bm.edges.ensure_lookup_table()
active_edge_length = None
for elem in reversed(bm.select_history):
if isinstance(elem, bmesh.types.BMEdge):
active_edge_length = elem.calc_length()
break
return active_edge_length
return 0.0
def invoke(self, context, event):
wm = context.window_manager
obj = context.edit_object
bm = bmesh.from_edit_mesh(obj.data)
bpy.ops.mesh.select_mode(type="EDGE")
self.selected_edges = get_selected(bm, 'edges')
if self.selected_edges:
vertex_set = []
for edge in self.selected_edges:
vector = get_edge_vector(edge)
if edge.verts[0].index not in vertex_set:
vertex_set.append(edge.verts[0].index)
else:
self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
return {'CANCELLED'}
if edge.verts[1].index not in vertex_set:
vertex_set.append(edge.verts[1].index)
else:
self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
return {'CANCELLED'}
# warning, it's a constant !
verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
self.originary_edge_length_dict[verts_index] = vector
self.edge_lengths.append(vector.length)
self.old_length = vector.length
else:
self.report({'ERROR'}, _error_message)
return {'CANCELLED'}
if edge_length_debug:
self.report({'INFO'}, str(self.originary_edge_length_dict))
if bpy.context.scene.unit_settings.system == 'IMPERIAL':
# imperial to metric conversion
vector.length = (0.9144 * vector.length) / 3
self.target_length = vector.length
return wm.invoke_props_dialog(self)
def execute(self, context):
bpy.ops.mesh.select_mode(type="EDGE")
self.context = context
obj = context.edit_object
bm = bmesh.from_edit_mesh(obj.data)
self.selected_edges = get_selected(bm, 'edges')
if not self.selected_edges:
self.report({'ERROR'}, _error_message)
return {'CANCELLED'}
for edge in self.selected_edges:
vector = get_edge_vector(edge)
# what we should see in original length dialog field
self.old_length = vector.length
if self.set_length_type == 'manual':
vector.length = abs(self.target_length)
else:
get_lengths = self.get_existing_edge_length(bm)
# check for edit mode
if not get_lengths:
self.report({'WARNING'},
"Operation Cancelled. "
"Active Edge could not be determined (needs selection in Edit Mode)")
return {'CANCELLED'}
vector.length = get_lengths
if vector.length == 0.0:
self.report({'ERROR'}, "Operation cancelled. Target length is set to zero")
return {'CANCELLED'}
center_vector = get_center_vector((edge.verts[0].co, edge.verts[1].co))
verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
if edge_length_debug:
self.report({'INFO'},
' - '.join(('vector ' + str(vector),
'originary_vector ' +
str(self.originary_edge_length_dict[verts_index])
)))
verts = (edge.verts[0].co, edge.verts[1].co)
if edge_length_debug:
self.report({'INFO'},
'\n edge.verts[0].co ' + str(verts[0]) +
'\n edge.verts[1].co ' + str(verts[1]) +
'\n vector.length' + str(vector.length))
# the clockwise direction have v1 -> v0, unclockwise v0 -> v1
if self.target_length >= 0:
if self.behaviour == 'proportional':
edge.verts[1].co = center_vector + vector / 2
edge.verts[0].co = center_vector - vector / 2
if self.mode == 'decrement':
edge.verts[0].co = (center_vector + vector / 2) - \
(self.originary_edge_length_dict[verts_index] / 2)
edge.verts[1].co = (center_vector - vector / 2) + \
(self.originary_edge_length_dict[verts_index] / 2)
elif self.mode == 'increment':
edge.verts[1].co = (center_vector + vector / 2) + \
self.originary_edge_length_dict[verts_index] / 2
edge.verts[0].co = (center_vector - vector / 2) - \
self.originary_edge_length_dict[verts_index] / 2
elif self.behaviour == 'unclockwise':
if self.mode == 'increment':
edge.verts[1].co = \
verts[0] + (self.originary_edge_length_dict[verts_index] + vector)
elif self.mode == 'decrement':
edge.verts[0].co = \
verts[1] - (self.originary_edge_length_dict[verts_index] - vector)
else:
edge.verts[1].co = verts[0] + vector
else:
# clockwise
if self.mode == 'increment':
edge.verts[0].co = \
verts[1] - (self.originary_edge_length_dict[verts_index] + vector)
elif self.mode == 'decrement':
edge.verts[1].co = \
verts[0] + (self.originary_edge_length_dict[verts_index] - vector)
else:
edge.verts[0].co = verts[1] - vector
if bpy.context.scene.unit_settings.system == 'IMPERIAL':
"""
# yards to metric conversion
vector.length = ( 3. * vector.length ) / 0.9144
# metric to yards conversion
vector.length = ( 0.9144 * vector.length ) / 3.
"""
for mvert in edge.verts:
# school time: 0.9144 : 3 = X : mvert
mvert.co = (0.9144 * mvert.co) / 3
if edge_length_debug:
self.report({'INFO'},
'\n edge.verts[0].co' + str(verts[0]) +
'\n edge.verts[1].co' + str(verts[1]) +
'\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0]))
)
bmesh.update_edit_mesh(obj.data, True)
return {'FINISHED'}
def register():
bpy.utils.register_class(LengthSet)
def unregister():
bpy.utils.unregister_class(LengthSet)
if __name__ == "__main__":
register()

1880
mesh_tools/mesh_edgetools.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,377 @@
# ##### 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 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/>.
#
# ##### END GPL LICENSE BLOCK #####
# Contact for more information about the Addon:
# Email: germano.costa@ig.com.br
# Twitter: wii_mano @mano_wii
bl_info = {
"name": "Extrude and Reshape",
"author": "Germano Cavalcante",
"version": (0, 8, 1),
"blender": (2, 80, 0),
"location": "View3D > UI > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)",
"description": "Extrude face and merge edge intersections "
"between the mesh and the new edges",
"wiki_url": "http://blenderartists.org/forum/"
"showthread.php?376618-Addon-Push-Pull-Face",
"category": "Mesh"}
import bpy
import bmesh
from mathutils.geometry import intersect_line_line
from bpy.types import Operator
class BVHco():
i = 0
c1x = 0.0
c1y = 0.0
c1z = 0.0
c2x = 0.0
c2y = 0.0
c2z = 0.0
def edges_BVH_overlap(bm, edges, epsilon=0.0001):
bco = set()
for e in edges:
bvh = BVHco()
bvh.i = e.index
b1 = e.verts[0]
b2 = e.verts[1]
co1 = b1.co.x
co2 = b2.co.x
if co1 <= co2:
bvh.c1x = co1 - epsilon
bvh.c2x = co2 + epsilon
else:
bvh.c1x = co2 - epsilon
bvh.c2x = co1 + epsilon
co1 = b1.co.y
co2 = b2.co.y
if co1 <= co2:
bvh.c1y = co1 - epsilon
bvh.c2y = co2 + epsilon
else:
bvh.c1y = co2 - epsilon
bvh.c2y = co1 + epsilon
co1 = b1.co.z
co2 = b2.co.z
if co1 <= co2:
bvh.c1z = co1 - epsilon
bvh.c2z = co2 + epsilon
else:
bvh.c1z = co2 - epsilon
bvh.c2z = co1 + epsilon
bco.add(bvh)
del edges
overlap = {}
oget = overlap.get
for e1 in bm.edges:
by = bz = True
a1 = e1.verts[0]
a2 = e1.verts[1]
c1x = a1.co.x
c2x = a2.co.x
if c1x > c2x:
tm = c1x
c1x = c2x
c2x = tm
for bvh in bco:
if c1x <= bvh.c2x and c2x >= bvh.c1x:
if by:
by = False
c1y = a1.co.y
c2y = a2.co.y
if c1y > c2y:
tm = c1y
c1y = c2y
c2y = tm
if c1y <= bvh.c2y and c2y >= bvh.c1y:
if bz:
bz = False
c1z = a1.co.z
c2z = a2.co.z
if c1z > c2z:
tm = c1z
c1z = c2z
c2z = tm
if c1z <= bvh.c2z and c2z >= bvh.c1z:
e2 = bm.edges[bvh.i]
if e1 != e2:
overlap[e1] = oget(e1, set()).union({e2})
return overlap
def intersect_edges_edges(overlap, precision=4):
epsilon = .1**precision
fpre_min = -epsilon
fpre_max = 1 + epsilon
splits = {}
sp_get = splits.get
new_edges1 = set()
new_edges2 = set()
targetmap = {}
for edg1 in overlap:
# print("***", ed1.index, "***")
for edg2 in overlap[edg1]:
a1 = edg1.verts[0]
a2 = edg1.verts[1]
b1 = edg2.verts[0]
b2 = edg2.verts[1]
# test if are linked
if a1 in {b1, b2} or a2 in {b1, b2}:
# print('linked')
continue
aco1, aco2 = a1.co, a2.co
bco1, bco2 = b1.co, b2.co
tp = intersect_line_line(aco1, aco2, bco1, bco2)
if tp:
p1, p2 = tp
if (p1 - p2).to_tuple(precision) == (0, 0, 0):
v = aco2 - aco1
f = p1 - aco1
x, y, z = abs(v.x), abs(v.y), abs(v.z)
max1 = 0 if x >= y and x >= z else\
1 if y >= x and y >= z else 2
fac1 = f[max1] / v[max1]
v = bco2 - bco1
f = p2 - bco1
x, y, z = abs(v.x), abs(v.y), abs(v.z)
max2 = 0 if x >= y and x >= z else\
1 if y >= x and y >= z else 2
fac2 = f[max2] / v[max2]
if fpre_min <= fac1 <= fpre_max:
# print(edg1.index, 'can intersect', edg2.index)
ed1 = edg1
elif edg1 in splits:
for ed1 in splits[edg1]:
a1 = ed1.verts[0]
a2 = ed1.verts[1]
vco1 = a1.co
vco2 = a2.co
v = vco2 - vco1
f = p1 - vco1
fac1 = f[max1] / v[max1]
if fpre_min <= fac1 <= fpre_max:
# print(e.index, 'can intersect', edg2.index)
break
else:
# print(edg1.index, 'really does not intersect', edg2.index)
continue
else:
# print(edg1.index, 'not intersect', edg2.index)
continue
if fpre_min <= fac2 <= fpre_max:
# print(ed1.index, 'actually intersect', edg2.index)
ed2 = edg2
elif edg2 in splits:
for ed2 in splits[edg2]:
b1 = ed2.verts[0]
b2 = ed2.verts[1]
vco1 = b1.co
vco2 = b2.co
v = vco2 - vco1
f = p2 - vco1
fac2 = f[max2] / v[max2]
if fpre_min <= fac2 <= fpre_max:
# print(ed1.index, 'actually intersect', e.index)
break
else:
# print(ed1.index, 'really does not intersect', ed2.index)
continue
else:
# print(ed1.index, 'not intersect', edg2.index)
continue
new_edges1.add(ed1)
new_edges2.add(ed2)
if abs(fac1) <= epsilon:
nv1 = a1
elif fac1 + epsilon >= 1:
nv1 = a2
else:
ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1)
new_edges1.add(ne1)
splits[edg1] = sp_get(edg1, set()).union({ne1})
if abs(fac2) <= epsilon:
nv2 = b1
elif fac2 + epsilon >= 1:
nv2 = b2
else:
ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2)
new_edges2.add(ne2)
splits[edg2] = sp_get(edg2, set()).union({ne2})
if nv1 != nv2: # necessary?
targetmap[nv1] = nv2
return new_edges1, new_edges2, targetmap
class ER_OT_Extrude_and_Reshape(Operator):
bl_idname = "mesh.extrude_reshape"
bl_label = "Extrude and Reshape"
bl_description = "Push and pull face entities to sculpt 3d models"
bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
@classmethod
def poll(cls, context):
if context.mode=='EDIT_MESH':
return True
def modal(self, context, event):
if self.confirm:
sface = self.bm.faces.active
if not sface:
for face in self.bm.faces:
if face.select is True:
sface = face
break
else:
return {'FINISHED'}
# edges to intersect
edges = set()
[[edges.add(ed) for ed in v.link_edges] for v in sface.verts]
overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001)
overlap = {k: v for k, v in overlap.items() if k not in edges} # remove repetition
"""
print([e.index for e in edges])
for a, b in overlap.items():
print(a.index, [e.index for e in b])
"""
new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap)
pos_weld = set()
for e in new_edges1:
v1, v2 = e.verts
if v1 in targetmap and v2 in targetmap:
pos_weld.add((targetmap[v1], targetmap[v2]))
if targetmap:
bmesh.ops.weld_verts(self.bm, targetmap=targetmap)
"""
print([e.is_valid for e in new_edges1])
print([e.is_valid for e in new_edges2])
sp_faces1 = set()
"""
for e in pos_weld:
v1, v2 = e
lf1 = set(v1.link_faces)
lf2 = set(v2.link_faces)
rlfe = lf1.intersection(lf2)
for f in rlfe:
try:
nf = bmesh.utils.face_split(f, v1, v2)
# sp_faces1.update({f, nf[0]})
except:
pass
# sp_faces2 = set()
for e in new_edges2:
lfe = set(e.link_faces)
v1, v2 = e.verts
lf1 = set(v1.link_faces)
lf2 = set(v2.link_faces)
rlfe = lf1.intersection(lf2)
for f in rlfe.difference(lfe):
nf = bmesh.utils.face_split(f, v1, v2)
# sp_faces2.update({f, nf[0]})
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
return {'FINISHED'}
if self.cancel:
return {'FINISHED'}
self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'}
self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}
return {'PASS_THROUGH'}
def execute(self, context):
self.mesh = context.object.data
self.bm = bmesh.from_edit_mesh(self.mesh)
try:
selection = self.bm.select_history[-1]
except:
for face in self.bm.faces:
if face.select is True:
selection = face
break
else:
return {'FINISHED'}
if not isinstance(selection, bmesh.types.BMFace):
bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT')
return {'FINISHED'}
else:
face = selection
# face.select = False
bpy.ops.mesh.select_all(action='DESELECT')
geom = []
for edge in face.edges:
if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: # self.angle_tolerance:
geom.append(edge)
ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face])
for face in ret_dict['faces']:
self.bm.faces.active = face
face.select = True
sface = face
dfaces = bmesh.ops.dissolve_edges(
self.bm, edges=geom, use_verts=True, use_face_split=False
)
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
bpy.ops.transform.translate(
'INVOKE_DEFAULT', constraint_axis=(False, False, True),
orient_type='NORMAL', release_confirm=True
)
context.window_manager.modal_handler_add(self)
self.cancel = False
self.confirm = False
return {'RUNNING_MODAL'}
def operator_draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.operator("mesh.extrude_reshape")
def register():
bpy.utils.register_class(ER_OT_Extrude_and_Reshape)
def unregister():
bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape)
if __name__ == "__main__":
register()

View File

@ -0,0 +1,430 @@
# -*- coding: utf-8 -*-
# ##### END 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 LICENSE BLOCK #####
bl_info = {
"name": "FilletPlus",
"author": "Gert De Roost - original by zmj100",
"version": (0, 4, 3),
"blender": (2, 80, 0),
"location": "View3D > Tool Shelf",
"description": "",
"warning": "",
"wiki_url": "",
"category": "Mesh"}
import bpy
from bpy.props import (
FloatProperty,
IntProperty,
BoolProperty,
)
from bpy.types import Operator
import bmesh
from mathutils import Matrix
from math import (
cos, pi, sin,
degrees, tan,
)
def list_clear_(l):
if l:
del l[:]
return l
def get_adj_v_(list_):
tmp = {}
for i in list_:
try:
tmp[i[0]].append(i[1])
except KeyError:
tmp[i[0]] = [i[1]]
try:
tmp[i[1]].append(i[0])
except KeyError:
tmp[i[1]] = [i[0]]
return tmp
class f_buf():
# one of the angles was not 0 or 180
check = False
def fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius):
try:
dict_0 = get_adj_v_(list_0)
list_1 = [[dict_0[i][0], i, dict_0[i][1]] for i in dict_0 if (len(dict_0[i]) == 2)][0]
list_3 = []
for elem in list_1:
list_3.append(bm.verts[elem])
list_2 = []
p_ = list_3[1]
p = (list_3[1].co).copy()
p1 = (list_3[0].co).copy()
p2 = (list_3[2].co).copy()
vec1 = p - p1
vec2 = p - p2
ang = vec1.angle(vec2, any)
check_angle = round(degrees(ang))
if check_angle == 180 or check_angle == 0.0:
return False
else:
f_buf.check = True
opp = adj
if radius is False:
h = adj * (1 / cos(ang * 0.5))
adj_ = adj
elif radius is True:
h = opp / sin(ang * 0.5)
adj_ = opp / tan(ang * 0.5)
p3 = p - (vec1.normalized() * adj_)
p4 = p - (vec2.normalized() * adj_)
rp = p - ((p - ((p3 + p4) * 0.5)).normalized() * h)
vec3 = rp - p3
vec4 = rp - p4
axis = vec1.cross(vec2)
if out is False:
if flip is False:
rot_ang = vec3.angle(vec4)
elif flip is True:
rot_ang = vec1.angle(vec2)
elif out is True:
rot_ang = (2 * pi) - vec1.angle(vec2)
for j in range(n + 1):
new_angle = rot_ang * j / n
mtrx = Matrix.Rotation(new_angle, 3, axis)
if out is False:
if flip is False:
tmp = p4 - rp
tmp1 = mtrx @ tmp
tmp2 = tmp1 + rp
elif flip is True:
p3 = p - (vec1.normalized() * opp)
tmp = p3 - p
tmp1 = mtrx @ tmp
tmp2 = tmp1 + p
elif out is True:
p4 = p - (vec2.normalized() * opp)
tmp = p4 - p
tmp1 = mtrx @ tmp
tmp2 = tmp1 + p
v = bm.verts.new(tmp2)
list_2.append(v)
if flip is True:
list_3[1:2] = list_2
else:
list_2.reverse()
list_3[1:2] = list_2
list_clear_(list_2)
n1 = len(list_3)
for t in range(n1 - 1):
bm.edges.new([list_3[t], list_3[(t + 1) % n1]])
v = bm.verts.new(p)
bm.edges.new([v, p_])
bm.edges.ensure_lookup_table()
if face is not None:
for l in face.loops:
if l.vert == list_3[0]:
startl = l
break
vertlist2 = []
if startl.link_loop_next.vert == startv:
l = startl.link_loop_prev
while len(vertlist) > 0:
vertlist2.insert(0, l.vert)
vertlist.pop(vertlist.index(l.vert))
l = l.link_loop_prev
else:
l = startl.link_loop_next
while len(vertlist) > 0:
vertlist2.insert(0, l.vert)
vertlist.pop(vertlist.index(l.vert))
l = l.link_loop_next
for v in list_3:
vertlist2.append(v)
bm.faces.new(vertlist2)
if startv.is_valid:
bm.verts.remove(startv)
else:
print("\n[Function fillets Error]\n"
"Starting vertex (startv var) couldn't be removed\n")
return False
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
list_3[1].select = 1
list_3[-2].select = 1
bm.edges.get([list_3[0], list_3[1]]).select = 1
bm.edges.get([list_3[-1], list_3[-2]]).select = 1
bm.verts.index_update()
bm.edges.index_update()
bm.faces.index_update()
me.update(calc_edges=True, calc_loop_triangles=True)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
except Exception as e:
print("\n[Function fillets Error]\n{}\n".format(e))
return False
def do_filletplus(self, pair):
is_finished = True
try:
startv = None
global inaction
global flip
list_0 = [list([e.verts[0].index, e.verts[1].index]) for e in pair]
vertset = set([])
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
vertset.add(bm.verts[list_0[0][0]])
vertset.add(bm.verts[list_0[0][1]])
vertset.add(bm.verts[list_0[1][0]])
vertset.add(bm.verts[list_0[1][1]])
v1, v2, v3 = vertset
if len(list_0) != 2:
self.report({'WARNING'}, "Two adjacent edges must be selected")
is_finished = False
else:
inaction = 1
vertlist = []
found = 0
for f in v1.link_faces:
if v2 in f.verts and v3 in f.verts:
found = 1
if not found:
for v in [v1, v2, v3]:
if v.index in list_0[0] and v.index in list_0[1]:
startv = v
face = None
else:
for f in v1.link_faces:
if v2 in f.verts and v3 in f.verts:
for v in f.verts:
if not(v in vertset):
vertlist.append(v)
if (v in vertset and v.link_loops[0].link_loop_prev.vert in vertset and
v.link_loops[0].link_loop_next.vert in vertset):
startv = v
face = f
if out is True:
flip = False
if startv:
fills = fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius)
if not fills:
is_finished = False
else:
is_finished = False
except Exception as e:
print("\n[Function do_filletplus Error]\n{}\n".format(e))
is_finished = False
return is_finished
def check_is_not_coplanar(bm_data):
from mathutils import Vector
check = False
angles, norm_angle = 0, 0
z_vec = Vector((0, 0, 1))
try:
bm_data.faces.ensure_lookup_table()
for f in bm_data.faces:
norm_angle = f.normal.angle(z_vec)
if angles == 0:
angles = norm_angle
if angles != norm_angle:
check = True
break
except Exception as e:
print("\n[Function check_is_not_coplanar Error]\n{}\n".format(e))
check = True
return check
# Operator
class MESH_OT_fillet_plus(Operator):
bl_idname = "mesh.fillet_plus"
bl_label = "Fillet Plus"
bl_description = ("Fillet adjoining edges\n"
"Note: Works on a mesh whose all faces share the same normal")
bl_options = {"REGISTER", "UNDO"}
adj: FloatProperty(
name="",
description="Size of the filleted corners",
default=0.1,
min=0.00001, max=100.0,
step=1,
precision=3
)
n: IntProperty(
name="",
description="Subdivision of the filleted corners",
default=3,
min=1, max=50,
step=1
)
out: BoolProperty(
name="Outside",
description="Fillet towards outside",
default=False
)
flip: BoolProperty(
name="Flip",
description="Flip the direction of the Fillet\n"
"Only available if Outside option is not active",
default=False
)
radius: BoolProperty(
name="Radius",
description="Use radius for the size of the filleted corners",
default=False
)
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
def draw(self, context):
layout = self.layout
if f_buf.check is False:
layout.label(text="Angle is equal to 0 or 180", icon="INFO")
layout.label(text="Can not fillet", icon="BLANK1")
else:
layout.prop(self, "radius")
if self.radius is True:
layout.label(text="Radius:")
elif self.radius is False:
layout.label(text="Distance:")
layout.prop(self, "adj")
layout.label(text="Number of sides:")
layout.prop(self, "n")
if self.n > 1:
row = layout.row(align=False)
row.prop(self, "out")
if self.out is False:
row.prop(self, "flip")
def execute(self, context):
global inaction
global bm, me, adj, n, out, flip, radius, f_buf
adj = self.adj
n = self.n
out = self.out
flip = self.flip
radius = self.radius
inaction = 0
f_buf.check = False
ob_act = context.active_object
try:
me = ob_act.data
bm = bmesh.from_edit_mesh(me)
warn_obj = bool(check_is_not_coplanar(bm))
if warn_obj is False:
tempset = set([])
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
for v in bm.verts:
if v.select and v.is_boundary:
tempset.add(v)
for v in tempset:
edgeset = set([])
for e in v.link_edges:
if e.select and e.is_boundary:
edgeset.add(e)
if len(edgeset) == 2:
is_finished = do_filletplus(self, edgeset)
if not is_finished:
break
if inaction == 1:
bpy.ops.mesh.select_all(action="DESELECT")
for v in bm.verts:
if len(v.link_edges) == 0:
bm.verts.remove(v)
bpy.ops.object.editmode_toggle()
bpy.ops.object.editmode_toggle()
else:
self.report({'WARNING'}, "Filletplus operation could not be performed")
return {'CANCELLED'}
else:
self.report({'WARNING'}, "Mesh is not a coplanar surface. Operation cancelled")
return {'CANCELLED'}
except:
self.report({'WARNING'}, "Filletplus operation could not be performed")
return {'CANCELLED'}
return {'FINISHED'}
# define classes for registration
classes = (
MESH_OT_fillet_plus,
)
# registering and menu integration
def register():
for cls in classes:
bpy.utils.register_class(cls)
# unregistering and removing menus
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()

View File

@ -0,0 +1,370 @@
# ##### 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 LICENSE BLOCK #####
# Repeats extrusion + rotation + scale for one or more faces
# Original code by liero
# Update by Jimmy Hazevoet 03/2017 for Blender 2.79
# normal rotation, probability, scaled offset, object coords, initial and per step noise
bl_info = {
"name": "MExtrude Plus1",
"author": "liero, Jimmy Hazevoet",
"version": (1, 3, 0),
"blender": (2, 77, 0),
"location": "View3D > Tool Shelf",
"description": "Repeat extrusions from faces to create organic shapes",
"warning": "",
"wiki_url": "",
"category": "Mesh"}
import bpy
import bmesh
import random
from bpy.types import Operator
from random import gauss
from math import radians
from mathutils import (
Euler, Vector,
)
from bpy.props import (
FloatProperty,
IntProperty,
BoolProperty,
)
def gloc(self, r):
return Vector((self.offx, self.offy, self.offz))
def vloc(self, r):
random.seed(self.ran + r)
return self.off * (1 + gauss(0, self.var1 / 3))
def nrot(self, n):
return Euler((radians(self.nrotx) * n[0],
radians(self.nroty) * n[1],
radians(self.nrotz) * n[2]), 'XYZ')
def vrot(self, r):
random.seed(self.ran + r)
return Euler((radians(self.rotx) + gauss(0, self.var2 / 3),
radians(self.roty) + gauss(0, self.var2 / 3),
radians(self.rotz) + gauss(0, self.var2 / 3)), 'XYZ')
def vsca(self, r):
random.seed(self.ran + r)
return self.sca * (1 + gauss(0, self.var3 / 3))
class MExtrude(Operator):
bl_idname = "object.mextrude"
bl_label = "Multi Extrude"
bl_description = ("Extrude selected Faces with Rotation,\n"
"Scaling, Variation, Randomization")
bl_options = {"REGISTER", "UNDO", "PRESET"}
off: FloatProperty(
name="Offset",
soft_min=0.001, soft_max=10,
min=-100, max=100,
default=1.0,
description="Translation"
)
offx: FloatProperty(
name="Loc X",
soft_min=-10.0, soft_max=10.0,
min=-100.0, max=100.0,
default=0.0,
description="Global Translation X"
)
offy: FloatProperty(
name="Loc Y",
soft_min=-10.0, soft_max=10.0,
min=-100.0, max=100.0,
default=0.0,
description="Global Translation Y"
)
offz: FloatProperty(
name="Loc Z",
soft_min=-10.0, soft_max=10.0,
min=-100.0, max=100.0,
default=0.0,
description="Global Translation Z"
)
rotx: FloatProperty(
name="Rot X",
min=-85, max=85,
soft_min=-30, soft_max=30,
default=0,
description="X Rotation"
)
roty: FloatProperty(
name="Rot Y",
min=-85, max=85,
soft_min=-30,
soft_max=30,
default=0,
description="Y Rotation"
)
rotz: FloatProperty(
name="Rot Z",
min=-85, max=85,
soft_min=-30, soft_max=30,
default=-0,
description="Z Rotation"
)
nrotx: FloatProperty(
name="N Rot X",
min=-85, max=85,
soft_min=-30, soft_max=30,
default=0,
description="Normal X Rotation"
)
nroty: FloatProperty(
name="N Rot Y",
min=-85, max=85,
soft_min=-30, soft_max=30,
default=0,
description="Normal Y Rotation"
)
nrotz: FloatProperty(
name="N Rot Z",
min=-85, max=85,
soft_min=-30, soft_max=30,
default=-0,
description="Normal Z Rotation"
)
sca: FloatProperty(
name="Scale",
min=0.01, max=10,
soft_min=0.5, soft_max=1.5,
default=1.0,
description="Scaling of the selected faces after extrusion"
)
var1: FloatProperty(
name="Offset Var", min=-10, max=10,
soft_min=-1, soft_max=1,
default=0,
description="Offset variation"
)
var2: FloatProperty(
name="Rotation Var",
min=-10, max=10,
soft_min=-1, soft_max=1,
default=0,
description="Rotation variation"
)
var3: FloatProperty(
name="Scale Noise",
min=-10, max=10,
soft_min=-1, soft_max=1,
default=0,
description="Scaling noise"
)
var4: IntProperty(
name="Probability",
min=0, max=100,
default=100,
description="Probability, chance of extruding a face"
)
num: IntProperty(
name="Repeat",
min=1, max=500,
soft_max=100,
default=5,
description="Repetitions"
)
ran: IntProperty(
name="Seed",
min=-9999, max=9999,
default=0,
description="Seed to feed random values"
)
opt1: BoolProperty(
name="Polygon coordinates",
default=True,
description="Polygon coordinates, Object coordinates"
)
opt2: BoolProperty(
name="Proportional offset",
default=False,
description="Scale * Offset"
)
opt3: BoolProperty(
name="Per step rotation noise",
default=False,
description="Per step rotation noise, Initial rotation noise"
)
opt4: BoolProperty(
name="Per step scale noise",
default=False,
description="Per step scale noise, Initial scale noise"
)
@classmethod
def poll(cls, context):
obj = context.object
return (obj and obj.type == 'MESH')
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.label(text="Transformations:")
col.prop(self, "off", slider=True)
col.prop(self, "offx", slider=True)
col.prop(self, "offy", slider=True)
col.prop(self, "offz", slider=True)
col = layout.column(align=True)
col.prop(self, "rotx", slider=True)
col.prop(self, "roty", slider=True)
col.prop(self, "rotz", slider=True)
col.prop(self, "nrotx", slider=True)
col.prop(self, "nroty", slider=True)
col.prop(self, "nrotz", slider=True)
col = layout.column(align=True)
col.prop(self, "sca", slider=True)
col = layout.column(align=True)
col.label(text="Variation settings:")
col.prop(self, "var1", slider=True)
col.prop(self, "var2", slider=True)
col.prop(self, "var3", slider=True)
col.prop(self, "var4", slider=True)
col.prop(self, "ran")
col = layout.column(align=False)
col.prop(self, 'num')
col = layout.column(align=True)
col.label(text="Options:")
col.prop(self, "opt1")
col.prop(self, "opt2")
col.prop(self, "opt3")
col.prop(self, "opt4")
def execute(self, context):
obj = bpy.context.object
om = obj.mode
bpy.context.tool_settings.mesh_select_mode = [False, False, True]
origin = Vector([0.0, 0.0, 0.0])
# bmesh operations
bpy.ops.object.mode_set()
bm = bmesh.new()
bm.from_mesh(obj.data)
sel = [f for f in bm.faces if f.select]
after = []
# faces loop
for i, of in enumerate(sel):
nro = nrot(self, of.normal)
off = vloc(self, i)
loc = gloc(self, i)
of.normal_update()
# initial rotation noise
if self.opt3 is False:
rot = vrot(self, i)
# initial scale noise
if self.opt4 is False:
s = vsca(self, i)
# extrusion loop
for r in range(self.num):
# random probability % for extrusions
if self.var4 > int(random.random() * 100):
nf = of.copy()
nf.normal_update()
no = nf.normal.copy()
# face/obj coördinates
if self.opt1 is True:
ce = nf.calc_center_bounds()
else:
ce = origin
# per step rotation noise
if self.opt3 is True:
rot = vrot(self, i + r)
# per step scale noise
if self.opt4 is True:
s = vsca(self, i + r)
# proportional, scale * offset
if self.opt2 is True:
off = s * off
for v in nf.verts:
v.co -= ce
v.co.rotate(nro)
v.co.rotate(rot)
v.co += ce + loc + no * off
v.co = v.co.lerp(ce, 1 - s)
# extrude code from TrumanBlending
for a, b in zip(of.loops, nf.loops):
sf = bm.faces.new((a.vert, a.link_loop_next.vert,
b.link_loop_next.vert, b.vert))
sf.normal_update()
bm.faces.remove(of)
of = nf
after.append(of)
for v in bm.verts:
v.select = False
for e in bm.edges:
e.select = False
for f in after:
if f not in sel:
f.select = True
else:
f.select = False
bm.to_mesh(obj.data)
obj.data.update()
# restore user settings
bpy.ops.object.mode_set(mode=om)
if not len(sel):
self.report({"WARNING"},
"No suitable Face selection found. Operation cancelled")
return {'CANCELLED'}
return {'FINISHED'}
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == '__main__':
register()

View 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,
'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=[('', "", ""),
('15°', "15°", "15°"),
('30°', "30°", "30°"),
('45°', "45°", "45°"),
('60°', "60°", "60°"),
('75°', "75°", "75°"),
('90°', "90°", "90°"), ],
name="Angle Presets", default='',
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()

View File

@ -0,0 +1,164 @@
# ##### 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 LICENSE BLOCK #####
# <pep8 compliant>
bl_info = {
"name": "Vertex Chamfer",
"author": "Andrew Hale (TrumanBlending)",
"version": (0, 1),
"blender": (2, 63, 0),
"location": "Spacebar Menu",
"description": "Chamfer vertex",
"wiki_url": "",
"category": "Mesh"}
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import (
BoolProperty,
FloatProperty,
)
class VertexChamfer(Operator):
bl_idname = "mesh.vertex_chamfer"
bl_label = "Chamfer Vertex"
bl_description = "Tri chamfer selected vertices"
bl_options = {'REGISTER', 'UNDO'}
factor: FloatProperty(
name="Factor",
description="Size of the Champfer",
default=0.1,
min=0.0,
soft_max=1.0
)
relative: BoolProperty(
name="Relative",
description="If Relative, Champfer size is relative to the edge length",
default=True
)
dissolve: BoolProperty(
name="Remove",
description="Remove/keep the original selected vertices\n"
"Remove creates a new triangle face between the Champfer edges,\n"
"similar to the Dissolve Vertices operator",
default=True
)
displace: FloatProperty(
name="Displace",
description="Active only if Remove option is disabled\n"
"Displaces the original selected vertices along the normals\n"
"defined by the Champfer edges",
soft_min=-5.0,
soft_max=5.0
)
@classmethod
def poll(self, context):
return (context.active_object.type == 'MESH' and
context.mode == 'EDIT_MESH')
def draw(self, context):
layout = self.layout
layout.prop(self, "factor", text="Distance" if self.relative else "Factor")
sub = layout.row()
sub.prop(self, "relative")
sub.prop(self, "dissolve")
if not self.dissolve:
layout.prop(self, "displace")
def execute(self, context):
ob = context.active_object
me = ob.data
bm = bmesh.from_edit_mesh(me)
bm.select_flush(True)
fac = self.factor
rel = self.relative
dissolve = self.dissolve
displace = self.displace
for v in bm.verts:
v.tag = False
# Loop over edges to find those with both verts selected
for e in bm.edges[:]:
e.tag = e.select
if not e.select:
continue
elen = e.calc_length()
val = fac if rel else fac / elen
val = min(val, 0.5)
# Loop over the verts of the edge to split
for v in e.verts:
# if val == 0.5 and e.other_vert(v).tag:
# continue
en, vn = bmesh.utils.edge_split(e, v, val)
en.tag = vn.tag = True
val = 1.0 if val == 1.0 else val / (1.0 - val)
# Get all verts which are selected but not created previously
verts = [v for v in bm.verts if v.select and not v.tag]
# Loop over all verts to split their linked edges
for v in verts:
for e in v.link_edges[:]:
if e.tag:
continue
elen = e.calc_length()
val = fac if rel else fac / elen
bmesh.utils.edge_split(e, v, val)
# Loop over all the loops of the vert
for l in v.link_loops:
# Split the face
bmesh.utils.face_split(
l.face,
l.link_loop_next.vert,
l.link_loop_prev.vert
)
# Remove the vert or displace otherwise
if dissolve:
bmesh.utils.vert_dissolve(v)
else:
v.co += displace * v.normal
me.calc_loop_triangles()
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
def register():
bpy.utils.register_class(VertexChamfer)
def unregister():
bpy.utils.unregister_class(VertexChamfer)
if __name__ == "__main__":
register()

842
mesh_tools/pkhg_faces.py Normal file
View File

@ -0,0 +1,842 @@
# gpl author: PHKG
bl_info = {
"name": "PKHG faces",
"author": "PKHG",
"version": (0, 0, 6),
"blender": (2, 71, 0),
"location": "View3D > Tools > PKHG (tab)",
"description": "Faces selected will become added faces of different style",
"warning": "",
"wiki_url": "",
"category": "Mesh",
}
import bpy
import bmesh
from bpy.types import Operator
from mathutils import Vector
from bpy.props import (
BoolProperty,
StringProperty,
IntProperty,
FloatProperty,
EnumProperty,
)
class MESH_OT_add_faces_to_object(Operator):
bl_idname = "mesh.add_faces_to_object"
bl_label = "Face Shape"
bl_description = "Set parameters and build object with added faces"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
reverse_faces: BoolProperty(
name="Reverse Faces",
default=False,
description="Revert the normals of selected faces"
)
name_source_object: StringProperty(
name="Mesh",
description="Choose a Source Mesh",
default="Cube"
)
remove_start_faces: BoolProperty(
name="Remove Start Faces",
default=True,
description="Make a choice about removal of Original Faces"
)
base_height: FloatProperty(
name="Base Height",
min=-20,
soft_max=10, max=20,
default=0.2,
description="Set general Base Height"
)
use_relative_base_height: BoolProperty(
name="Relative Base Height",
default=False,
description="Relative or absolute Base Height"
)
second_height: FloatProperty(
name="2nd height", min=-5,
soft_max=5, max=20,
default=0.2,
description="Second height for various shapes"
)
width: FloatProperty(
name="Width Faces",
min=-20, max=20,
default=0.5,
description="Set general width"
)
repeat_extrude: IntProperty(
name="Repeat",
min=1,
soft_max=5, max=20,
description="For longer base"
)
move_inside: FloatProperty(
name="Move Inside",
min=0.0,
max=1.0,
default=0.5,
description="How much move to inside"
)
thickness: FloatProperty(
name="Thickness",
soft_min=0.01, min=0,
soft_max=5.0, max=20.0,
default=0
)
depth: FloatProperty(
name="Depth",
min=-5,
soft_max=5.0, max=20.0,
default=0
)
collapse_edges: BoolProperty(
name="Make Point",
default=False,
description="Collapse the vertices of edges"
)
spike_base_width: FloatProperty(
name="Spike Base Width",
default=0.4,
min=-4.0,
soft_max=1, max=20,
description="Base width of a spike"
)
base_height_inset: FloatProperty(
name="Base Height Inset",
default=0.0,
min=-5, max=5,
description="To elevate or drop the Base height Inset"
)
top_spike: FloatProperty(
name="Top Spike",
default=1.0,
min=-10.0, max=10.0,
description="The Base Height of a spike"
)
top_extra_height: FloatProperty(
name="Top Extra Height",
default=0.0,
min=-10.0, max=10.0,
description="Add extra height"
)
step_with_real_spike: BoolProperty(
name="Step with Real Spike",
default=False,
description="In stepped, use a real spike"
)
use_relative: BoolProperty(
name="Use Relative",
default=False,
description="Change size using area, min or max"
)
face_types: EnumProperty(
name="Face Types",
description="Different types of Faces",
default="no",
items=[
('no', "Pick an Option", "Choose one of the available options"),
('open_inset', "Open Inset", "Inset without closing faces (holes)"),
('with_base', "With Base", "Base and ..."),
('clsd_vertical', "Closed Vertical", "Closed Vertical"),
('open_vertical', "Open Vertical", "Open Vertical"),
('spiked', "Spiked", "Spike"),
('stepped', "Stepped", "Stepped"),
('boxed', "Boxed", "Boxed"),
('bar', "Bar", "Bar"),
]
)
strange_boxed_effect: BoolProperty(
name="Strange Effect",
default=False,
description="Do not show one extrusion"
)
use_boundary: BoolProperty(
name="Use Boundary",
default=True
)
use_even_offset: BoolProperty(
name="Even Offset",
default=True
)
use_relative_offset: BoolProperty(
name="Relative Offset",
default=True
)
use_edge_rail: BoolProperty(
name="Edge Rail",
default=False
)
use_outset: BoolProperty(
name="Outset",
default=False
)
use_select_inset: BoolProperty(
name="Inset",
default=False
)
use_interpolate: BoolProperty(
name="Interpolate",
default=True
)
@classmethod
def poll(cls, context):
result = False
active_object = context.active_object
if active_object:
mesh_objects_name = [el.name for el in bpy.data.objects if el.type == "MESH"]
if active_object.name in mesh_objects_name:
result = True
return result
def draw(self, context):
layout = self.layout
col = layout.column()
col.separator()
col.label(text="Using Active Object", icon="INFO")
col.separator()
col.label(text="Face Types:")
col.prop(self, "face_types", text="")
col.separator()
col.prop(self, "use_relative")
if self.face_types == "open_inset":
col.prop(self, "move_inside")
col.prop(self, "base_height")
elif self.face_types == "with_base":
col.prop(self, "move_inside")
col.prop(self, "base_height")
col.prop(self, "second_height")
col.prop(self, "width")
elif self.face_types == "clsd_vertical":
col.prop(self, "base_height")
elif self.face_types == "open_vertical":
col.prop(self, "base_height")
elif self.face_types == "boxed":
col.prop(self, "move_inside")
col.prop(self, "base_height")
col.prop(self, "top_spike")
col.prop(self, "strange_boxed_effect")
elif self.face_types == "spiked":
col.prop(self, "spike_base_width")
col.prop(self, "base_height_inset")
col.prop(self, "top_spike")
elif self.face_types == "bar":
col.prop(self, "spike_base_width")
col.prop(self, "top_spike")
col.prop(self, "top_extra_height")
elif self.face_types == "stepped":
col.prop(self, "spike_base_width")
col.prop(self, "base_height_inset")
col.prop(self, "top_extra_height")
col.prop(self, "second_height")
col.prop(self, "step_with_real_spike")
def execute(self, context):
obj_name = self.name_source_object
face_type = self.face_types
is_selected = check_is_selected()
if not is_selected:
self.report({'WARNING'},
"Operation Cancelled. No selected Faces found on the Active Object")
return {'CANCELLED'}
if face_type == "spiked":
Spiked(spike_base_width=self.spike_base_width,
base_height_inset=self.base_height_inset,
top_spike=self.top_spike, top_relative=self.use_relative)
elif face_type == "boxed":
startinfo = prepare(self, context, self.remove_start_faces)
bm = startinfo['bm']
top = self.top_spike
obj = startinfo['obj']
obj_matrix_local = obj.matrix_local
distance = None
base_heights = None
t = self.move_inside
areas = startinfo['areas']
base_height = self.base_height
if self.use_relative:
distance = [min(t * area, 1.0) for i, area in enumerate(areas)]
base_heights = [base_height * area for i, area in enumerate(areas)]
else:
distance = [t] * len(areas)
base_heights = [base_height] * len(areas)
rings = startinfo['rings']
centers = startinfo['centers']
normals = startinfo['normals']
for i in range(len(rings)):
make_one_inset(self, context, bm=bm, ringvectors=rings[i],
center=centers[i], normal=normals[i],
t=distance[i], base_height=base_heights[i])
bpy.ops.mesh.select_mode(type="EDGE")
bpy.ops.mesh.select_more()
bpy.ops.mesh.select_more()
bpy.ops.object.mode_set(mode='OBJECT')
# PKHG>INFO base extrusion done and set to the mesh
# PKHG>INFO if the extrusion is NOT done ... it'll look strange soon!
if not self.strange_boxed_effect:
bpy.ops.object.mode_set(mode='EDIT')
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
bmfaces = [face for face in bm.faces if face.select]
res = extrude_faces(self, context, bm=bm, face_l=bmfaces)
ring_edges = [face.edges[:] for face in res]
bpy.ops.object.mode_set(mode='OBJECT')
# PKHG>INFO now the extruded facec have to move in normal direction
bpy.ops.object.mode_set(mode='EDIT')
obj = bpy.context.view_layer.objects.active
bm = bmesh.from_edit_mesh(obj.data)
todo_faces = [face for face in bm.faces if face.select]
for face in todo_faces:
bmesh.ops.translate(bm, vec=face.normal * top, space=obj_matrix_local,
verts=face.verts)
bpy.ops.object.mode_set(mode='OBJECT')
elif face_type == "stepped":
Stepped(spike_base_width=self.spike_base_width,
base_height_inset=self.base_height_inset,
top_spike=self.second_height,
top_extra_height=self.top_extra_height,
use_relative_offset=self.use_relative, with_spike=self.step_with_real_spike)
elif face_type == "open_inset":
startinfo = prepare(self, context, self.remove_start_faces)
bm = startinfo['bm']
# PKHG>INFO adjust for relative, via areas
t = self.move_inside
areas = startinfo['areas']
base_height = self.base_height
base_heights = None
distance = None
if self.use_relative:
distance = [min(t * area, 1.0) for i, area in enumerate(areas)]
base_heights = [base_height * area for i, area in enumerate(areas)]
else:
distance = [t] * len(areas)
base_heights = [base_height] * len(areas)
rings = startinfo['rings']
centers = startinfo['centers']
normals = startinfo['normals']
for i in range(len(rings)):
make_one_inset(self, context, bm=bm, ringvectors=rings[i],
center=centers[i], normal=normals[i],
t=distance[i], base_height=base_heights[i])
bpy.ops.object.mode_set(mode='OBJECT')
elif face_type == "with_base":
startinfo = prepare(self, context, self.remove_start_faces)
bm = startinfo['bm']
obj = startinfo['obj']
object_matrix = obj.matrix_local
# PKHG>INFO for relative (using areas)
t = self.move_inside
areas = startinfo['areas']
base_height = self.base_height
distance = None
base_heights = None
if self.use_relative:
distance = [min(t * area, 1.0) for i, area in enumerate(areas)]
base_heights = [base_height * area for i, area in enumerate(areas)]
else:
distance = [t] * len(areas)
base_heights = [base_height] * len(areas)
next_rings = []
rings = startinfo['rings']
centers = startinfo['centers']
normals = startinfo['normals']
for i in range(len(rings)):
next_rings.append(make_one_inset(self, context, bm=bm, ringvectors=rings[i],
center=centers[i], normal=normals[i],
t=distance[i], base_height=base_heights[i]))
prepare_ring = extrude_edges(self, context, bm=bm, edge_l_l=next_rings)
second_height = self.second_height
width = self.width
vectors = [[ele.verts[:] for ele in edge] for edge in prepare_ring]
n_ring_vecs = []
for rings in vectors:
v = []
for edgv in rings:
v.extend(edgv)
# PKHF>INFO no double verts allowed, coming from two adjacents edges!
bm.verts.ensure_lookup_table()
vv = list(set([ele.index for ele in v]))
vvv = [bm.verts[i].co for i in vv]
n_ring_vecs.append(vvv)
for i, ring in enumerate(n_ring_vecs):
make_one_inset(self, context, bm=bm, ringvectors=ring,
center=centers[i], normal=normals[i],
t=width, base_height=base_heights[i] + second_height)
bpy.ops.object.mode_set(mode='OBJECT')
else:
if face_type == "clsd_vertical":
obj_name = context.active_object.name
ClosedVertical(name=obj_name, base_height=self.base_height,
use_relative_base_height=self.use_relative)
elif face_type == "open_vertical":
obj_name = context.active_object.name
OpenVertical(name=obj_name, base_height=self.base_height,
use_relative_base_height=self.use_relative)
elif face_type == "bar":
startinfo = prepare(self, context, self.remove_start_faces)
result = []
bm = startinfo['bm']
rings = startinfo['rings']
centers = startinfo['centers']
normals = startinfo['normals']
spike_base_width = self.spike_base_width
for i, ring in enumerate(rings):
result.append(make_one_inset(self, context, bm=bm,
ringvectors=ring, center=centers[i],
normal=normals[i], t=spike_base_width))
next_ring_edges_list = extrude_edges(self, context, bm=bm,
edge_l_l=result)
top_spike = self.top_spike
fac = top_spike
object_matrix = startinfo['obj'].matrix_local
for i in range(len(next_ring_edges_list)):
translate_ONE_ring(
self, context, bm=bm,
object_matrix=object_matrix,
ring_edges=next_ring_edges_list[i],
normal=normals[i], distance=fac
)
next_ring_edges_list_2 = extrude_edges(self, context, bm=bm,
edge_l_l=next_ring_edges_list)
top_extra_height = self.top_extra_height
for i in range(len(next_ring_edges_list_2)):
move_corner_vecs_outside(
self, context, bm=bm,
edge_list=next_ring_edges_list_2[i],
center=centers[i], normal=normals[i],
base_height_erlier=fac + top_extra_height,
distance=fac
)
bpy.ops.mesh.select_mode(type="VERT")
bpy.ops.mesh.select_more()
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
def find_one_ring(sel_vertices):
ring0 = sel_vertices.pop(0)
to_delete = []
for i, edge in enumerate(sel_vertices):
len_nu = len(ring0)
if len(ring0 - edge) < len_nu:
to_delete.append(i)
ring0 = ring0.union(edge)
to_delete.reverse()
for el in to_delete:
sel_vertices.pop(el)
return (ring0, sel_vertices)
class Stepped:
def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2,
top_relative=False, top_extra_height=0, use_relative_offset=False,
with_spike=False):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset,
use_edge_rail=False, thickness=top_extra_height, depth=base_height_inset,
use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset,
use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
if with_spike:
bpy.ops.mesh.merge(type='COLLAPSE')
bpy.ops.object.mode_set(mode='OBJECT')
class Spiked:
def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2, top_relative=False):
obj = bpy.context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=False, thickness=spike_base_width, depth=base_height_inset,
use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=top_relative,
use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bm = bmesh.from_edit_mesh(obj.data)
bpy.ops.mesh.merge(type='COLLAPSE')
bpy.ops.object.mode_set(mode='OBJECT')
class ClosedVertical:
def __init__(self, name="Plane", base_height=1, use_relative_base_height=False):
obj = bpy.data.objects[name]
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(obj.data)
# PKHG>INFO deselect chosen faces
sel = [f for f in bm.faces if f.select]
for f in sel:
f.select = False
res = bmesh.ops.extrude_discrete_faces(bm, faces=sel)
# PKHG>INFO select extruded faces
for f in res['faces']:
f.select = True
factor = base_height
for face in res['faces']:
if use_relative_base_height:
area = face.calc_area()
factor = area * base_height
else:
factor = base_height
for el in face.verts:
tmp = el.co + face.normal * factor
el.co = tmp
me = bpy.data.meshes[name]
bm.to_mesh(me)
bm.free()
class OpenVertical:
def __init__(self, name="Plane", base_height=1, use_relative_base_height=False):
obj = bpy.data.objects[name]
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(obj.data)
# PKHG>INFO deselect chosen faces
sel = [f for f in bm.faces if f.select]
for f in sel:
f.select = False
res = bmesh.ops.extrude_discrete_faces(bm, faces=sel)
# PKHG>INFO select extruded faces
for f in res['faces']:
f.select = True
# PKHG>INFO adjust extrusion by a vector
factor = base_height
for face in res['faces']:
if use_relative_base_height:
area = face.calc_area()
factor = area * base_height
else:
factor = base_height
for el in face.verts:
tmp = el.co + face.normal * factor
el.co = tmp
me = bpy.data.meshes[name]
bm.to_mesh(me)
bm.free()
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.delete(type='FACE')
bpy.ops.object.editmode_toggle()
class StripFaces:
def __init__(self, use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=True, thickness=0.0, depth=0.0, use_outset=False,
use_select_inset=False, use_individual=True, use_interpolate=True):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.inset(
use_boundary=use_boundary, use_even_offset=True, use_relative_offset=False,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=use_outset,
use_select_inset=use_select_inset, use_individual=use_individual,
use_interpolate=use_interpolate
)
bpy.ops.object.mode_set(mode='OBJECT')
# PKHG>IMFO only 3 parameters inc execution context supported!!
if False:
bpy.ops.mesh.inset(
use_boundary, use_even_offset, use_relative_offset, use_edge_rail,
thickness, depth, use_outset, use_select_inset, use_individual,
use_interpolate
)
elif type == 0:
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False,
use_select_inset=False, use_individual=True, use_interpolate=True
)
elif type == 1:
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=True, use_relative_offset=False,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False,
use_select_inset=False, use_individual=True, use_interpolate=False
)
bpy.ops.mesh.delete(type='FACE')
elif type == 2:
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=False, use_relative_offset=True,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False,
use_select_inset=False, use_individual=True, use_interpolate=False
)
bpy.ops.mesh.delete(type='FACE')
elif type == 3:
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=False, use_relative_offset=True,
use_edge_rail=True, thickness=depth, depth=thickness, use_outset=False,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.delete(type='FACE')
elif type == 4:
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=False, use_relative_offset=True,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.inset(
use_boundary=True, use_even_offset=False, use_relative_offset=True,
use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True,
use_select_inset=False, use_individual=True, use_interpolate=True
)
bpy.ops.mesh.delete(type='FACE')
bpy.ops.object.mode_set(mode='OBJECT')
def check_is_selected():
is_selected = False
for face in bpy.context.active_object.data.polygons:
if face.select:
is_selected = True
break
return is_selected
def prepare(self, context, remove_start_faces=True):
"""
Start for a face selected change of faces
select an object of type mesh, with activated several (all) faces
"""
obj = bpy.context.view_layer.objects.active
bpy.ops.object.mode_set(mode='OBJECT')
selectedpolygons = [el for el in obj.data.polygons if el.select]
# PKHG>INFO copies of the vectors are needed, otherwise Blender crashes!
centers = [face.center for face in selectedpolygons]
centers_copy = [Vector((el[0], el[1], el[2])) for el in centers]
normals = [face.normal for face in selectedpolygons]
normals_copy = [Vector((el[0], el[1], el[2])) for el in normals]
vertindicesofpolgons = [
[vert for vert in face.vertices] for face in selectedpolygons
]
vertVectorsOfSelectedFaces = [
[obj.data.vertices[ind].co for ind in vertIndiceofface] for
vertIndiceofface in vertindicesofpolgons
]
vertVectorsOfSelectedFaces_copy = [
[Vector((el[0], el[1], el[2])) for el in listofvecs] for
listofvecs in vertVectorsOfSelectedFaces
]
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
selected_bm_faces = [ele for ele in bm.faces if ele.select]
selected_edges_per_face_ind = [
[ele.index for ele in face.edges] for face in selected_bm_faces
]
indices = [el.index for el in selectedpolygons]
selected_faces_areas = [bm.faces[:][i] for i in indices]
tmp_area = [el.calc_area() for el in selected_faces_areas]
# PKHG>INFO, selected faces are removed, only their edges are used!
if remove_start_faces:
bpy.ops.mesh.delete(type='ONLY_FACE')
bpy.ops.object.mode_set(mode='OBJECT')
obj.data.update()
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
start_ring_raw = [
[bm.verts[ind].index for ind in vertIndiceofface] for
vertIndiceofface in vertindicesofpolgons
]
start_ring = []
for el in start_ring_raw:
start_ring.append(set(el))
bm.edges.ensure_lookup_table()
bm_selected_edges_l_l = [
[bm.edges[i] for i in bm_ind_list] for
bm_ind_list in selected_edges_per_face_ind
]
result = {
'obj': obj, 'centers': centers_copy, 'normals': normals_copy,
'rings': vertVectorsOfSelectedFaces_copy, 'bm': bm,
'areas': tmp_area, 'startBMRingVerts': start_ring,
'base_edges': bm_selected_edges_l_l
}
return result
def make_one_inset(self, context, bm=None, ringvectors=None, center=None,
normal=None, t=None, base_height=0):
# a face will get 'inserted' faces to create (normally) a hole if t is > 0 and < 1)
tmp = []
for el in ringvectors:
tmp.append((el * (1 - t) + center * t) + normal * base_height)
tmp = [bm.verts.new(v) for v in tmp] # the new corner bmvectors
# PKHG>INFO so to say sentinells, to use ONE for ...
tmp.append(tmp[0])
vectorsFace_i = [bm.verts.new(v) for v in ringvectors]
vectorsFace_i.append(vectorsFace_i[0])
myres = []
for ii in range(len(vectorsFace_i) - 1):
# PKHG>INFO next line: sequence is important! for added edge
bmvecs = [vectorsFace_i[ii], vectorsFace_i[ii + 1], tmp[ii + 1], tmp[ii]]
res = bm.faces.new(bmvecs)
myres.append(res.edges[2])
myres[-1].select = True # PKHG>INFO to be used later selected!
return (myres)
def extrude_faces(self, context, bm=None, face_l=None):
# to make a ring extrusion
res = bmesh.ops.extrude_discrete_faces(bm, faces=face_l)['faces']
for face in res:
face.select = True
return res
def extrude_edges(self, context, bm=None, edge_l_l=None):
# to make a ring extrusion
all_results = []
for edge_l in edge_l_l:
for edge in edge_l:
edge.select = False
res = bmesh.ops.extrude_edge_only(bm, edges=edge_l)
tmp = [ele for ele in res['geom'] if isinstance(ele, bmesh.types.BMEdge)]
for edge in tmp:
edge.select = True
all_results.append(tmp)
return all_results
def translate_ONE_ring(self, context, bm=None, object_matrix=None, ring_edges=None,
normal=(0, 0, 1), distance=0.5):
# translate a ring in given (normal?!) direction with given (global) amount
tmp = []
for edge in ring_edges:
tmp.extend(edge.verts[:])
# PKHG>INFO no double vertices allowed by bmesh!
tmp = set(tmp)
tmp = list(tmp)
bmesh.ops.translate(bm, vec=normal * distance, space=object_matrix, verts=tmp)
# PKHG>INFO relevant edges will stay selected
return ring_edges
def move_corner_vecs_outside(self, context, bm=None, edge_list=None, center=None,
normal=None, base_height_erlier=0.5, distance=0.5):
# move corners (outside meant mostly) dependent on the parameters
tmp = []
for edge in edge_list:
tmp.extend([ele for ele in edge.verts if isinstance(ele, bmesh.types.BMVert)])
# PKHG>INFO to remove vertices, they are all used twice in the ring!
tmp = set(tmp)
tmp = list(tmp)
for i in range(len(tmp)):
vec = tmp[i].co
direction = vec + (vec - (normal * base_height_erlier + center)) * distance
tmp[i].co = direction
# define classes for registration
classes = (
MESH_OT_add_faces_to_object,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()

View File

@ -0,0 +1,140 @@
# gpl authors: Oscurart, Greg
bl_info = {
"name": "Random Vertices",
"author": "Oscurart, Greg",
"version": (1, 3),
"blender": (2, 63, 0),
"location": "Object > Transform > Random Vertices",
"description": "Randomize selected components of active object",
"warning": "",
"wiki_url": "",
"category": "Mesh"}
import bpy
from bpy.types import Operator
import random
import bmesh
from bpy.props import (
BoolProperty,
FloatProperty,
IntVectorProperty,
)
def add_object(self, context, valmin, valmax, factor, vgfilter):
# select an option with weight map or not
mode = bpy.context.active_object.mode
# generate variables
objact = bpy.context.active_object
listver = []
warn_message = False
# switch to edit mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
# bmesh object
odata = bmesh.from_edit_mesh(objact.data)
odata.select_flush(False)
# if the vertex is selected add to the list
for vertice in odata.verts[:]:
if vertice.select:
listver.append(vertice.index)
# If the minimum value is greater than the maximum,
# it adds a value to the maximum
if valmin[0] >= valmax[0]:
valmax[0] = valmin[0] + 1
if valmin[1] >= valmax[1]:
valmax[1] = valmin[1] + 1
if valmin[2] >= valmax[2]:
valmax[2] = valmin[2] + 1
odata.verts.ensure_lookup_table()
random_factor = factor
for vertice in listver:
odata.verts.ensure_lookup_table()
if odata.verts[vertice].select:
if vgfilter is True:
has_group = getattr(objact.data.vertices[vertice], "groups", None)
vertex_group = has_group[0] if has_group else None
vertexweight = getattr(vertex_group, "weight", None)
if vertexweight:
random_factor = factor * vertexweight
else:
random_factor = factor
warn_message = True
odata.verts[vertice].co = (
(((random.randrange(valmin[0], valmax[0], 1)) * random_factor) / 1000) +
odata.verts[vertice].co[0],
(((random.randrange(valmin[1], valmax[1], 1)) * random_factor) / 1000) +
odata.verts[vertice].co[1],
(((random.randrange(valmin[2], valmax[2], 1)) * random_factor) / 1000) +
odata.verts[vertice].co[2]
)
if warn_message:
self.report({'WARNING'},
"Some of the Selected Vertices don't have a Group with Vertex Weight assigned")
bpy.ops.object.mode_set(mode=mode)
class MESH_OT_random_vertices(Operator):
bl_idname = "mesh.random_vertices"
bl_label = "Random Vertices"
bl_description = ("Randomize the location of vertices by a specified\n"
"Multiplier Factor and random values in the defined range\n"
"or a multiplication of them and the Vertex Weights")
bl_options = {'REGISTER', 'UNDO'}
vgfilter: BoolProperty(
name="Vertex Group",
description="Use Vertex Weight defined in the Active Group",
default=False
)
factor: FloatProperty(
name="Factor",
description="Base Multiplier of the randomization effect",
default=1
)
valmin: IntVectorProperty(
name="Min XYZ",
description="Define the minimum range of randomization values",
default=(0, 0, 0)
)
valmax: IntVectorProperty(
name="Max XYZ",
description="Define the maximum range of randomization values",
default=(1, 1, 1)
)
@classmethod
def poll(cls, context):
return (context.object and context.object.type == "MESH" and
context.mode == "EDIT_MESH")
def execute(self, context):
add_object(self, context, self.valmin, self.valmax, self.factor, self.vgfilter)
return {'FINISHED'}
# Registration
def register():
bpy.utils.register_class(MESH_OT_random_vertices)
def unregister():
bpy.utils.unregister_class(MESH_OT_random_vertices)
if __name__ == '__main__':
register()

View File

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
# ##### 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 LICENSE BLOCK #####
bl_info = {
"name": "Split Solidify",
"author": "zmj100, updated by zeffii to BMesh",
"version": (0, 1, 2),
"blender": (2, 80, 0),
"location": "View3D > Tool Shelf",
"description": "",
"warning": "",
"wiki_url": "",
"category": "Mesh"}
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import (
EnumProperty,
FloatProperty,
BoolProperty,
)
import random
from math import cos
# define the functions
def solidify_split(self, list_0):
loc_random = self.loc_random
random_dist = self.random_dist
distance = self.distance
thickness = self.thickness
normal_extr = self.normal_extr
bm = self.bm
for fi in list_0:
bm.faces.ensure_lookup_table()
f = bm.faces[fi]
list_1 = []
list_2 = []
if loc_random:
d = random_dist * random.randrange(0, 10)
elif not loc_random:
d = distance
# add new vertices
for vi in f.verts:
bm.verts.ensure_lookup_table()
v = bm.verts[vi.index]
if normal_extr == 'opt0':
p1 = (v.co).copy() + ((f.normal).copy() * d) # out
p2 = (v.co).copy() + ((f.normal).copy() * (d - thickness)) # in
elif normal_extr == 'opt1':
ang = ((v.normal).copy()).angle((f.normal).copy())
h = thickness / cos(ang)
p1 = (v.co).copy() + ((f.normal).copy() * d)
p2 = p1 + (-h * (f.normal).copy())
v1 = bm.verts.new(p1)
v2 = bm.verts.new(p2)
v1.select = False
v2.select = False
list_1.append(v1)
list_2.append(v2)
# add new faces, allows faces with more than 4 verts
n = len(list_1)
k = bm.faces.new(list_1)
k.select = False
for i in range(n):
j = (i + 1) % n
vseq = list_1[i], list_2[i], list_2[j], list_1[j]
k = bm.faces.new(vseq)
k.select = False
list_2.reverse()
k = bm.faces.new(list_2)
k.select = False
bpy.ops.mesh.normals_make_consistent(inside=False)
bmesh.update_edit_mesh(self.me, True)
class MESH_OT_split_solidify(Operator):
bl_idname = "mesh.split_solidify"
bl_label = "Split Solidify"
bl_description = "Split and Solidify selected Faces"
bl_options = {"REGISTER", "UNDO"}
distance: FloatProperty(
name="",
description="Distance of the splitted Faces to the original geometry",
default=0.4,
min=-100.0, max=100.0,
step=1,
precision=3
)
thickness: FloatProperty(
name="",
description="Thickness of the splitted Faces",
default=0.04,
min=-100.0, max=100.0,
step=1,
precision=3
)
random_dist: FloatProperty(
name="",
description="Randomization factor of the splitted Faces' location",
default=0.06,
min=-10.0, max=10.0,
step=1,
precision=3
)
loc_random: BoolProperty(
name="Random",
description="Randomize the locations of splitted faces",
default=False
)
del_original: BoolProperty(
name="Delete original faces",
default=True
)
normal_extr: EnumProperty(
items=(('opt0', "Face", "Solidify along Face Normals"),
('opt1', "Vertex", "Solidify along Vertex Normals")),
name="Normal",
default='opt0'
)
def draw(self, context):
layout = self.layout
layout.label(text="Normal:")
layout.prop(self, "normal_extr", expand=True)
layout.prop(self, "loc_random")
if not self.loc_random:
layout.label(text="Distance:")
layout.prop(self, "distance")
elif self.loc_random:
layout.label(text="Random distance:")
layout.prop(self, "random_dist")
layout.label(text="Thickness:")
layout.prop(self, "thickness")
layout.prop(self, "del_original")
def execute(self, context):
obj = bpy.context.active_object
self.me = obj.data
self.bm = bmesh.from_edit_mesh(self.me)
self.me.update()
list_0 = [f.index for f in self.bm.faces if f.select]
if len(list_0) == 0:
self.report({'WARNING'},
"No suitable selection found. Operation cancelled")
return {'CANCELLED'}
elif len(list_0) != 0:
solidify_split(self, list_0)
context.tool_settings.mesh_select_mode = (True, True, True)
if self.del_original:
bpy.ops.mesh.delete(type='FACE')
else:
pass
return {'FINISHED'}
def register():
bpy.utils.register_class(MESH_OT_split_solidify)
def unregister():
bpy.utils.unregister_class(MESH_OT_split_solidify)
if __name__ == "__main__":
register()

301
mesh_tools/vertex_align.py Normal file
View File

@ -0,0 +1,301 @@
# -*- coding: utf-8 -*-
# ##### 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 LICENSE BLOCK #####
# Note: Property group was moved to __init__
bl_info = {
"name": "Vertex Align",
"author": "",
"version": (0, 1, 7),
"blender": (2, 61, 0),
"location": "View3D > Tool Shelf",
"description": "",
"warning": "",
"wiki_url": "",
"category": "Mesh"}
import bpy
from bpy.props import (
BoolVectorProperty,
FloatVectorProperty,
)
from mathutils import Vector
from bpy.types import Operator
# Edit Mode Toggle
def edit_mode_out():
bpy.ops.object.mode_set(mode='OBJECT')
def edit_mode_in():
bpy.ops.object.mode_set(mode='EDIT')
def get_mesh_data_():
edit_mode_out()
ob_act = bpy.context.active_object
me = ob_act.data
edit_mode_in()
return me
def list_clear_(l):
l[:] = []
return l
class va_buf():
list_v = []
list_0 = []
# Store The Vertex coordinates
class Vertex_align_store(Operator):
bl_idname = "vertex_align.store_id"
bl_label = "Active Vertex"
bl_description = ("Store Selected Vertex coordinates as an align point\n"
"Single Selected Vertex only")
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
def execute(self, context):
try:
me = get_mesh_data_()
list_0 = [v.index for v in me.vertices if v.select]
if len(list_0) == 1:
list_clear_(va_buf.list_v)
for v in me.vertices:
if v.select:
va_buf.list_v.append(v.index)
bpy.ops.mesh.select_all(action='DESELECT')
else:
self.report({'WARNING'}, "Please select just One Vertex")
return {'CANCELLED'}
except:
self.report({'WARNING'}, "Storing selection could not be completed")
return {'CANCELLED'}
self.report({'INFO'}, "Selected Vertex coordinates are stored")
return {'FINISHED'}
# Align to original
class Vertex_align_original(Operator):
bl_idname = "vertex_align.align_original"
bl_label = "Align to original"
bl_description = "Align selection to stored single vertex coordinates"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
def draw(self, context):
layout = self.layout
layout.label(text="Axis:")
row = layout.row(align=True)
row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
text="X", index=0, toggle=True)
row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
text="Y", index=1, toggle=True)
row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
text="Z", index=2, toggle=True)
def execute(self, context):
edit_mode_out()
ob_act = context.active_object
me = ob_act.data
cen1 = context.scene.mesh_extra_tools.vert_align_axis
list_0 = [v.index for v in me.vertices if v.select]
if len(va_buf.list_v) == 0:
self.report({'INFO'},
"Original vertex not stored in memory. Operation Cancelled")
edit_mode_in()
return {'CANCELLED'}
elif len(va_buf.list_v) != 0:
if len(list_0) == 0:
self.report({'INFO'}, "No vertices selected. Operation Cancelled")
edit_mode_in()
return {'CANCELLED'}
elif len(list_0) != 0:
vo = (me.vertices[va_buf.list_v[0]].co).copy()
if cen1[0] is True:
for i in list_0:
v = (me.vertices[i].co).copy()
me.vertices[i].co = Vector((vo[0], v[1], v[2]))
if cen1[1] is True:
for i in list_0:
v = (me.vertices[i].co).copy()
me.vertices[i].co = Vector((v[0], vo[1], v[2]))
if cen1[2] is True:
for i in list_0:
v = (me.vertices[i].co).copy()
me.vertices[i].co = Vector((v[0], v[1], vo[2]))
edit_mode_in()
return {'FINISHED'}
# Align to custom coordinates
class Vertex_align_coord_list(Operator):
bl_idname = "vertex_align.coord_list_id"
bl_label = ""
bl_description = "Align to custom coordinates"
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
def execute(self, context):
edit_mode_out()
ob_act = context.active_object
me = ob_act.data
list_clear_(va_buf.list_0)
va_buf.list_0 = [v.index for v in me.vertices if v.select][:]
if len(va_buf.list_0) == 0:
self.report({'INFO'}, "No vertices selected. Operation Cancelled")
edit_mode_in()
return {'CANCELLED'}
elif len(va_buf.list_0) != 0:
bpy.ops.vertex_align.coord_menu_id('INVOKE_DEFAULT')
edit_mode_in()
return {'FINISHED'}
# Align to custom coordinates menu
class Vertex_align_coord_menu(Operator):
bl_idname = "vertex_align.coord_menu_id"
bl_label = "Tweak custom coordinates"
bl_description = "Change the custom coordinates for aligning"
bl_options = {'REGISTER', 'UNDO'}
def_axis_coord: FloatVectorProperty(
name="",
description="Enter the values of coordinates",
default=(0.0, 0.0, 0.0),
min=-100.0, max=100.0,
step=1, size=3,
subtype='XYZ',
precision=3
)
use_axis_coord = BoolVectorProperty(
name="Axis",
description="Choose Custom Coordinates axis",
default=(False,) * 3,
size=3,
)
is_not_undo = False
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH')
def using_store(self, context):
scene = context.scene
return scene.mesh_extra_tools.vert_align_use_stored
def draw(self, context):
layout = self.layout
if self.using_store(context) and self.is_not_undo:
layout.label(text="Using Stored Coordinates", icon="INFO")
row = layout.split(0.25)
row.prop(self, "use_axis_coord", index=0, text="X")
row.prop(self, "def_axis_coord", index=0)
row = layout.split(0.25)
row.prop(self, "use_axis_coord", index=1, text="Y")
row.prop(self, "def_axis_coord", index=1)
row = layout.split(0.25)
row.prop(self, "use_axis_coord", index=2, text="Z")
row.prop(self, "def_axis_coord", index=2)
def invoke(self, context, event):
self.is_not_undo = True
scene = context.scene
if self.using_store(context):
self.def_axis_coord = scene.mesh_extra_tools.vert_align_store_axis
return context.window_manager.invoke_props_dialog(self, width=200)
def execute(self, context):
self.is_not_undo = False
edit_mode_out()
ob_act = context.active_object
me = ob_act.data
for i in va_buf.list_0:
v = (me.vertices[i].co).copy()
tmp = Vector((v[0], v[1], v[2]))
if self.use_axis_coord[0] is True:
tmp[0] = self.def_axis_coord[0]
if self.use_axis_coord[1] is True:
tmp[1] = self.def_axis_coord[1]
if self.use_axis_coord[2] is True:
tmp[2] = self.def_axis_coord[2]
me.vertices[i].co = tmp
edit_mode_in()
return {'FINISHED'}
# Register
classes = (
Vertex_align_store,
Vertex_align_original,
Vertex_align_coord_list,
Vertex_align_coord_menu,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()