1060 lines
38 KiB
Python
1060 lines
38 KiB
Python
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
|
# ------------------------------- version 0.84 ------------------------------- #
|
|
# #
|
|
# Creates duplicates of selected mesh to active morphing the shape according #
|
|
# to target faces. #
|
|
# #
|
|
# (c) Alessandro Zomparelli #
|
|
# (2017) #
|
|
# #
|
|
# http://www.co-de-it.com/ #
|
|
# #
|
|
# ############################################################################ #
|
|
|
|
|
|
import bpy
|
|
from bpy.types import (
|
|
Operator,
|
|
Panel,
|
|
PropertyGroup,
|
|
)
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
EnumProperty,
|
|
FloatProperty,
|
|
IntProperty,
|
|
StringProperty,
|
|
PointerProperty
|
|
)
|
|
from mathutils import Vector, Quaternion, Matrix
|
|
import numpy as np
|
|
from math import *
|
|
import random, time, copy
|
|
import bmesh
|
|
from .utils import *
|
|
|
|
def anim_polyhedra_active(self, context):
|
|
ob = context.object
|
|
props = ob.tissue_polyhedra
|
|
if ob.tissue.tissue_type=='POLYHEDRA' and not ob.tissue.bool_lock:
|
|
props.object.name
|
|
bpy.ops.object.tissue_update_polyhedra()
|
|
|
|
class tissue_polyhedra_prop(PropertyGroup):
|
|
object : PointerProperty(
|
|
type=bpy.types.Object,
|
|
name="Object",
|
|
description="Source object",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
mode : EnumProperty(
|
|
items=(
|
|
('POLYHEDRA', "Polyhedra", "Polyhedral Complex Decomposition, the result are disconnected polyhedra geometries"),
|
|
('WIREFRAME', "Wireframe", "Polyhedral Wireframe through edges tickening")
|
|
),
|
|
default='POLYHEDRA',
|
|
name="Polyhedra Mode",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
bool_modifiers : BoolProperty(
|
|
name="Use Modifiers",
|
|
description="",
|
|
default=True,
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
dissolve : EnumProperty(
|
|
items=(
|
|
('NONE', "None", "Keeps original topology"),
|
|
('INNER', "Inner", "Dissolve inner loops"),
|
|
('OUTER', "Outer", "Dissolve outer loops")
|
|
),
|
|
default='NONE',
|
|
name="Dissolve",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
thickness : FloatProperty(
|
|
name="Thickness", default=1, soft_min=0, soft_max=10,
|
|
description="Thickness along the edges",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
crease : FloatProperty(
|
|
name="Crease", default=0, min=0, max=1,
|
|
description="Crease Inner Loops",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
segments : IntProperty(
|
|
name="Segments",
|
|
default=0,
|
|
min=1,
|
|
soft_max=20,
|
|
description="Segments for every edge",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
proportional_segments : BoolProperty(
|
|
name="Proportional Segments", default=True,
|
|
description="The number of segments is proportional to the length of the edges",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
selective_wireframe : EnumProperty(
|
|
name="Selective",
|
|
items=(
|
|
('NONE', "None", "Apply wireframe to every cell"),
|
|
('THICKNESS', "Thickness", "Wireframe only on bigger cells compared to the thickness"),
|
|
('AREA', "Area", "Wireframe based on cells dimensions"),
|
|
('WEIGHT', "Weight", "Wireframe based on vertex groups")
|
|
),
|
|
default='NONE',
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
thickness_threshold_correction : FloatProperty(
|
|
name="Correction", default=1, min=0, soft_max=2,
|
|
description="Adjust threshold based on thickness",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
area_threshold : FloatProperty(
|
|
name="Threshold", default=0, min=0, soft_max=10,
|
|
description="Use only faces with an area greater than the threshold",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
thicken_all : BoolProperty(
|
|
name="Thicken all",
|
|
description="Thicken original faces as well",
|
|
default=True,
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
vertex_group_thickness : StringProperty(
|
|
name="Thickness weight", default='',
|
|
description="Vertex Group used for thickness",
|
|
update = anim_polyhedra_active
|
|
)
|
|
invert_vertex_group_thickness : BoolProperty(
|
|
name="Invert", default=False,
|
|
description="Invert the vertex group influence",
|
|
update = anim_polyhedra_active
|
|
)
|
|
vertex_group_thickness_factor : FloatProperty(
|
|
name="Factor",
|
|
default=0,
|
|
min=0,
|
|
max=1,
|
|
description="Thickness factor to use for zero vertex group influence",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
vertex_group_selective : StringProperty(
|
|
name="Thickness weight", default='',
|
|
description="Vertex Group used for selective wireframe",
|
|
update = anim_polyhedra_active
|
|
)
|
|
invert_vertex_group_selective : BoolProperty(
|
|
name="Invert", default=False,
|
|
description="Invert the vertex group influence",
|
|
update = anim_polyhedra_active
|
|
)
|
|
vertex_group_selective_threshold : FloatProperty(
|
|
name="Threshold",
|
|
default=0.5,
|
|
min=0,
|
|
max=1,
|
|
description="Selective wireframe threshold",
|
|
update = anim_polyhedra_active
|
|
)
|
|
bool_smooth : BoolProperty(
|
|
name="Smooth Shading",
|
|
default=False,
|
|
description="Output faces with smooth shading rather than flat shaded",
|
|
update = anim_polyhedra_active
|
|
)
|
|
|
|
error_message : StringProperty(
|
|
name="Error Message",
|
|
default=""
|
|
)
|
|
|
|
class polyhedral_wireframe(Operator):
|
|
bl_idname = "object.polyhedral_wireframe"
|
|
bl_label = "Tissue Polyhedral Wireframe"
|
|
bl_description = "Generate wireframes around the faces.\
|
|
\nDoesn't works with boundary edges.\
|
|
\n(Experimental)"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
thickness : FloatProperty(
|
|
name="Thickness", default=0.1, min=0.001, soft_max=200,
|
|
description="Wireframe thickness"
|
|
)
|
|
|
|
crease : FloatProperty(
|
|
name="Crease", default=0, min=0, max=1,
|
|
description="Crease Inner Loops"
|
|
)
|
|
|
|
segments : IntProperty(
|
|
name="Segments", default=1, min=1, soft_max=10,
|
|
description="Segments for every edge"
|
|
)
|
|
|
|
proportional_segments : BoolProperty(
|
|
name="Proportional Segments", default=True,
|
|
description="The number of segments is proportional to the length of the edges"
|
|
)
|
|
|
|
mode : EnumProperty(
|
|
items=(
|
|
('POLYHEDRA', "Polyhedra", "Polyhedral Complex Decomposition, the result are disconnected polyhedra geometries"),
|
|
('WIREFRAME', "Wireframe", "Polyhedral Wireframe through edges tickening")
|
|
),
|
|
default='POLYHEDRA',
|
|
name="Polyhedra Mode"
|
|
)
|
|
|
|
dissolve : EnumProperty(
|
|
items=(
|
|
('NONE', "None", "Keeps original topology"),
|
|
('INNER', "Inner", "Dissolve inner loops"),
|
|
('OUTER', "Outer", "Dissolve outer loops")
|
|
),
|
|
default='NONE',
|
|
name="Dissolve"
|
|
)
|
|
|
|
selective_wireframe : EnumProperty(
|
|
items=(
|
|
('NONE', "None", "Apply wireframe to every cell"),
|
|
('THICKNESS', "Thickness", "Wireframe only on bigger cells compared to the thickness"),
|
|
('AREA', "Area", "Wireframe based on cells dimensions"),
|
|
('WEIGHT', "Weight", "Wireframe based on vertex groups")
|
|
),
|
|
default='NONE',
|
|
name="Selective"
|
|
)
|
|
|
|
thickness_threshold_correction : FloatProperty(
|
|
name="Correction", default=1, min=0, soft_max=2,
|
|
description="Adjust threshold based on thickness"
|
|
)
|
|
|
|
area_threshold : FloatProperty(
|
|
name="Threshold", default=0, min=0, soft_max=10,
|
|
description="Use only faces with an area greater than the threshold"
|
|
)
|
|
|
|
thicken_all : BoolProperty(
|
|
name="Thicken all",
|
|
description="Thicken original faces as well",
|
|
default=True
|
|
)
|
|
|
|
vertex_group_thickness : StringProperty(
|
|
name="Thickness weight", default='',
|
|
description="Vertex Group used for thickness"
|
|
)
|
|
|
|
invert_vertex_group_thickness : BoolProperty(
|
|
name="Invert", default=False,
|
|
description="Invert the vertex group influence"
|
|
)
|
|
|
|
vertex_group_thickness_factor : FloatProperty(
|
|
name="Factor",
|
|
default=0,
|
|
min=0,
|
|
max=1,
|
|
description="Thickness factor to use for zero vertex group influence"
|
|
)
|
|
|
|
vertex_group_selective : StringProperty(
|
|
name="Thickness weight", default='',
|
|
description="Vertex Group used for thickness"
|
|
)
|
|
|
|
invert_vertex_group_selective : BoolProperty(
|
|
name="Invert", default=False,
|
|
description="Invert the vertex group influence"
|
|
)
|
|
|
|
vertex_group_selective_threshold : FloatProperty(
|
|
name="Threshold",
|
|
default=0.5,
|
|
min=0,
|
|
max=1,
|
|
description="Selective wireframe threshold"
|
|
)
|
|
|
|
bool_smooth : BoolProperty(
|
|
name="Smooth Shading",
|
|
default=False,
|
|
description="Output faces with smooth shading rather than flat shaded"
|
|
)
|
|
|
|
bool_hold : BoolProperty(
|
|
name="Hold",
|
|
description="Wait...",
|
|
default=False
|
|
)
|
|
|
|
def draw(self, context):
|
|
ob = context.object
|
|
layout = self.layout
|
|
col = layout.column(align=True)
|
|
self.bool_hold = True
|
|
if self.mode == 'WIREFRAME':
|
|
col.separator()
|
|
col.prop(self, "thickness")
|
|
col.separator()
|
|
col.prop(self, "segments")
|
|
return
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def execute(self, context):
|
|
ob0 = context.object
|
|
|
|
self.object_name = "Polyhedral Wireframe"
|
|
# Check if existing object with same name
|
|
names = [o.name for o in bpy.data.objects]
|
|
if self.object_name in names:
|
|
count_name = 1
|
|
while True:
|
|
test_name = self.object_name + '.{:03d}'.format(count_name)
|
|
if not (test_name in names):
|
|
self.object_name = test_name
|
|
break
|
|
count_name += 1
|
|
|
|
if ob0.type not in ('MESH'):
|
|
message = "Source object must be a Mesh!"
|
|
self.report({'ERROR'}, message)
|
|
|
|
if bpy.ops.object.select_all.poll():
|
|
bpy.ops.object.select_all(action='TOGGLE')
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
bool_update = False
|
|
auto_layer_collection()
|
|
new_ob = convert_object_to_mesh(ob0,False,False)
|
|
new_ob.data.name = self.object_name
|
|
new_ob.name = self.object_name
|
|
|
|
# Store parameters
|
|
props = new_ob.tissue_polyhedra
|
|
lock_status = new_ob.tissue.bool_lock
|
|
new_ob.tissue.bool_lock = True
|
|
props.mode = self.mode
|
|
props.thickness = self.thickness
|
|
props.segments = self.segments
|
|
props.dissolve = self.dissolve
|
|
props.proportional_segments = self.proportional_segments
|
|
props.crease = self.crease
|
|
props.object = ob0
|
|
|
|
new_ob.tissue.tissue_type = 'POLYHEDRA'
|
|
try: bpy.ops.object.tissue_update_polyhedra()
|
|
except RuntimeError as e:
|
|
bpy.data.objects.remove(new_ob)
|
|
remove_temp_objects()
|
|
self.report({'ERROR'}, str(e))
|
|
return {'CANCELLED'}
|
|
if not bool_update:
|
|
self.object_name = new_ob.name
|
|
new_ob.location = ob0.location
|
|
new_ob.matrix_world = ob0.matrix_world
|
|
|
|
# Assign collection of the base object
|
|
old_coll = new_ob.users_collection
|
|
if old_coll != ob0.users_collection:
|
|
for c in old_coll:
|
|
c.objects.unlink(new_ob)
|
|
for c in ob0.users_collection:
|
|
c.objects.link(new_ob)
|
|
context.view_layer.objects.active = new_ob
|
|
|
|
# unlock
|
|
new_ob.tissue.bool_lock = lock_status
|
|
|
|
return {'FINISHED'}
|
|
|
|
class tissue_update_polyhedra(Operator):
|
|
bl_idname = "object.tissue_update_polyhedra"
|
|
bl_label = "Tissue Update Polyhedral Wireframe"
|
|
bl_description = "Update a previously generated polyhedral object"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
ob = context.object
|
|
tissue_time(None,'Tissue: Polyhedral Wireframe of "{}"...'.format(ob.name), levels=0)
|
|
start_time = time.time()
|
|
begin_time = time.time()
|
|
props = ob.tissue_polyhedra
|
|
thickness = props.thickness
|
|
|
|
merge_dist = thickness*0.0001
|
|
|
|
subs = props.segments
|
|
if props.mode == 'POLYHEDRA': subs = 1
|
|
|
|
|
|
# Source mesh
|
|
ob0 = props.object
|
|
if props.bool_modifiers:
|
|
me = simple_to_mesh(ob0)
|
|
else:
|
|
me = ob0.data.copy()
|
|
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
|
|
pre_processing(bm)
|
|
polyhedral_subdivide_edges(bm, subs, props.proportional_segments)
|
|
tissue_time(start_time,'Subdivide edges',levels=1)
|
|
start_time = time.time()
|
|
|
|
thickness = np.ones(len(bm.verts))*props.thickness
|
|
if(props.vertex_group_thickness in ob.vertex_groups.keys()):
|
|
dvert_lay = bm.verts.layers.deform.active
|
|
group_index_thickness = ob.vertex_groups[props.vertex_group_thickness].index
|
|
thickness_weight = bmesh_get_weight_numpy(group_index_thickness, dvert_lay, bm.verts)
|
|
if 'invert_vertex_group_thickness' in props.keys():
|
|
if props['invert_vertex_group_thickness']:
|
|
thickness_weight = 1-thickness_weight
|
|
fact = 0
|
|
if 'vertex_group_thickness_factor' in props.keys():
|
|
fact = props['vertex_group_thickness_factor']
|
|
if fact > 0:
|
|
thickness_weight = thickness_weight*(1-fact) + fact
|
|
thickness *= thickness_weight
|
|
thickness_dict = dict(zip([tuple(v.co) for v in bm.verts],thickness))
|
|
|
|
bm1 = get_double_faces_bmesh(bm)
|
|
polyhedra = get_decomposed_polyhedra(bm)
|
|
if(type(polyhedra) is str):
|
|
bm.free()
|
|
bm1.free()
|
|
self.report({'ERROR'}, polyhedra)
|
|
return {'CANCELLED'}
|
|
|
|
selective_dict = None
|
|
accurate = False
|
|
if props.selective_wireframe == 'THICKNESS':
|
|
filter_faces = True
|
|
accurate = True
|
|
area_threshold = (thickness*props.thickness_threshold_correction)**2
|
|
elif props.selective_wireframe == 'AREA':
|
|
filter_faces = True
|
|
area_threshold = props.area_threshold
|
|
elif props.selective_wireframe == 'WEIGHT':
|
|
filter_faces = True
|
|
if(props.vertex_group_selective in ob.vertex_groups.keys()):
|
|
dvert_lay = bm.verts.layers.deform.active
|
|
group_index_selective = ob.vertex_groups[props.vertex_group_selective].index
|
|
thresh = props.vertex_group_selective_threshold
|
|
selective_weight = bmesh_get_weight_numpy(group_index_selective, dvert_lay, bm.verts)
|
|
selective_weight = selective_weight >= thresh
|
|
invert = False
|
|
if 'invert_vertex_group_selective' in props.keys():
|
|
if props['invert_vertex_group_selective']:
|
|
invert = True
|
|
if invert:
|
|
selective_weight = selective_weight <= thresh
|
|
else:
|
|
selective_weight = selective_weight >= thresh
|
|
selective_dict = dict(zip([tuple(v.co) for v in bm.verts],selective_weight))
|
|
else:
|
|
filter_faces = False
|
|
else:
|
|
filter_faces = False
|
|
|
|
bm.free()
|
|
|
|
end_time = time.time()
|
|
tissue_time(start_time,'Found {} polyhedra'.format(len(polyhedra)),levels=1)
|
|
start_time = time.time()
|
|
|
|
bm1.faces.ensure_lookup_table()
|
|
bm1.faces.index_update()
|
|
|
|
#unique_verts_dict = dict(zip([tuple(v.co) for v in bm1.verts],bm1.verts))
|
|
bm1, all_faces_dict, polyhedra_faces_id, polyhedra_faces_id_neg = combine_polyhedra_faces(bm1, polyhedra)
|
|
|
|
if props.mode == 'POLYHEDRA':
|
|
poly_me = me.copy()
|
|
bm1.to_mesh(poly_me)
|
|
poly_me.update()
|
|
old_me = ob.data
|
|
ob.data = poly_me
|
|
mesh_name = old_me.name
|
|
bpy.data.meshes.remove(old_me)
|
|
bpy.data.meshes.remove(me)
|
|
ob.data.name = mesh_name
|
|
end_time = time.time()
|
|
print('Tissue: Polyhedral wireframe in {:.4f} sec'.format(end_time-start_time))
|
|
return {'FINISHED'}
|
|
|
|
delete_faces = set({})
|
|
wireframe_faces = []
|
|
not_wireframe_faces = []
|
|
#flat_faces = []
|
|
count = 0
|
|
outer_faces = get_outer_faces(bm1)
|
|
for faces_id in polyhedra_faces_id:
|
|
delete_faces_poly = []
|
|
wireframe_faces_poly = []
|
|
for id in faces_id:
|
|
if id in delete_faces: continue
|
|
delete = False
|
|
cen = None
|
|
f = None
|
|
if filter_faces:
|
|
f = all_faces_dict[id]
|
|
if selective_dict:
|
|
for v in f.verts:
|
|
if selective_dict[tuple(v.co)]:
|
|
delete = True
|
|
break
|
|
elif accurate:
|
|
cen = f.calc_center_median()
|
|
for e in f.edges:
|
|
v0 = e.verts[0]
|
|
v1 = e.verts[1]
|
|
mid = (v0.co + v1.co)/2
|
|
vec1 = v0.co - v1.co
|
|
vec2 = mid - cen
|
|
ang = Vector.angle(vec1,vec2)
|
|
length = vec2.length
|
|
length = sin(ang)*length
|
|
thick0 = thickness_dict[tuple(v0.co)]
|
|
thick1 = thickness_dict[tuple(v1.co)]
|
|
thick = (thick0 + thick1)/4
|
|
if length < thick*props.thickness_threshold_correction:
|
|
delete = True
|
|
break
|
|
else:
|
|
delete = f.calc_area() < area_threshold
|
|
if delete:
|
|
if props.thicken_all:
|
|
delete_faces_poly.append(id)
|
|
else:
|
|
wireframe_faces_poly.append(id)
|
|
if len(wireframe_faces_poly) <= 2:
|
|
delete_faces.update(set([id for id in faces_id]))
|
|
not_wireframe_faces += [polyhedra_faces_id_neg[id] for id in faces_id]
|
|
else:
|
|
wireframe_faces += wireframe_faces_poly
|
|
#flat_faces += delete_faces_poly
|
|
wireframe_faces_id = [i for i in wireframe_faces if i not in not_wireframe_faces]
|
|
wireframe_faces = [all_faces_dict[i] for i in wireframe_faces_id]
|
|
#flat_faces = [all_faces_dict[i] for i in flat_faces]
|
|
delete_faces = [all_faces_dict[i] for i in delete_faces if all_faces_dict[i] not in outer_faces]
|
|
|
|
tissue_time(start_time,'Merge and delete',levels=1)
|
|
start_time = time.time()
|
|
|
|
############# FRAME #############
|
|
new_faces, outer_wireframe_faces = create_frame_faces(
|
|
bm1,
|
|
wireframe_faces,
|
|
wireframe_faces_id,
|
|
polyhedra_faces_id_neg,
|
|
thickness_dict,
|
|
outer_faces
|
|
)
|
|
faces_to_delete = wireframe_faces+delete_faces
|
|
outer_wireframe_faces += [f for f in outer_faces if not f in faces_to_delete]
|
|
bmesh.ops.delete(bm1, geom=faces_to_delete, context='FACES')
|
|
|
|
bm1.verts.ensure_lookup_table()
|
|
bm1.edges.ensure_lookup_table()
|
|
bm1.faces.ensure_lookup_table()
|
|
bm1.verts.index_update()
|
|
|
|
wireframe_indexes = [f.index for f in new_faces]
|
|
outer_indexes = [f.index for f in outer_wireframe_faces]
|
|
edges_to_crease = [f.edges[2].index for f in new_faces]
|
|
layer_is_wireframe = bm1.faces.layers.int.new('tissue_is_wireframe')
|
|
for id in wireframe_indexes:
|
|
bm1.faces[id][layer_is_wireframe] = 1
|
|
layer_is_outer = bm1.faces.layers.int.new('tissue_is_outer')
|
|
for id in outer_indexes:
|
|
bm1.faces[id][layer_is_outer] = 1
|
|
if props.crease > 0 and props.dissolve != 'INNER':
|
|
crease_layer = bm1.edges.layers.float.new('crease_edge')
|
|
bm1.edges.index_update()
|
|
crease_edges = []
|
|
for edge_index in edges_to_crease:
|
|
bm1.edges[edge_index][crease_layer] = props.crease
|
|
|
|
tissue_time(start_time,'Generate frames',levels=1)
|
|
start_time = time.time()
|
|
|
|
### Displace vertices ###
|
|
corners = [[] for i in range(len(bm1.verts))]
|
|
normals = [0]*len(bm1.verts)
|
|
vertices = [0]*len(bm1.verts)
|
|
# Define vectors direction
|
|
for f in bm1.faces:
|
|
v0 = f.verts[0]
|
|
v1 = f.verts[1]
|
|
id = v0.index
|
|
corners[id].append((v1.co - v0.co).normalized())
|
|
v0.normal_update()
|
|
normals[id] = v0.normal.copy()
|
|
vertices[id] = v0
|
|
# Displace vertices
|
|
for i, vecs in enumerate(corners):
|
|
if len(vecs) > 0:
|
|
v = vertices[i]
|
|
nor = normals[i]
|
|
ang = 0
|
|
for vec in vecs:
|
|
if nor == Vector((0,0,0)): continue
|
|
ang += nor.angle(vec)
|
|
ang /= len(vecs)
|
|
div = sin(ang)
|
|
if div == 0: div = 1
|
|
v.co += nor*thickness_dict[tuple(v.co)]/div
|
|
|
|
tissue_time(start_time,'Corners displace',levels=1)
|
|
start_time = time.time()
|
|
|
|
if props.dissolve != 'NONE':
|
|
if props.dissolve == 'INNER': dissolve_id = 2
|
|
if props.dissolve == 'OUTER': dissolve_id = 0
|
|
bm1.edges.index_update()
|
|
dissolve_edges = []
|
|
for f in bm1.faces:
|
|
e = f.edges[dissolve_id]
|
|
if e not in dissolve_edges:
|
|
dissolve_edges.append(e)
|
|
bmesh.ops.dissolve_edges(bm1, edges=dissolve_edges, use_verts=True, use_face_split=False)
|
|
|
|
for v in bm1.verts: v.select_set(False)
|
|
for f in bm1.faces: f.select_set(False)
|
|
|
|
dissolve_verts = [v for v in bm1.verts if len(v.link_edges) < 3]
|
|
bmesh.ops.dissolve_verts(bm1, verts=dissolve_verts, use_face_split=False, use_boundary_tear=False)
|
|
|
|
# clean meshes
|
|
bm1.to_mesh(me)
|
|
if props.bool_smooth: me.shade_smooth()
|
|
me.update()
|
|
old_me = ob.data
|
|
ob.data = me
|
|
mesh_name = old_me.name
|
|
bpy.data.meshes.remove(old_me)
|
|
ob.data.name = mesh_name
|
|
bm1.free()
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.uv.reset()
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
tissue_time(start_time,'Clean mesh',levels=1)
|
|
start_time = time.time()
|
|
|
|
tissue_time(begin_time,'Polyhedral Wireframe',levels=0)
|
|
return {'FINISHED'}
|
|
|
|
def pre_processing(bm):
|
|
delete = [e for e in bm.edges if len(e.link_faces) < 2]
|
|
while len(delete) > 0:
|
|
bmesh.ops.delete(bm, geom=delete, context='EDGES')
|
|
bm.faces.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.verts.ensure_lookup_table()
|
|
delete = [e for e in bm.edges if len(e.link_faces) < 2]
|
|
return bm
|
|
|
|
def get_outer_faces(bm):
|
|
bm_copy = bm.copy()
|
|
bmesh.ops.recalc_face_normals(bm_copy, faces=bm_copy.faces)
|
|
outer = []
|
|
for f1, f2 in zip(bm.faces, bm_copy.faces):
|
|
f1.normal_update()
|
|
if f1.normal == f2.normal:
|
|
outer.append(f1)
|
|
return outer
|
|
|
|
def create_frame_faces(
|
|
bm,
|
|
wireframe_faces,
|
|
wireframe_faces_id,
|
|
polyhedra_faces_id_neg,
|
|
thickness_dict,
|
|
outer_faces
|
|
):
|
|
new_faces = []
|
|
for f in wireframe_faces:
|
|
f.normal_update()
|
|
all_loops = [[loop for loop in f.loops] for f in wireframe_faces]
|
|
is_outer = [f in outer_faces for f in wireframe_faces]
|
|
outer_wireframe_faces = []
|
|
frames_verts_dict = {}
|
|
for loops_index, loops in enumerate(all_loops):
|
|
n_loop = len(loops)
|
|
frame_id = wireframe_faces_id[loops_index]
|
|
single_face_id = min(frame_id,polyhedra_faces_id_neg[frame_id])
|
|
verts_inner = []
|
|
loops_keys = [tuple(loop.vert.co) + tuple((single_face_id,)) for loop in loops]
|
|
if loops_keys[0] in frames_verts_dict:
|
|
verts_inner = [frames_verts_dict[key] for key in loops_keys]
|
|
else:
|
|
tangents = []
|
|
nor = wireframe_faces[loops_index].normal
|
|
for loop in loops:
|
|
tan = loop.calc_tangent() #nor.cross(loop.calc_tangent().cross(nor)).normalized()
|
|
thickness = thickness_dict[tuple(loop.vert.co)]
|
|
tangents.append(tan/sin(loop.calc_angle()/2)*thickness)
|
|
for i in range(n_loop):
|
|
loop = loops[i]
|
|
new_co = loop.vert.co + tangents[i]
|
|
new_vert = bm.verts.new(new_co)
|
|
frames_verts_dict[loops_keys[i]] = new_vert
|
|
verts_inner.append(new_vert)
|
|
# add faces
|
|
loops += [loops[0]]
|
|
verts_inner += [verts_inner[0]]
|
|
for i in range(n_loop):
|
|
v0 = loops[i].vert
|
|
v1 = loops[i+1].vert
|
|
v2 = verts_inner[i+1]
|
|
v3 = verts_inner[i]
|
|
face_verts = [v0,v1,v2,v3]
|
|
new_face = bm.faces.new(face_verts)
|
|
new_face.select = True
|
|
new_faces.append(new_face)
|
|
if is_outer[loops_index]:
|
|
outer_wireframe_faces.append(new_face)
|
|
new_face.normal_update()
|
|
return new_faces, outer_wireframe_faces
|
|
|
|
def polyhedral_subdivide_edges(bm, subs, proportional_segments):
|
|
if subs > 1:
|
|
if proportional_segments:
|
|
wire_length = [e.calc_length() for e in bm.edges]
|
|
all_edges = list(bm.edges)
|
|
max_segment = max(wire_length)/subs+0.00001 # prevent out_of_bounds
|
|
split_edges = [[] for i in range(subs)]
|
|
for e, l in zip(all_edges, wire_length):
|
|
split_edges[int(l//max_segment)].append(e)
|
|
for i in range(1,subs):
|
|
bmesh.ops.bisect_edges(bm, edges=split_edges[i], cuts=i)
|
|
else:
|
|
bmesh.ops.bisect_edges(bm, edges=bm.edges, cuts=subs-1)
|
|
|
|
def get_double_faces_bmesh(bm):
|
|
double_faces = []
|
|
for f in bm.faces:
|
|
verts0 = [v.co for v in f.verts]
|
|
verts1 = verts0.copy()
|
|
verts1.reverse()
|
|
double_faces.append(verts0)
|
|
double_faces.append(verts1)
|
|
bm1 = bmesh.new()
|
|
for verts_co in double_faces:
|
|
bm1.faces.new([bm1.verts.new(v) for v in verts_co])
|
|
bm1.verts.ensure_lookup_table()
|
|
bm1.edges.ensure_lookup_table()
|
|
bm1.faces.ensure_lookup_table()
|
|
return bm1
|
|
|
|
def get_decomposed_polyhedra(bm):
|
|
polyhedra_from_facekey = {}
|
|
count = 0
|
|
to_merge = []
|
|
for e in bm.edges:
|
|
done = []
|
|
# ERROR: Naked edges
|
|
link_faces = e.link_faces
|
|
n_radial_faces = len(link_faces)
|
|
if n_radial_faces < 2:
|
|
return "Naked edges are not allowed"
|
|
vert0 = e.verts[0]
|
|
vert1 = e.verts[1]
|
|
edge_vec = vert1.co - vert0.co
|
|
|
|
for id1 in range(n_radial_faces-1):
|
|
f1 = link_faces[id1]
|
|
facekey1 = f1.index+1
|
|
verts1 = [v.index for v in f1.verts]
|
|
v0_index = verts1.index(vert0.index)
|
|
v1_index = verts1.index(vert1.index)
|
|
|
|
ref_loop_dir = v0_index == (v1_index+1)%len(verts1)
|
|
edge_vec1 = edge_vec if ref_loop_dir else -edge_vec
|
|
tan1 = f1.normal.cross(edge_vec1)
|
|
|
|
# faces to compare with
|
|
faceskeys2, normals2 = get_second_faces(
|
|
link_faces,
|
|
vert0.index,
|
|
vert1.index,
|
|
ref_loop_dir,
|
|
f1
|
|
)
|
|
|
|
tangents2 = [nor.cross(-edge_vec1) for nor in normals2]
|
|
|
|
# positive side
|
|
facekey2_pos = get_closest_face(
|
|
faceskeys2,
|
|
tangents2,
|
|
tan1,
|
|
edge_vec1,
|
|
True
|
|
)
|
|
polyhedra_from_facekey, count, to_merge = store_neighbor_faces(
|
|
facekey1,
|
|
facekey2_pos,
|
|
polyhedra_from_facekey,
|
|
count,
|
|
to_merge
|
|
)
|
|
# negative side
|
|
facekey2_neg = get_closest_face(
|
|
faceskeys2,
|
|
tangents2,
|
|
tan1,
|
|
edge_vec1,
|
|
False
|
|
)
|
|
polyhedra_from_facekey, count, to_merge = store_neighbor_faces(
|
|
-facekey1,
|
|
facekey2_neg,
|
|
polyhedra_from_facekey,
|
|
count,
|
|
to_merge
|
|
)
|
|
|
|
polyhedra = [ [] for i in range(count)]
|
|
unique_index = get_unique_polyhedra_index(count, to_merge)
|
|
for key, val in polyhedra_from_facekey.items():
|
|
polyhedra[unique_index[val]].append(key)
|
|
polyhedra = list(set(tuple(i) for i in polyhedra if i))
|
|
polyhedra = remove_double_faces_from_polyhedra(polyhedra)
|
|
return polyhedra
|
|
|
|
def remove_double_faces_from_polyhedra(polyhedra):
|
|
new_polyhedra = []
|
|
for polyhedron in polyhedra:
|
|
new_polyhedron = [key for key in polyhedron if not -key in polyhedron]
|
|
new_polyhedra.append(new_polyhedron)
|
|
return new_polyhedra
|
|
|
|
def get_unique_polyhedra_index(count, to_merge):
|
|
out = list(range(count))
|
|
keep_going = True
|
|
while keep_going:
|
|
keep_going = False
|
|
for pair in to_merge:
|
|
if out[pair[1]] != out[pair[0]]:
|
|
out[pair[0]] = out[pair[1]] = min(out[pair[0]], out[pair[1]])
|
|
keep_going = True
|
|
return out
|
|
|
|
def get_closest_face(faces, tangents, ref_vector, axis, is_positive):
|
|
facekey = None
|
|
min_angle = 1000000
|
|
for fk, tangent in zip(faces, tangents):
|
|
rot_axis = -axis if is_positive else axis
|
|
angle = round_angle_with_axis(ref_vector, tangent, rot_axis)
|
|
if angle < min_angle:
|
|
facekey = fk
|
|
min_angle = angle
|
|
return facekey if is_positive else -facekey
|
|
|
|
def get_second_faces(face_list, edge_v0, edge_v1, reference_loop_dir, self):
|
|
nFaces = len(face_list)-1
|
|
facekeys = [None]*nFaces
|
|
normals = [None]*nFaces
|
|
count = 0
|
|
for face in face_list:
|
|
if(face == self): continue
|
|
verts = [v.index for v in face.verts]
|
|
v0_index = verts.index(edge_v0)
|
|
v1_index = verts.index(edge_v1)
|
|
loop_dir = v0_index == (v1_index+1)%len(verts)
|
|
if reference_loop_dir != loop_dir:
|
|
facekeys[count] = face.index+1
|
|
normals[count] = face.normal
|
|
else:
|
|
facekeys[count] = -(face.index+1)
|
|
normals[count] = -face.normal
|
|
count+=1
|
|
return facekeys, normals
|
|
|
|
def store_neighbor_faces(
|
|
key1,
|
|
key2,
|
|
polyhedra,
|
|
polyhedra_count,
|
|
to_merge
|
|
):
|
|
poly1 = polyhedra.get(key1)
|
|
poly2 = polyhedra.get(key2)
|
|
if poly1 and poly2:
|
|
if poly1 != poly2:
|
|
to_merge.append((poly1, poly2))
|
|
elif poly1:
|
|
polyhedra[key2] = poly1
|
|
elif poly2:
|
|
polyhedra[key1] = poly2
|
|
else:
|
|
polyhedra[key1] = polyhedra[key2] = polyhedra_count
|
|
polyhedra_count += 1
|
|
return polyhedra, polyhedra_count, to_merge
|
|
|
|
def add_polyhedron(bm,source_faces):
|
|
faces_verts_key = [[tuple(v.co) for v in f.verts] for f in source_faces]
|
|
polyhedron_verts_key = [key for face_key in faces_verts_key for key in face_key]
|
|
polyhedron_verts = [bm.verts.new(co) for co in polyhedron_verts_key]
|
|
polyhedron_verts_dict = dict(zip(polyhedron_verts_key, polyhedron_verts))
|
|
new_faces = [None]*len(faces_verts_key)
|
|
count = 0
|
|
for verts_keys in faces_verts_key:
|
|
new_faces[count] = bm.faces.new([polyhedron_verts_dict.get(key) for key in verts_keys])
|
|
count+=1
|
|
|
|
bm.faces.ensure_lookup_table()
|
|
bm.faces.index_update()
|
|
return new_faces
|
|
|
|
def combine_polyhedra_faces(bm,polyhedra):
|
|
new_bm = bmesh.new()
|
|
polyhedra_faces_id = [None]*len(polyhedra)
|
|
all_faces_dict = {}
|
|
#polyhedra_faces_pos = {}
|
|
polyhedra_faces_id_neg = {}
|
|
vertices_key = [tuple(v.co) for v in bm.verts]
|
|
count = 0
|
|
for p in polyhedra:
|
|
faces_id = [(f-1)*2 if f > 0 else (-f-1)*2+1 for f in p]
|
|
faces_id_neg = [(-f-1)*2 if f < 0 else (f-1)*2+1 for f in p]
|
|
new_faces = add_polyhedron(new_bm,[bm.faces[f_id] for f_id in faces_id])
|
|
faces_dict = {}
|
|
for i in range(len(new_faces)):
|
|
face = new_faces[i]
|
|
id = faces_id[i]
|
|
id_neg = faces_id_neg[i]
|
|
polyhedra_faces_id_neg[id] = id_neg
|
|
all_faces_dict[id] = face
|
|
polyhedra_faces_id[count] = faces_id
|
|
count+=1
|
|
return new_bm, all_faces_dict, polyhedra_faces_id, polyhedra_faces_id_neg
|
|
|
|
class TISSUE_PT_polyhedra_object(Panel):
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "data"
|
|
bl_label = "Tissue Polyhedra"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
try:
|
|
ob = context.object
|
|
return ob.type == 'MESH' and ob.tissue.tissue_type == 'POLYHEDRA'
|
|
except: return False
|
|
|
|
def draw(self, context):
|
|
ob = context.object
|
|
props = ob.tissue_polyhedra
|
|
tissue_props = ob.tissue
|
|
|
|
bool_polyhedra = tissue_props.tissue_type == 'POLYHEDRA'
|
|
layout = self.layout
|
|
if not bool_polyhedra:
|
|
layout.label(text="The selected object is not a Polyhedral object",
|
|
icon='INFO')
|
|
else:
|
|
if props.error_message != "":
|
|
layout.label(text=props.error_message,
|
|
icon='ERROR')
|
|
col = layout.column(align=True)
|
|
row = col.row(align=True)
|
|
|
|
#set_tessellate_handler(self,context)
|
|
row.operator("object.tissue_update_tessellate_deps", icon='FILE_REFRESH', text='Refresh') ####
|
|
lock_icon = 'LOCKED' if tissue_props.bool_lock else 'UNLOCKED'
|
|
#lock_icon = 'PINNED' if props.bool_lock else 'UNPINNED'
|
|
deps_icon = 'LINKED' if tissue_props.bool_dependencies else 'UNLINKED'
|
|
row.prop(tissue_props, "bool_dependencies", text="", icon=deps_icon)
|
|
row.prop(tissue_props, "bool_lock", text="", icon=lock_icon)
|
|
col2 = row.column(align=True)
|
|
col2.prop(tissue_props, "bool_run", text="",icon='TIME')
|
|
col2.enabled = not tissue_props.bool_lock
|
|
col2 = row.column(align=True)
|
|
col2.operator("mesh.tissue_remove", text="", icon='X')
|
|
#layout.use_property_split = True
|
|
#layout.use_property_decorate = False # No animation.
|
|
col = layout.column(align=True)
|
|
col.label(text='Polyhedral Mode:')
|
|
col.prop(props, 'mode', text='')
|
|
col.separator()
|
|
col.label(text='Source object:')
|
|
row = col.row(align=True)
|
|
row.prop_search(props, "object", context.scene, "objects", text='')
|
|
col2 = row.column(align=True)
|
|
col2.prop(props, "bool_modifiers", text='Use Modifiers',icon='MODIFIER')
|
|
if props.mode == 'WIREFRAME':
|
|
col.separator()
|
|
col.prop(props, 'thickness')
|
|
row = col.row(align=True)
|
|
ob0 = props.object
|
|
row.prop_search(props, 'vertex_group_thickness',
|
|
ob0, "vertex_groups", text='')
|
|
col2 = row.column(align=True)
|
|
row2 = col2.row(align=True)
|
|
row2.prop(props, "invert_vertex_group_thickness", text="",
|
|
toggle=True, icon='ARROW_LEFTRIGHT')
|
|
row2.prop(props, "vertex_group_thickness_factor")
|
|
row2.enabled = props.vertex_group_thickness in ob0.vertex_groups.keys()
|
|
col.prop(props, 'bool_smooth')
|
|
col.separator()
|
|
col.label(text='Selective Wireframe:')
|
|
col.prop(props, 'selective_wireframe', text='Mode')
|
|
col.separator()
|
|
if props.selective_wireframe == 'THICKNESS':
|
|
col.prop(props, 'thickness_threshold_correction')
|
|
elif props.selective_wireframe == 'AREA':
|
|
col.prop(props, 'area_threshold')
|
|
elif props.selective_wireframe == 'WEIGHT':
|
|
row = col.row(align=True)
|
|
row.prop_search(props, 'vertex_group_selective',
|
|
ob0, "vertex_groups", text='')
|
|
col2 = row.column(align=True)
|
|
row2 = col2.row(align=True)
|
|
row2.prop(props, "invert_vertex_group_selective", text="",
|
|
toggle=True, icon='ARROW_LEFTRIGHT')
|
|
row2.prop(props, "vertex_group_selective_threshold")
|
|
row2.enabled = props.vertex_group_selective in ob0.vertex_groups.keys()
|
|
#if props.selective_wireframe != 'NONE':
|
|
# col.prop(props, 'thicken_all')
|
|
col.separator()
|
|
col.label(text='Subdivide edges:')
|
|
row = col.row()
|
|
row.prop(props, 'segments')
|
|
row.prop(props, 'proportional_segments', text='Proportional')
|
|
col.separator()
|
|
col.label(text='Loops:')
|
|
col.prop(props, 'dissolve')
|
|
col.separator()
|
|
col.prop(props, 'crease')
|