From 498d912a9d5854c0cfc1f54ed8b4216d442b89f1 Mon Sep 17 00:00:00 2001 From: meta-androcto Date: Mon, 1 Jul 2019 21:06:16 +1000 Subject: mesh_tissue: initial update 2.80 --- mesh_tissue/README.md | 30 +- mesh_tissue/__init__.py | 78 +- mesh_tissue/colors_groups_exchanger.py | 2663 ++++++++++++++++++++--- mesh_tissue/dual_mesh.py | 166 +- mesh_tissue/lattice.py | 181 +- mesh_tissue/numba_functions.py | 40 + mesh_tissue/tessellate_numpy.py | 3593 ++++++++++++++++++++++---------- mesh_tissue/utils.py | 110 + mesh_tissue/uv_to_mesh.py | 36 +- 9 files changed, 5270 insertions(+), 1627 deletions(-) create mode 100644 mesh_tissue/numba_functions.py create mode 100644 mesh_tissue/utils.py (limited to 'mesh_tissue') diff --git a/mesh_tissue/README.md b/mesh_tissue/README.md index 9998d1cf..e5511cef 100644 --- a/mesh_tissue/README.md +++ b/mesh_tissue/README.md @@ -1,14 +1,38 @@ # Tissue +![cover](http://www.co-de-it.com/wordpress/wp-content/uploads/2015/07/tissue_graphics.jpg) Tissue - Blender's add-on for computational design by Co-de-iT http://www.co-de-it.com/wordpress/code/blender-tissue -Latest version (master): https://github.com/alessandro-zomparelli/tissue/archive/master.zip +### Blender 2.79 -Releases: https://github.com/alessandro-zomparelli/tissue/releases +Official version (master): https://github.com/alessandro-zomparelli/tissue/archive/master.zip -Installation: +Latest development version (dev1): https://github.com/alessandro-zomparelli/tissue/tree/dev1 +(Includes animatable Tessellation) + +### Blender 2.80 + +Latest development version (b280-dev): https://github.com/alessandro-zomparelli/tissue/tree/b280-dev +(Includes animatable Tessellation and Patch method) + + + +### Installation: 1. Start Blender. Open User Preferences, the addons tab 2. Click "install from file" and point Blender at the downloaded zip 3. Activate Tissue add-on from user preferences 3. Save user preferences if you want to have it on at startup. + + +### Contribute +Please help me keeping Tissue stable and updated, report any issue here: https://github.com/alessandro-zomparelli/tissue/issues + +Tissue is free and open-source. I really think that this is the power of Blender and I wanted to give my small contribution to it. +If you like my work and you want to help to continue the development of Tissue, please consider to make a small donation. Any small contribution is really appreciated, thanks! :-D + +Alessandro + + +[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARSDJWXVFZ346) + diff --git a/mesh_tissue/__init__.py b/mesh_tissue/__init__.py index d2fbb989..26fbd199 100644 --- a/mesh_tissue/__init__.py +++ b/mesh_tissue/__init__.py @@ -33,14 +33,13 @@ bl_info = { "name": "Tissue", "author": "Alessandro Zomparelli (Co-de-iT)", - "version": (0, 3, 4), - "blender": (2, 79, 0), + "version": (0, 3, 25), + "blender": (2, 80, 0), "location": "", "description": "Tools for Computational Design", "warning": "", - "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/" - "Py/Scripts/Mesh/Tissue", - "tracker_url": "https://plus.google.com/u/0/+AlessandroZomparelli/", + "wiki_url": "https://github.com/alessandro-zomparelli/tissue/wiki", + "tracker_url": "https://github.com/alessandro-zomparelli/tissue/issues", "category": "Mesh"} @@ -51,6 +50,7 @@ if "bpy" in locals(): importlib.reload(dual_mesh) importlib.reload(lattice) importlib.reload(uv_to_mesh) + importlib.reload(utils) else: from . import tessellate_numpy @@ -58,24 +58,76 @@ else: from . import dual_mesh from . import lattice from . import uv_to_mesh + from . import utils import bpy -from bpy.props import PointerProperty +from bpy.props import PointerProperty, CollectionProperty, BoolProperty +classes = ( + tessellate_numpy.tissue_tessellate_prop, + tessellate_numpy.tessellate, + tessellate_numpy.update_tessellate, + tessellate_numpy.TISSUE_PT_tessellate, + tessellate_numpy.rotate_face, + tessellate_numpy.TISSUE_PT_tessellate_object, + + colors_groups_exchanger.face_area_to_vertex_groups, + colors_groups_exchanger.vertex_colors_to_vertex_groups, + colors_groups_exchanger.vertex_group_to_vertex_colors, + colors_groups_exchanger.TISSUE_PT_weight, + colors_groups_exchanger.TISSUE_PT_color, + colors_groups_exchanger.weight_contour_curves, + colors_groups_exchanger.weight_contour_mask, + colors_groups_exchanger.weight_contour_displace, + colors_groups_exchanger.harmonic_weight, + colors_groups_exchanger.edges_deformation, + colors_groups_exchanger.edges_bending, + colors_groups_exchanger.weight_laplacian, + colors_groups_exchanger.reaction_diffusion, + colors_groups_exchanger.start_reaction_diffusion, + colors_groups_exchanger.TISSUE_PT_reaction_diffusion, + colors_groups_exchanger.reset_reaction_diffusion_weight, + colors_groups_exchanger.formula_prop, + colors_groups_exchanger.reaction_diffusion_prop, + colors_groups_exchanger.weight_formula, + colors_groups_exchanger.curvature_to_vertex_groups, + colors_groups_exchanger.weight_formula_wiki, + + dual_mesh.dual_mesh, + dual_mesh.dual_mesh_tessellated, + + lattice.lattice_along_surface, + + uv_to_mesh.uv_to_mesh +) def register(): - bpy.utils.register_module(__name__) + from bpy.utils import register_class + for cls in classes: + bpy.utils.register_class(cls) + #bpy.utils.register_module(__name__) bpy.types.Object.tissue_tessellate = PointerProperty( type=tessellate_numpy.tissue_tessellate_prop ) - + bpy.types.Object.formula_settings = CollectionProperty( + type=colors_groups_exchanger.formula_prop + ) + bpy.types.Object.reaction_diffusion_settings = PointerProperty( + type=colors_groups_exchanger.reaction_diffusion_prop + ) + # colors_groups_exchanger + bpy.app.handlers.frame_change_post.append(colors_groups_exchanger.reaction_diffusion_def) + #bpy.app.handlers.frame_change_post.append(tessellate_numpy.anim_tessellate) def unregister(): - tessellate_numpy.unregister() - colors_groups_exchanger.unregister() - dual_mesh.unregister() - lattice.unregister() - uv_to_mesh.unregister() + from bpy.utils import unregister_class + for cls in classes: + bpy.utils.unregister_class(cls) + #tessellate_numpy.unregister() + #colors_groups_exchanger.unregister() + #dual_mesh.unregister() + #lattice.unregister() + #uv_to_mesh.unregister() del bpy.types.Object.tissue_tessellate diff --git a/mesh_tissue/colors_groups_exchanger.py b/mesh_tissue/colors_groups_exchanger.py index 77c26d7e..0da15e85 100644 --- a/mesh_tissue/colors_groups_exchanger.py +++ b/mesh_tissue/colors_groups_exchanger.py @@ -16,12 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# ------------------------- COLORS / GROUPS EXCHANGER ------------------------ # +#-------------------------- COLORS / GROUPS EXCHANGER -------------------------# # # -# Vertex Color to Vertex Group allow you to convert colors channels to weight # +# Vertex Color to Vertex Group allow you to convert colors channles to weight # # maps. # # The main purpose is to use vertex colors to store information when importing # -# files from other software. The script works with the active vertex color # +# files from other softwares. The script works with the active vertex color # # slot. # # For use the command "Vertex Clors to Vertex Groups" use the search bar # # (space bar). # @@ -31,181 +31,1807 @@ # # # http://www.co-de-it.com/ # # # -# ############################################################################ # +################################################################################ + +import bpy, bmesh +import numpy as np +import math, timeit +from math import *#pi, sin +from statistics import mean, stdev +from mathutils import Vector +from numpy import * +try: from .numba_functions import numba_reaction_diffusion +except: pass + +from bpy.types import ( + Operator, + Panel, + PropertyGroup, + ) + +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, + IntProperty, + StringProperty, + FloatVectorProperty, + IntVectorProperty +) + +from .utils import * bl_info = { "name": "Colors/Groups Exchanger", "author": "Alessandro Zomparelli (Co-de-iT)", - "version": (0, 3, 2), - "blender": (2, 78, 0), + "version": (0, 3), + "blender": (2, 7, 9), "location": "", "description": ("Convert vertex colors channels to vertex groups and vertex" " groups to colors"), "warning": "", "wiki_url": "", + "tracker_url": "", "category": "Mesh"} +def reaction_diffusion_add_handler(self, context): + # remove existing handlers + old_handlers = [] + for h in bpy.app.handlers.frame_change_post: + if "reaction_diffusion" in str(h): + old_handlers.append(h) + for h in old_handlers: bpy.app.handlers.frame_change_post.remove(h) + # add new handler + bpy.app.handlers.frame_change_post.append(reaction_diffusion_def) + +class formula_prop(PropertyGroup): + name : StringProperty() + formula : StringProperty() + float_var : FloatVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5) + int_var : IntVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5) + +class reaction_diffusion_prop(PropertyGroup): + run : BoolProperty(default=False, update = reaction_diffusion_add_handler) + + time_steps : bpy.props.IntProperty( + name="Steps", default=10, min=0, soft_max=50, + description="Number of Steps") + + dt : bpy.props.FloatProperty( + name="dt", default=1, min=0, soft_max=0.2, + description="Time Step") + + diff_a : bpy.props.FloatProperty( + name="Diff A", default=0.1, min=0, soft_max=2, precision=3, + description="Diffusion A") + + diff_b : bpy.props.FloatProperty( + name="Diff B", default=0.05, min=0, soft_max=2, precision=3, + description="Diffusion B") + + f : bpy.props.FloatProperty( + name="f", default=0.055, min=0, soft_max=0.5, precision=3, + description="Feed Rate") + + k : bpy.props.FloatProperty( + name="k", default=0.062, min=0, soft_max=0.5, precision=3, + description="Kill Rate") + + diff_mult : bpy.props.FloatProperty( + name="Scale", default=1, min=0, soft_max=1, max=2, precision=2, + description="Multiplier for the diffusion of both substances") + +def compute_formula(ob=None, formula="rx", float_var=(0,0,0,0,0), int_var=(0,0,0,0,0)): + verts = ob.data.vertices + n_verts = len(verts) + + f1,f2,f3,f4,f5 = float_var + i1,i2,i3,i4,i5 = int_var + + do_groups = "w[" in formula + do_local = "lx" in formula or "ly" in formula or "lz" in formula + do_global = "gx" in formula or "gy" in formula or "gz" in formula + do_relative = "rx" in formula or "ry" in formula or "rz" in formula + do_normal = "nx" in formula or "ny" in formula or "nz" in formula + mat = ob.matrix_world + + for i in range(1000): + if "w["+str(i)+"]" in formula and i > len(ob.vertex_groups)-1: + return "w["+str(i)+"] not found" + + w = [] + for i in range(len(ob.vertex_groups)): + w.append([]) + if "w["+str(i)+"]" in formula: + vg = ob.vertex_groups[i] + for v in verts: + try: + w[i].append(vg.weight(v.index)) + except: + w[i].append(0) + w[i] = array(w[i]) + + start_time = timeit.default_timer() + # compute vertex coordinates + if do_local or do_relative or do_global: + co = [0]*n_verts*3 + verts.foreach_get('co', co) + np_co = array(co).reshape((n_verts, 3)) + lx, ly, lz = array(np_co).transpose() + if do_relative: + rx = np.interp(lx, (lx.min(), lx.max()), (0, +1)) + ry = np.interp(ly, (ly.min(), ly.max()), (0, +1)) + rz = np.interp(lz, (lz.min(), lz.max()), (0, +1)) + if do_global: + co = [v.co for v in verts] + global_co = [] + for v in co: + global_co.append(mat * v) + global_co = array(global_co).reshape((n_verts, 3)) + gx, gy, gz = array(global_co).transpose() + # compute vertex normals + if do_normal: + normal = [0]*n_verts*3 + verts.foreach_get('normal', normal) + normal = array(normal).reshape((n_verts, 3)) + nx, ny, nz = array(normal).transpose() + + try: + weight = eval(formula) + return weight + except: + return "There is something wrong" + print("Weight Formula: " + str(timeit.default_timer() - start_time)) + +class weight_formula_wiki(bpy.types.Operator): + bl_idname = "scene.weight_formula_wiki" + bl_label = "Online Documentation" + bl_options = {'REGISTER', 'UNDO'} -import bpy -from bpy.props import ( - BoolProperty, - EnumProperty, - FloatProperty, - IntProperty, - ) -from bpy.types import Operator -from math import ( - pi, sin, - ) + def execute(self, context): + bpy.ops.wm.url_open(url="https://github.com/alessandro-zomparelli/tissue/wiki/Weight-Tools#weight-formula") + return {'FINISHED'} + +class weight_formula(bpy.types.Operator): + bl_idname = "object.weight_formula" + bl_label = "Weight Formula" + bl_options = {'REGISTER', 'UNDO'} + + ex = [ + #'cos(arctan(nx/ny)*6 + sin(rz*30)*0.5)/2 + cos(arctan(nx/ny)*6 - sin(rz*30)*0.5 + pi/2)/2 + 0.5', + 'cos(arctan(nx/ny)*i1*2 + sin(rz*i3))/i2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i3))/i2 + 0.5', + 'cos(arctan(nx/ny)*i1*2 + sin(rz*i2))/2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i2))/2', + '(sin(arctan(nx/ny)*i1)*sin(nz*i1)+1)/2', + 'cos(arctan(nx/ny)*f1)', + 'cos(arctan(lx/ly)*f1 + sin(rz*f2)*f3)', + 'sin(nx*15) 0', + 'sin(nz*i1)', + 'w[0]**2', + 'sqrt((rx-0.5)**2 + (ry-0.5)**2)*2', + 'abs(0.5-rz)*2', + 'rx' + ] + ex_items = list((s,s,"") for s in ex) + ex_items.append(('CUSTOM', "User Formula", "")) + + examples : bpy.props.EnumProperty( + items = ex_items, default='CUSTOM', name="Examples") + + old_ex = "" + + formula : bpy.props.StringProperty( + name="Formula", default="", description="Formula to Evaluate") + bl_description = ("Generate a Vertex Group based on the given formula") + + slider_f01 : bpy.props.FloatProperty( + name="f1", default=1, description="Slider") + bl_description = ("Slider Float 1") + slider_f02 : bpy.props.FloatProperty( + name="f2", default=1, description="Slider") + bl_description = ("Slider Float 2") + slider_f03 : bpy.props.FloatProperty( + name="f3", default=1, description="Slider") + bl_description = ("Slider Float 3") + slider_f04 : bpy.props.FloatProperty( + name="f4", default=1, description="Slider") + bl_description = ("Slider Float 4") + slider_f05 : bpy.props.FloatProperty( + name="f5", default=1, description="Slider") + bl_description = ("Slider Float 5") + slider_i01 : bpy.props.IntProperty( + name="i1", default=1, description="Slider") + bl_description = ("Slider Integer 1") + slider_i02 : bpy.props.IntProperty( + name="i2", default=1, description="Slider") + bl_description = ("Slider Integer 2") + slider_i03 : bpy.props.IntProperty( + name="i3", default=1, description="Slider") + bl_description = ("Slider Integer 3") + slider_i04 : bpy.props.IntProperty( + name="i4", default=1, description="Slider") + bl_description = ("Slider Integer 4") + slider_i05 : bpy.props.IntProperty( + name="i5", default=1, description="Slider") + bl_description = ("Slider Integer 5") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=350) + + def draw(self, context): + layout = self.layout + #layout.label(text="Examples") + layout.prop(self, "examples", text="Examples") + #if self.examples == 'CUSTOM': + layout.label(text="Formula") + layout.prop(self, "formula", text="") + #try: self.examples = self.formula + #except: pass + + if self.examples != self.old_ex and self.examples != 'CUSTOM': + self.formula = self.examples + self.old_ex = self.examples + elif self.formula != self.examples: + self.examples = 'CUSTOM' + formula = self.formula + + layout.separator() + if "f1" in formula: layout.prop(self, "slider_f01") + if "f2" in formula: layout.prop(self, "slider_f02") + if "f3" in formula: layout.prop(self, "slider_f03") + if "f4" in formula: layout.prop(self, "slider_f04") + if "f5" in formula: layout.prop(self, "slider_f05") + if "i1" in formula: layout.prop(self, "slider_i01") + if "i2" in formula: layout.prop(self, "slider_i02") + if "i3" in formula: layout.prop(self, "slider_i03") + if "i4" in formula: layout.prop(self, "slider_i04") + if "i5" in formula: layout.prop(self, "slider_i05") + + layout.label(text="Variables (for each vertex):") + layout.label(text="lx, ly, lz: Local Coordinates", icon='ORIENTATION_LOCAL') + layout.label(text="gx, gy, gz: Global Coordinates", icon='WORLD') + layout.label(text="rx, ry, rz: Local Coordinates (0 to 1)", icon='NORMALIZE_FCURVES') + layout.label(text="nx, ny, nz: Normal Coordinates", icon='SNAP_NORMAL') + layout.label(text="w[0], w[1], w[2], ... : Vertex Groups", icon="GROUP_VERTEX") + layout.separator() + layout.label(text="f1, f2, f3, f4, f5: Float Sliders", icon='MOD_HUE_SATURATION')#PROPERTIES + layout.label(text="i1, i2, i3, i4, i5: Integer Sliders", icon='MOD_HUE_SATURATION') + layout.separator() + #layout.label(text="All mathematical functions are based on Numpy", icon='INFO') + #layout.label(text="https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.math.html", icon='INFO') + layout.operator("scene.weight_formula_wiki", icon="HELP") + #layout.label(text="(where 'i' is the index of the Vertex Group)") + + def execute(self, context): + ob = bpy.context.active_object + n_verts = len(ob.data.vertices) + #if self.examples == 'CUSTOM': + # formula = self.formula + #else: + #self.formula = self.examples + # formula = self.examples + + #f1, f2, f3, f4, f5 = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05 + #i1, i2, i3, i4, i5 = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05 + f_sliders = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05 + i_sliders = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05 + + if self.examples != self.old_ex and self.examples != 'CUSTOM': + self.formula = self.examples + self.old_ex = self.examples + elif self.formula != self.examples: + self.examples = 'CUSTOM' + formula = self.formula + + if formula == "": return {'FINISHED'} + vertex_group_name = "Formula " + formula + ob.vertex_groups.new(name=vertex_group_name) + + weight = compute_formula(ob, formula=formula, float_var=f_sliders, int_var=i_sliders) + if type(weight) == str: + self.report({'ERROR'}, weight) + return {'CANCELLED'} + + #print("time: " + str(timeit.default_timer() - start_time)) + + #start_time = timeit.default_timer() + weight = nan_to_num(weight) + if type(weight) == int or type(weight) == float: + for i in range(n_verts): + ob.vertex_groups[-1].add([i], weight, 'REPLACE') + elif type(weight) == ndarray: + for i in range(n_verts): + ob.vertex_groups[-1].add([i], weight[i], 'REPLACE') + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + + # Store formula settings + new_formula = ob.formula_settings.add() + new_formula.name = ob.vertex_groups[-1].name + new_formula.formula = formula + new_formula.int_var = i_sliders + new_formula.float_var = f_sliders + + #for f in ob.formula_settings: + # print(f.name, f.formula, f.int_var, f.float_var) + return {'FINISHED'} + +class _weight_laplacian(bpy.types.Operator): + bl_idname = "object._weight_laplacian" + bl_label = "Weight Laplacian" + bl_description = ("Compute the Vertex Group Laplacian") + bl_options = {'REGISTER', 'UNDO'} + + bounds : bpy.props.EnumProperty( + items=(('MANUAL', "Manual Bounds", ""), + ('POSITIVE', "Positive Only", ""), + ('NEGATIVE', "Negative Only", ""), + ('AUTOMATIC', "Automatic Bounds", "")), + default='AUTOMATIC', name="Bounds") + + mode : bpy.props.EnumProperty( + items=(('LENGTH', "Length Weight", ""), + ('SIMPLE', "Simple", "")), + default='SIMPLE', name="Evaluation Mode") + + min_def : bpy.props.FloatProperty( + name="Min", default=0, soft_min=-1, soft_max=0, + description="Laplacian value with 0 weight") + + max_def : bpy.props.FloatProperty( + name="Max", default=0.5, soft_min=0, soft_max=5, + description="Laplacian value with 1 weight") + + bounds_string = "" + + frame = None + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.label(text="Evaluation Mode") + col.prop(self, "mode", text="") + col.label(text="Bounds") + col.prop(self, "bounds", text="") + if self.bounds == 'MANUAL': + col.label(text="Strain Rate \u03B5:") + col.prop(self, "min_def") + col.prop(self, "max_def") + col.label(text="\u03B5" + ": from " + self.bounds_string) + + + def execute(self, context): + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + + group_id = ob.vertex_groups.active_index + input_group = ob.vertex_groups[group_id].name + print(input_group) + + group_name = "Laplacian" + ob.vertex_groups.new(name=group_name) + me = ob.data + bm = bmesh.new() + bm.from_mesh(me) + bm.edges.ensure_lookup_table() + + # store weight values + weight = [] + for v in me.vertices: + try: + weight.append(ob.vertex_groups[input_group].weight(v.index)) + except: + weight.append(0) + + n_verts = len(bm.verts) + lap = [0]*n_verts + #print(len(lap)) + #print(len(weight)) + for e in bm.edges: + if self.mode == 'LENGTH': + length = e.calc_length() + if length == 0: continue + id0 = e.verts[0].index + id1 = e.verts[1].index + lap[id0] += weight[id1]/length - weight[id0]/length + lap[id1] += weight[id0]/length - weight[id1]/length + else: + id0 = e.verts[0].index + id1 = e.verts[1].index + lap[id0] += weight[id1] - weight[id0] + lap[id1] += weight[id0] - weight[id1] + + mean_lap = mean(lap) + stdev_lap = stdev(lap) + filter_lap = [i for i in lap if mean_lap-2*stdev_lap < i < mean_lap+2*stdev_lap] + if self.bounds == 'MANUAL': + min_def = self.min_def + max_def = self.max_def + elif self.bounds == 'AUTOMATIC': + min_def = min(filter_lap) + max_def = max(filter_lap) + self.min_def = min_def + self.max_def = max_def + elif self.bounds == 'NEGATIVE': + min_def = 0 + max_def = min(filter_lap) + self.min_def = min_def + self.max_def = max_def + elif self.bounds == 'POSITIVE': + min_def = 0 + max_def = max(filter_lap) + self.min_def = min_def + self.max_def = max_def + delta_def = max_def - min_def + + # check undeformed errors + if delta_def == 0: delta_def = 0.0001 + + for i in range(len(lap)): + val = (lap[i]-min_def)/delta_def + if val > 0.7: print(str(val) + " " + str(lap[i])) + #val = weight[i] + 0.2*lap[i] + ob.vertex_groups[-1].add([i], val, 'REPLACE') + self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2)) + ob.vertex_groups[-1].name = group_name + " " + self.bounds_string + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + return {'FINISHED'} + +class weight_laplacian(bpy.types.Operator): + bl_idname = "object.weight_laplacian" + bl_label = "Weight Laplacian" + bl_description = ("Compute the Vertex Group Laplacian") + bl_options = {'REGISTER', 'UNDO'} + + steps : bpy.props.IntProperty( + name="Steps", default=10, min=0, soft_max=50, + description="Number of Steps") + + dt : bpy.props.FloatProperty( + name="dt", default=0.2, min=0, soft_max=0.2, + description="Time Step") + + diff_a : bpy.props.FloatProperty( + name="Diff A", default=1, min=0, soft_max=2, + description="Diffusion A") + + diff_b : bpy.props.FloatProperty( + name="Diff B", default=0.5, min=0, soft_max=2, + description="Diffusion B") + + f : bpy.props.FloatProperty( + name="f", default=0.055, min=0, soft_max=0.5, + description="Feed Rate") + + k : bpy.props.FloatProperty( + name="k", default=0.062, min=0, soft_max=0.5, + description="Kill Rate") + + diff_mult : bpy.props.FloatProperty( + name="Scale", default=1, min=0, soft_max=1, max=2, precision=2, + description="Multiplier for the diffusion of both substances") + + bounds_string = "" + + frame = None + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + + def execute(self, context): + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + + me = ob.data + bm = bmesh.new() + bm.from_mesh(me) + bm.edges.ensure_lookup_table() + + # store weight values + a = [] + b = [] + for v in me.vertices: + try: + a.append(ob.vertex_groups["A"].weight(v.index)) + except: + a.append(0) + try: + b.append(ob.vertex_groups["B"].weight(v.index)) + except: + b.append(0) + + a = array(a) + b = array(b) + f = self.f + k = self.k + diff_a = self.diff_a * self.diff_mult + diff_b = self.diff_b * self.diff_mult + dt = self.dt + + # initialize + n_verts = len(bm.verts) + # find max number of edges for vertex + max_edges = 0 + n_neighbors = [] + id_neighbors = [] + for v in bm.verts: + n_edges = len(v.link_edges) + max_edges = max(max_edges, n_edges) + n_neighbors.append(n_edges) + neighbors = [] + for e in link_edges: + for v1 in e.verts: + if v != v1: neighbors.append(v1.index) + id_neighbors.append(neighbors) + n_neighbors = array(n_neighbors) + + + a = [[] for i in range(n_verts)] + lap_map = [] + + for e in bm.edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + lap_map[id0].append(id1) + lap_map[id1].append(id0) + + e1 = array(e1) + e2 = array(e2) + lap_a = a[e1] + + for i in range(self.steps): + + lap_a = zeros((n_verts))#[0]*n_verts + lap_b = zeros((n_verts))#[0]*n_verts + #print(len(lap)) + #print(len(weight)) + for e in bm.edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + lap_a[id0] += a[id1] - a[id0] + lap_a[id1] += a[id0] - a[id1] + lap_b[id0] += b[id1] - b[id0] + lap_b[id1] += b[id0] - b[id1] + ab2 = a*b**2 + a += (diff_a*lap_a - ab2 + f*(1-a))*dt + b += (diff_b*lap_b + ab2 - (k+f)*b)*dt + + for i in range(n_verts): + ob.vertex_groups['A'].add([i], a[i], 'REPLACE') + ob.vertex_groups['B'].add([i], b[i], 'REPLACE') + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + return {'FINISHED'} + + +class reaction_diffusion(bpy.types.Operator): + bl_idname = "object.reaction_diffusion" + bl_label = "Reaction Diffusion" + bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B") + bl_options = {'REGISTER', 'UNDO'} + + steps : bpy.props.IntProperty( + name="Steps", default=10, min=0, soft_max=50, + description="Number of Steps") + + dt : bpy.props.FloatProperty( + name="dt", default=0.2, min=0, soft_max=0.2, + description="Time Step") + + diff_a : bpy.props.FloatProperty( + name="Diff A", default=1, min=0, soft_max=2, + description="Diffusion A") + + diff_b : bpy.props.FloatProperty( + name="Diff B", default=0.5, min=0, soft_max=2, + description="Diffusion B") + + f : bpy.props.FloatProperty( + name="f", default=0.055, min=0, soft_max=0.5, + description="Feed Rate") + + k : bpy.props.FloatProperty( + name="k", default=0.062, min=0, soft_max=0.5, + description="Kill Rate") + + bounds_string = "" + + frame = None + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + + def execute(self, context): + #bpy.app.handlers.frame_change_post.remove(reaction_diffusion_def) + reaction_diffusion_add_handler(self, context) + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + + me = ob.data + bm = bmesh.new() + bm.from_mesh(me) + bm.edges.ensure_lookup_table() + + # store weight values + a = [] + b = [] + for v in me.vertices: + try: + a.append(ob.vertex_groups["A"].weight(v.index)) + except: + a.append(0) + try: + b.append(ob.vertex_groups["B"].weight(v.index)) + except: + b.append(0) + + a = array(a) + b = array(b) + f = self.f + k = self.k + diff_a = self.diff_a + diff_b = self.diff_b + dt = self.dt + n_verts = len(bm.verts) + + for i in range(self.steps): + + lap_a = zeros((n_verts))#[0]*n_verts + lap_b = zeros((n_verts))#[0]*n_verts + #print(len(lap)) + #print(len(weight)) + for e in bm.edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + lap_a[id0] += a[id1] - a[id0] + lap_a[id1] += a[id0] - a[id1] + lap_b[id0] += b[id1] - b[id0] + lap_b[id1] += b[id0] - b[id1] + ab2 = a*b**2 + a += (diff_a*lap_a - ab2 + f*(1-a))*dt + b += (diff_b*lap_b + ab2 - (k+f)*b)*dt + + for i in range(n_verts): + ob.vertex_groups['A'].add([i], a[i], 'REPLACE') + ob.vertex_groups['B'].add([i], b[i], 'REPLACE') + ob.vertex_groups.update() + ob.data.update() + + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + return {'FINISHED'} + + +class edges_deformation(bpy.types.Operator): + bl_idname = "object.edges_deformation" + bl_label = "Edges Deformation" + bl_description = ("Compute Weight based on the deformation of edges"+ + "according to visible modifiers.") + bl_options = {'REGISTER', 'UNDO'} + + bounds : bpy.props.EnumProperty( + items=(('MANUAL', "Manual Bounds", ""), + ('COMPRESSION', "Compressed Only", ""), + ('TENSION', "Extended Only", ""), + ('AUTOMATIC', "Automatic Bounds", "")), + default='AUTOMATIC', name="Bounds") + + mode : bpy.props.EnumProperty( + items=(('MAX', "Max Deformation", ""), + ('MEAN', "Average Deformation", "")), + default='MEAN', name="Evaluation Mode") + + min_def : bpy.props.FloatProperty( + name="Min", default=0, soft_min=-1, soft_max=0, + description="Deformations with 0 weight") + + max_def : bpy.props.FloatProperty( + name="Max", default=0.5, soft_min=0, soft_max=5, + description="Deformations with 1 weight") + + bounds_string = "" + + frame = None + + @classmethod + def poll(cls, context): + return len(context.object.modifiers) > 0 + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.label(text="Evaluation Mode") + col.prop(self, "mode", text="") + col.label(text="Bounds") + col.prop(self, "bounds", text="") + if self.bounds == 'MANUAL': + col.label(text="Strain Rate \u03B5:") + col.prop(self, "min_def") + col.prop(self, "max_def") + col.label(text="\u03B5" + ": from " + self.bounds_string) + + def execute(self, context): + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + + # check if the object is Cloth or Softbody + physics = False + for m in ob.modifiers: + if m.type == 'CLOTH' or m.type == 'SOFT_BODY': + physics = True + if context.scene.frame_current == 1 and self.frame != None: + context.scene.frame_current = self.frame + break + if not physics: self.frame = None + + if self.mode == 'MEAN': group_name = "Average Deformation" + elif self.mode == 'MAX': group_name = "Max Deformation" + ob.vertex_groups.new(name=group_name) + me0 = ob.data + + me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy() + if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges): + self.report({'ERROR'}, "The topology of the object should be" + + "unaltered") + return {'CANCELLED'} + + bm0 = bmesh.new() + bm0.from_mesh(me0) + bm = bmesh.new() + bm.from_mesh(me) + deformations = [] + for e0, e in zip(bm0.edges, bm.edges): + try: + l0 = e0.calc_length() + l1 = e.calc_length() + epsilon = (l1 - l0)/l0 + deformations.append(epsilon) + except: deformations.append(1) + v_deformations = [] + for v in bm.verts: + vdef = [] + for e in v.link_edges: + vdef.append(deformations[e.index]) + if self.mode == 'MEAN': v_deformations.append(mean(vdef)) + elif self.mode == 'MAX': v_deformations.append(max(vdef, key=abs)) + #elif self.mode == 'MIN': v_deformations.append(min(vdef, key=abs)) + + if self.bounds == 'MANUAL': + min_def = self.min_def + max_def = self.max_def + elif self.bounds == 'AUTOMATIC': + min_def = min(v_deformations) + max_def = max(v_deformations) + self.min_def = min_def + self.max_def = max_def + elif self.bounds == 'COMPRESSION': + min_def = 0 + max_def = min(v_deformations) + self.min_def = min_def + self.max_def = max_def + elif self.bounds == 'TENSION': + min_def = 0 + max_def = max(v_deformations) + self.min_def = min_def + self.max_def = max_def + delta_def = max_def - min_def + + # check undeformed errors + if delta_def == 0: + if self.bounds == 'MANUAL': + delta_def = 0.0001 + else: + message = "The object doesn't have deformations." + if physics: + message = message + ("\nIf you are using Physics try to " + + "save it in the cache before.") + self.report({'ERROR'}, message) + return {'CANCELLED'} + else: + if physics: + self.frame = context.scene.frame_current + + for i in range(len(v_deformations)): + weight = (v_deformations[i] - min_def)/delta_def + ob.vertex_groups[-1].add([i], weight, 'REPLACE') + self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2)) + ob.vertex_groups[-1].name = group_name + " " + self.bounds_string + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + bpy.data.meshes.remove(me) + return {'FINISHED'} + +class edges_bending(bpy.types.Operator): + bl_idname = "object.edges_bending" + bl_label = "Edges Bending" + bl_description = ("Compute Weight based on the bending of edges"+ + "according to visible modifiers.") + bl_options = {'REGISTER', 'UNDO'} + + bounds : bpy.props.EnumProperty( + items=(('MANUAL', "Manual Bounds", ""), + ('POSITIVE', "Positive Only", ""), + ('NEGATIVE', "Negative Only", ""), + ('UNSIGNED', "Absolute Bending", ""), + ('AUTOMATIC', "Signed Bending", "")), + default='AUTOMATIC', name="Bounds") + + min_def : bpy.props.FloatProperty( + name="Min", default=-10, soft_min=-45, soft_max=45, + description="Deformations with 0 weight") + + max_def : bpy.props.FloatProperty( + name="Max", default=10, soft_min=-45, soft_max=45, + description="Deformations with 1 weight") + + bounds_string = "" + frame = None + + @classmethod + def poll(cls, context): + return len(context.object.modifiers) > 0 + + def draw(self, context): + layout = self.layout + layout.label(text="Bounds") + layout.prop(self, "bounds", text="") + if self.bounds == 'MANUAL': + layout.prop(self, "min_def") + layout.prop(self, "max_def") + + def execute(self, context): + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + + group_name = "Edges Bending" + ob.vertex_groups.new(name=group_name) + + # check if the object is Cloth or Softbody + physics = False + for m in ob.modifiers: + if m.type == 'CLOTH' or m.type == 'SOFT_BODY': + physics = True + if context.scene.frame_current == 1 and self.frame != None: + context.scene.frame_current = self.frame + break + if not physics: self.frame = None + + #ob.data.update() + #context.scene.update() + me0 = ob.data + me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy() + if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges): + self.report({'ERROR'}, "The topology of the object should be" + + "unaltered") + bm0 = bmesh.new() + bm0.from_mesh(me0) + bm = bmesh.new() + bm.from_mesh(me) + deformations = [] + for e0, e in zip(bm0.edges, bm.edges): + try: + ang = e.calc_face_angle_signed() + ang0 = e0.calc_face_angle_signed() + if self.bounds == 'UNSIGNED': + deformations.append(abs(ang-ang0)) + else: + deformations.append(ang-ang0) + except: deformations.append(0) + v_deformations = [] + for v in bm.verts: + vdef = [] + for e in v.link_edges: + vdef.append(deformations[e.index]) + v_deformations.append(mean(vdef)) + print(v_deformations) + if self.bounds == 'MANUAL': + min_def = radians(self.min_def) + max_def = radians(self.max_def) + elif self.bounds == 'AUTOMATIC': + min_def = min(v_deformations) + max_def = max(v_deformations) + elif self.bounds == 'POSITIVE': + min_def = 0 + max_def = min(v_deformations) + elif self.bounds == 'NEGATIVE': + min_def = 0 + max_def = max(v_deformations) + elif self.bounds == 'UNSIGNED': + min_def = 0 + max_def = max(v_deformations) + delta_def = max_def - min_def + + # check undeformed errors + if delta_def == 0: + if self.bounds == 'MANUAL': + delta_def = 0.0001 + else: + message = "The object doesn't have deformations." + if physics: + message = message + ("\nIf you are using Physics try to " + + "save it in the cache before.") + self.report({'ERROR'}, message) + return {'CANCELLED'} + else: + if physics: + self.frame = context.scene.frame_current + + for i in range(len(v_deformations)): + weight = (v_deformations[i] - min_def)/delta_def + ob.vertex_groups[-1].add([i], weight, 'REPLACE') + self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2)) + ob.vertex_groups[-1].name = group_name + " " + self.bounds_string + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + bpy.data.meshes.remove(me) + return {'FINISHED'} + +class weight_contour_displace(bpy.types.Operator): + bl_idname = "object.weight_contour_displace" + bl_label = "Contour Displace" + bl_description = ("") + bl_options = {'REGISTER', 'UNDO'} + + use_modifiers : bpy.props.BoolProperty( + name="Use Modifiers", default=True, + description="Apply all the modifiers") + min_iso : bpy.props.FloatProperty( + name="Min Iso Value", default=0.49, min=0, max=1, + description="Threshold value") + max_iso : bpy.props.FloatProperty( + name="Max Iso Value", default=0.51, min=0, max=1, + description="Threshold value") + n_cuts : bpy.props.IntProperty( + name="Cuts", default=2, min=1, soft_max=10, + description="Number of cuts in the selected range of values") + bool_displace : bpy.props.BoolProperty( + name="Add Displace", default=True, description="Add Displace Modifier") + bool_flip : bpy.props.BoolProperty( + name="Flip", default=False, description="Flip Output Weight") + + weight_mode : bpy.props.EnumProperty( + items=[('Remapped', 'Remapped', 'Remap values'), + ('Alternate', 'Alternate', 'Alternate 0 and 1'), + ('Original', 'Original', 'Keep original Vertex Group')], + name="Weight", description="Choose how to convert vertex group", + default="Remapped", options={'LIBRARY_EDITABLE'}) + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=350) + + def execute(self, context): + start_time = timeit.default_timer() + try: + check = bpy.context.object.vertex_groups[0] + except: + self.report({'ERROR'}, "The object doesn't have Vertex Groups") + return {'CANCELLED'} + + ob0 = bpy.context.object + + group_id = ob0.vertex_groups.active_index + vertex_group_name = ob0.vertex_groups[group_id].name + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + if self.use_modifiers: + #me0 = ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy() + me0 = simple_to_mesh(ob0) + else: + me0 = ob0.data.copy() + + # generate new bmesh + bm = bmesh.new() + bm.from_mesh(me0) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + # store weight values + weight = [] + ob = bpy.data.objects.new("temp", me0) + for g in ob0.vertex_groups: + ob.vertex_groups.new(name=g.name) + for v in me0.vertices: + try: + weight.append(ob.vertex_groups[vertex_group_name].weight(v.index)) + except: + weight.append(0) + + # define iso values + iso_values = [] + for i_cut in range(self.n_cuts): + delta_iso = abs(self.max_iso - self.min_iso) + min_iso = min(self.min_iso, self.max_iso) + max_iso = max(self.min_iso, self.max_iso) + if delta_iso == 0: iso_val = min_iso + elif self.n_cuts > 1: iso_val = i_cut/(self.n_cuts-1)*delta_iso + min_iso + else: iso_val = (self.max_iso + self.min_iso)/2 + iso_values.append(iso_val) + + # Start Cuts Iterations + filtered_edges = bm.edges + for iso_val in iso_values: + delete_edges = [] + + faces_mask = [] + for f in bm.faces: + w_min = 2 + w_max = 2 + for v in f.verts: + w = weight[v.index] + if w_min == 2: + w_max = w_min = w + if w > w_max: w_max = w + if w < w_min: w_min = w + if w_min < iso_val and w_max > iso_val: + faces_mask.append(f) + break + #print("selected faces:" + str(len(faces_mask))) + + #link_faces = [[f for f in e.link_faces] for e in bm.edges] + + #faces_todo = [f.select for f in bm.faces] + #faces_todo = [True for f in bm.faces] + verts = [] + edges = [] + edges_id = {} + _filtered_edges = [] + n_verts = len(bm.verts) + count = n_verts + for e in filtered_edges: + #id0 = e.vertices[0] + #id1 = e.vertices[1] + id0 = e.verts[0].index + id1 = e.verts[1].index + w0 = weight[id0] + w1 = weight[id1] + + if w0 == w1: continue + elif w0 > iso_val and w1 > iso_val: + _filtered_edges.append(e) + continue + elif w0 < iso_val and w1 < iso_val: continue + elif w0 == iso_val or w1 == iso_val: + _filtered_edges.append(e) + continue + else: + v0 = bm.verts[id0].co + v1 = bm.verts[id1].co + v = v0.lerp(v1, (iso_val-w0)/(w1-w0)) + if e not in delete_edges: + delete_edges.append(e) + verts.append(v) + edges_id[str(id0)+"_"+str(id1)] = count + edges_id[str(id1)+"_"+str(id0)] = count + count += 1 + _filtered_edges.append(e) + filtered_edges = _filtered_edges + #print("creating faces") + splitted_faces = [] + + switch = False + # splitting faces + for f in faces_mask: + # create sub-faces slots. Once a new vertex is reached it will + # change slot, storing the next vertices for a new face. + build_faces = [[],[]] + #switch = False + verts0 = [v.index for v in f.verts] + verts1 = list(verts0) + verts1.append(verts1.pop(0)) # shift list + for id0, id1 in zip(verts0, verts1): + + # add first vertex to active slot + build_faces[switch].append(id0) + + # try to split edge + try: + # check if the edge must be splitted + new_vert = edges_id[str(id0)+"_"+str(id1)] + # add new vertex + build_faces[switch].append(new_vert) + # if there is an open face on the other slot + if len(build_faces[not switch]) > 0: + # store actual face + splitted_faces.append(build_faces[switch]) + # reset actual faces and switch + build_faces[switch] = [] + # change face slot + switch = not switch + # continue previous face + build_faces[switch].append(new_vert) + except: pass + if len(build_faces[not switch]) == 2: + build_faces[not switch].append(id0) + if len(build_faces[not switch]) > 2: + splitted_faces.append(build_faces[not switch]) + # add last face + splitted_faces.append(build_faces[switch]) + #del_faces.append(f.index) + + #print("generate new bmesh") + # adding new vertices + for v in verts: new_vert = bm.verts.new(v) + bm.verts.index_update() + bm.verts.ensure_lookup_table() + # adding new faces + missed_faces = [] + added_faces = [] + for f in splitted_faces: + try: + face_verts = [bm.verts[i] for i in f] + new_face = bm.faces.new(face_verts) + for e in new_face.edges: + filtered_edges.append(e) + except: + missed_faces.append(f) + + #print("missed " + str(len(missed_faces)) + " faces") + bm.faces.ensure_lookup_table() + # updating weight values + weight = weight + [iso_val]*len(verts) + + # deleting old edges/faces + bm.edges.ensure_lookup_table() + for e in delete_edges: + bm.edges.remove(e) + _filtered_edges = [] + for e in filtered_edges: + if e not in delete_edges: _filtered_edges.append(e) + filtered_edges = _filtered_edges + + #print("creating curve") + name = ob0.name + '_ContourDisp' + me = bpy.data.meshes.new(name) + bm.to_mesh(me) + ob = bpy.data.objects.new(name, me) + + # Link object to scene and make active + scn = bpy.context.scene + bpy.context.collection.objects.link(ob) + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + ob0.select_set(False) + + # generate new vertex group + for g in ob0.vertex_groups: + ob.vertex_groups.new(name=g.name) + #ob.vertex_groups.new(name=vertex_group_name) + + #print("doing weight") + all_weight = weight + [iso_val]*len(verts) + #mult = 1/(1-iso_val) + for id in range(len(all_weight)): + #if False: w = (all_weight[id]-iso_val)*mult + w = all_weight[id] + if self.weight_mode == 'Alternate': + direction = self.bool_flip + for i in range(len(iso_values)-1): + val0, val1 = iso_values[i], iso_values[i+1] + if val0 < w <= val1: + if direction: w1 = (w-val0)/(val1-val0) + else: w1 = (val1-w)/(val1-val0) + direction = not direction + if w < iso_values[0]: w1 = not self.bool_flip + if w > iso_values[-1]: w1 = not direction + elif self.weight_mode == 'Remapped': + if w < min_iso: w1 = 0 + elif w > max_iso: w1 = 1 + else: w1 = (w - min_iso)/delta_iso + else: + if self.bool_flip: w1 = 1-w + else: w1 = w + ob.vertex_groups[vertex_group_name].add([id], w1, 'REPLACE') + #print("weight done") + #for id in range(len(weight), len(ob.data.vertices)): + # ob.vertex_groups[vertex_group_name].add([id], iso_val*0, 'ADD') + + + ob.vertex_groups.active_index = group_id + + # align new object + ob.matrix_world = ob0.matrix_world + + # Displace Modifier + if self.bool_displace: + ob.modifiers.new(type='DISPLACE', name='Displace') + ob.modifiers["Displace"].mid_level = 0 + ob.modifiers["Displace"].strength = 0.1 + ob.modifiers['Displace'].vertex_group = vertex_group_name + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + print("Contour Displace time: " + str(timeit.default_timer() - start_time) + " sec") + + bpy.data.meshes.remove(me0) + + return {'FINISHED'} + +class weight_contour_mask(bpy.types.Operator): + bl_idname = "object.weight_contour_mask" + bl_label = "Contour Mask" + bl_description = ("") + bl_options = {'REGISTER', 'UNDO'} + + use_modifiers : bpy.props.BoolProperty( + name="Use Modifiers", default=True, + description="Apply all the modifiers") + iso : bpy.props.FloatProperty( + name="Iso Value", default=0.5, soft_min=0, soft_max=1, + description="Threshold value") + bool_solidify : bpy.props.BoolProperty( + name="Solidify", default=True, description="Add Solidify Modifier") + normalize_weight : bpy.props.BoolProperty( + name="Normalize Weight", default=True, + description="Normalize weight of remaining vertices") + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + def execute(self, context): + start_time = timeit.default_timer() + try: + check = bpy.context.object.vertex_groups[0] + except: + self.report({'ERROR'}, "The object doesn't have Vertex Groups") + return {'CANCELLED'} + + ob0 = bpy.context.object + + iso_val = self.iso + group_id = ob0.vertex_groups.active_index + vertex_group_name = ob0.vertex_groups[group_id].name + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + if self.use_modifiers: + me0 = simple_to_mesh(ob0)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy() + else: + me0 = ob0.data.copy() + + # generate new bmesh + bm = bmesh.new() + bm.from_mesh(me0) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + # store weight values + weight = [] + ob = bpy.data.objects.new("temp", me0) + for g in ob0.vertex_groups: + ob.vertex_groups.new(name=g.name) + for v in me0.vertices: + try: + #weight.append(v.groups[vertex_group_name].weight) + weight.append(ob.vertex_groups[vertex_group_name].weight(v.index)) + except: + weight.append(0) + + faces_mask = [] + for f in bm.faces: + w_min = 2 + w_max = 2 + for v in f.verts: + w = weight[v.index] + if w_min == 2: + w_max = w_min = w + if w > w_max: w_max = w + if w < w_min: w_min = w + if w_min < iso_val and w_max > iso_val: + faces_mask.append(f) + break + #print("selected faces:" + str(len(faces_mask))) + + #link_faces = [[f for f in e.link_faces] for e in bm.edges] + + filtered_edges = bm.edges# me0.edges + faces_todo = [f.select for f in bm.faces] + verts = [] + edges = [] + delete_edges = [] + edges_id = {} + _filtered_edges = [] + n_verts = len(bm.verts) + count = n_verts + for e in filtered_edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + w0 = weight[id0] + w1 = weight[id1] + + if w0 == w1: continue + elif w0 > iso_val and w1 > iso_val: + continue + elif w0 < iso_val and w1 < iso_val: continue + elif w0 == iso_val or w1 == iso_val: continue + else: + v0 = me0.vertices[id0].co + v1 = me0.vertices[id1].co + v = v0.lerp(v1, (iso_val-w0)/(w1-w0)) + delete_edges.append(e) + verts.append(v) + edges_id[str(id0)+"_"+str(id1)] = count + edges_id[str(id1)+"_"+str(id0)] = count + count += 1 + + #print("creating faces") + splitted_faces = [] + + switch = False + # splitting faces + for f in faces_mask: + # create sub-faces slots. Once a new vertex is reached it will + # change slot, storing the next vertices for a new face. + build_faces = [[],[]] + #switch = False + verts0 = list(me0.polygons[f.index].vertices) + verts1 = list(verts0) + verts1.append(verts1.pop(0)) # shift list + for id0, id1 in zip(verts0, verts1): + + # add first vertex to active slot + build_faces[switch].append(id0) + + # try to split edge + try: + # check if the edge must be splitted + new_vert = edges_id[str(id0)+"_"+str(id1)] + # add new vertex + build_faces[switch].append(new_vert) + # if there is an open face on the other slot + if len(build_faces[not switch]) > 0: + # store actual face + splitted_faces.append(build_faces[switch]) + # reset actual faces and switch + build_faces[switch] = [] + # change face slot + switch = not switch + # continue previous face + build_faces[switch].append(new_vert) + except: pass + if len(build_faces[not switch]) == 2: + build_faces[not switch].append(id0) + if len(build_faces[not switch]) > 2: + splitted_faces.append(build_faces[not switch]) + # add last face + splitted_faces.append(build_faces[switch]) + + #print("generate new bmesh") + # adding new vertices + for v in verts: bm.verts.new(v) + bm.verts.ensure_lookup_table() + + # deleting old edges/faces + bm.edges.ensure_lookup_table() + remove_edges = [] + for e in delete_edges: bm.edges.remove(e) + + bm.verts.ensure_lookup_table() + # adding new faces + missed_faces = [] + for f in splitted_faces: + try: + face_verts = [bm.verts[i] for i in f] + bm.faces.new(face_verts) + except: + missed_faces.append(f) + #print("missed " + str(len(missed_faces)) + " faces") + + # Mask geometry + if(True): + all_weight = weight + [iso_val+0.0001]*len(verts) + weight = [] + for w, v in zip(all_weight, bm.verts): + if w < iso_val: bm.verts.remove(v) + else: weight.append(w) + + # Create mesh and object + #print("creating curve") + name = ob0.name + '_ContourMask_{:.3f}'.format(iso_val) + me = bpy.data.meshes.new(name) + bm.to_mesh(me) + ob = bpy.data.objects.new(name, me) + + # Link object to scene and make active + scn = bpy.context.scene + bpy.context.collection.objects.link(ob) + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + ob0.select_set(False) + + # generate new vertex group + for g in ob0.vertex_groups: + ob.vertex_groups.new(name=g.name) + + if iso_val != 1: mult = 1/(1-iso_val) + else: mult = 1 + for id in range(len(weight)): + if self.normalize_weight: w = (weight[id]-iso_val)*mult + else: w = weight[id] + ob.vertex_groups[vertex_group_name].add([id], w, 'REPLACE') + ob.vertex_groups.active_index = group_id + + # align new object + ob.matrix_world = ob0.matrix_world + + # Add Solidify + if self.bool_solidify and True: + ob.modifiers.new(type='SOLIDIFY', name='Solidify') + ob.modifiers['Solidify'].thickness = 0.05 + ob.modifiers['Solidify'].offset = 0 + ob.modifiers['Solidify'].vertex_group = vertex_group_name + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + print("Contour Mask time: " + str(timeit.default_timer() - start_time) + " sec") + + bpy.data.meshes.remove(me0) + + return {'FINISHED'} + +class weight_contour_curves(bpy.types.Operator): + bl_idname = "object.weight_contour_curves" + bl_label = "Contour Curves" + bl_description = ("") + bl_options = {'REGISTER', 'UNDO'} + + use_modifiers : bpy.props.BoolProperty( + name="Use Modifiers", default=True, + description="Apply all the modifiers") + + min_iso : bpy.props.FloatProperty( + name="Min Value", default=0., soft_min=0, soft_max=1, + description="Minimum weight value") + max_iso : bpy.props.FloatProperty( + name="Max Value", default=1, soft_min=0, soft_max=1, + description="Maximum weight value") + n_curves : bpy.props.IntProperty( + name="Curves", default=3, soft_min=1, soft_max=10, + description="Number of Contour Curves") + + min_rad : bpy.props.FloatProperty( + name="Min Radius", default=0.25, soft_min=0, soft_max=1, + description="Minimum Curve Radius") + max_rad : bpy.props.FloatProperty( + name="Max Radius", default=0.75, soft_min=0, soft_max=1, + description="Maximum Curve Radius") + + @classmethod + def poll(cls, context): + ob = context.object + return len(ob.vertex_groups) > 0 or ob.type == 'CURVE' + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=350) + + def execute(self, context): + start_time = timeit.default_timer() + try: + check = bpy.context.object.vertex_groups[0] + except: + self.report({'ERROR'}, "The object doesn't have Vertex Groups") + return {'CANCELLED'} + ob0 = bpy.context.object + + group_id = ob0.vertex_groups.active_index + vertex_group_name = ob0.vertex_groups[group_id].name + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + if self.use_modifiers: + me0 = simple_to_mesh(ob0) #ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy() + else: + me0 = ob0.data.copy() + + # generate new bmesh + bm = bmesh.new() + bm.from_mesh(me0) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + # store weight values + weight = [] + ob = bpy.data.objects.new("temp", me0) + for g in ob0.vertex_groups: + ob.vertex_groups.new(name=g.name) + for v in me0.vertices: + try: + #weight.append(v.groups[vertex_group_name].weight) + weight.append(ob.vertex_groups[vertex_group_name].weight(v.index)) + except: + weight.append(0) + + filtered_edges = bm.edges + total_verts = [] + total_segments = [] + radius = [] + # start iterate contours levels + for c in range(self.n_curves): + min_iso = min(self.min_iso, self.max_iso) + max_iso = max(self.min_iso, self.max_iso) + try: + iso_val = c*(max_iso-min_iso)/(self.n_curves-1)+min_iso + if iso_val < 0: iso_val = (min_iso + max_iso)/2 + except: + iso_val = (min_iso + max_iso)/2 + #print(iso_val) + faces_mask = [] + for f in bm.faces: + w_min = 2 + w_max = 2 + for v in f.verts: + w = weight[v.index] + if w_min == 2: + w_max = w_min = w + if w > w_max: w_max = w + if w < w_min: w_min = w + if w_min < iso_val and w_max > iso_val: + faces_mask.append(f) + break + + faces_todo = [f.select for f in bm.faces] + verts = [] + + edges_id = {} + _filtered_edges = [] + n_verts = len(bm.verts) + count = len(total_verts) + for e in filtered_edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + w0 = weight[id0] + w1 = weight[id1] + + if w0 == w1: continue + elif w0 > iso_val and w1 > iso_val: + _filtered_edges.append(e) + continue + elif w0 < iso_val and w1 < iso_val: continue + elif w0 == iso_val or w1 == iso_val: + _filtered_edges.append(e) + continue + else: + #v0 = me0.vertices[id0].select = True + #v1 = me0.vertices[id1].select = True + v0 = me0.vertices[id0].co + v1 = me0.vertices[id1].co + v = v0.lerp(v1, (iso_val-w0)/(w1-w0)) + verts.append(v) + edges_id[e.index] = count + count += 1 + _filtered_edges.append(e) + filtered_edges = _filtered_edges + + if len(verts) == 0: continue + + # finding segments + segments = [] + for f in faces_mask: + seg = [] + for e in f.edges: + try: + seg.append(edges_id[e.index]) + if len(seg) == 2: + segments.append(seg) + seg = [] + except: pass + + total_segments = total_segments + segments + total_verts = total_verts + verts + + # Radius + + try: + iso_rad = c*(self.max_rad-self.min_rad)/(self.n_curves-1)+self.min_rad + if iso_rad < 0: iso_rad = (self.min_rad + self.max_rad)/2 + except: + iso_rad = (self.min_rad + self.max_rad)/2 + radius = radius + [iso_rad]*len(verts) -class vertex_colors_to_vertex_groups(Operator): + #print("generate new bmesh") + bm = bmesh.new() + # adding new vertices + for v in total_verts: bm.verts.new(v) + bm.verts.ensure_lookup_table() + + # adding new edges + for s in total_segments: + try: + pts = [bm.verts[i] for i in s] + bm.edges.new(pts) + except: pass + + try: + #print("creating curve") + name = ob0.name + '_ContourCurves' + me = bpy.data.meshes.new(name) + bm.to_mesh(me) + ob = bpy.data.objects.new(name, me) + + # Link object to scene and make active + scn = bpy.context.scene + bpy.context.collection.objects.link(ob) + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + ob0.select_set(False) + + bpy.ops.object.convert(target='CURVE') + ob = context.object + count = 0 + for s in ob.data.splines: + for p in s.points: + p.radius = radius[count] + count += 1 + ob.data.bevel_depth = 0.01 + ob.data.fill_mode = 'FULL' + ob.data.bevel_resolution = 3 + except: + self.report({'ERROR'}, "There are no values in the chosen range") + return {'CANCELLED'} + + # align new object + ob.matrix_world = ob0.matrix_world + print("Contour Curves time: " + str(timeit.default_timer() - start_time) + " sec") + + bpy.data.meshes.remove(me0) + bpy.data.meshes.remove(me) + + return {'FINISHED'} + +class vertex_colors_to_vertex_groups(bpy.types.Operator): bl_idname = "object.vertex_colors_to_vertex_groups" bl_label = "Vertex Color" - bl_description = ("Convert the active Vertex Color into a Vertex Group") bl_options = {'REGISTER', 'UNDO'} - - red: BoolProperty( - name="Red Channel", - default=False, - description="Convert Red Channel" - ) - green: BoolProperty( - name="Green Channel", - default=False, - description="Convert Green Channel" - ) - blue: BoolProperty( - name="Blue Channel", - default=False, - description="Convert Blue Channel" - ) - value: BoolProperty( - name="Value Channel", - default=True, - description="Convert Value Channel" - ) - invert: BoolProperty( - name="Invert", - default=False, - description="Invert all Color Channels" - ) + bl_description = ("Convert the active Vertex Color into a Vertex Group.") + + red : bpy.props.BoolProperty( + name="red channel", default=False, description="convert red channel") + green : bpy.props.BoolProperty( + name="green channel", default=False, + description="convert green channel") + blue : bpy.props.BoolProperty( + name="blue channel", default=False, description="convert blue channel") + value : bpy.props.BoolProperty( + name="value channel", default=True, description="convert value channel") + invert : bpy.props.BoolProperty( + name="invert", default=False, description="invert all color channels") + + @classmethod + def poll(cls, context): + return len(context.object.data.vertex_colors) > 0 def execute(self, context): obj = bpy.context.active_object - ids = len(obj.vertex_groups) - id_red = ids - id_green = ids - id_blue = ids - id_value = ids - - # Fix T53350: Access of h, s, v values has changed since 2.79 release - # use rgb_to_hsv function to get the v component - blender_version = bool(bpy.app.version >= (2, 79, 1)) - from colorsys import rgb_to_hsv + id = len(obj.vertex_groups) + id_red = id + id_green = id + id_blue = id + id_value = id boolCol = len(obj.data.vertex_colors) - if boolCol: - col_name = obj.data.vertex_colors.active.name + if(boolCol): col_name = obj.data.vertex_colors.active.name bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') - if self.red and boolCol: + if(self.red and boolCol): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() - id_red = ids + id_red = id obj.vertex_groups[id_red].name = col_name + '_red' - ids += 1 - if self.green and boolCol: + id+=1 + if(self.green and boolCol): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() - id_green = ids + id_green = id obj.vertex_groups[id_green].name = col_name + '_green' - ids += 1 - if self.blue and boolCol: + id+=1 + if(self.blue and boolCol): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() - id_blue = ids + id_blue = id obj.vertex_groups[id_blue].name = col_name + '_blue' - ids += 1 - if self.value and boolCol: + id+=1 + if(self.value and boolCol): bpy.ops.object.vertex_group_add() bpy.ops.object.vertex_group_assign() - id_value = ids + id_value = id obj.vertex_groups[id_value].name = col_name + '_value' - ids += 1 - - mult = -1 if self.invert else 1 + id+=1 + mult = 1 + if(self.invert): mult = -1 bpy.ops.object.mode_set(mode='OBJECT') - sub_red = 1 + self.value + self.blue + self.green sub_green = 1 + self.value + self.blue sub_blue = 1 + self.value sub_value = 1 - ids = len(obj.vertex_groups) - if (id_red <= ids and id_green <= ids and id_blue <= ids and id_value <= - ids and boolCol): + id = len(obj.vertex_groups) + if(id_red <= id and id_green <= id and id_blue <= id and id_value <= \ + id and boolCol): v_colors = obj.data.vertex_colors.active.data i = 0 for f in obj.data.polygons: for v in f.vertices: gr = obj.data.vertices[v].groups - if self.red: - gr[min(len(gr) - sub_red, id_red)].weight = \ - self.invert + mult * v_colors[i].color[0] - - if self.green: - gr[min(len(gr) - sub_green, id_green)].weight = \ - self.invert + mult * v_colors[i].color[1] - - if self.blue: - gr[min(len(gr) - sub_blue, id_blue)].weight = \ - self.invert + mult * v_colors[i].color[2] - - if self.value: - col_value = v_colors[i].color.v if not blender_version else \ - rgb_to_hsv(v_colors[i].color[0], - v_colors[i].color[0], - v_colors[i].color[0])[2] - - gr[min(len(gr) - sub_value, id_value)].weight = \ - self.invert + mult * col_value - i += 1 + if(self.red): gr[min(len(gr)-sub_red, id_red)].weight = \ + self.invert + mult * v_colors[i].color[0] + if(self.green): gr[min(len(gr)-sub_green, id_green)].weight\ + = self.invert + mult * v_colors[i].color[1] + if(self.blue): gr[min(len(gr)-sub_blue, id_blue)].weight = \ + self.invert + mult * v_colors[i].color[2] + if(self.value): + r = v_colors[i].color[0] + g = v_colors[i].color[1] + b = v_colors[i].color[2] + gr[min(len(gr)-sub_value, id_value)].weight\ + = self.invert + mult * (0.2126*r + 0.7152*g + 0.0722*b) + i+=1 bpy.ops.paint.weight_paint_toggle() - return {'FINISHED'} - -class vertex_group_to_vertex_colors(Operator): +class vertex_group_to_vertex_colors(bpy.types.Operator): bl_idname = "object.vertex_group_to_vertex_colors" bl_label = "Vertex Group" - bl_description = ("Convert the active Vertex Group into a Vertex Color") bl_options = {'REGISTER', 'UNDO'} + bl_description = ("Convert the active Vertex Group into a Vertex Color.") + + channel : bpy.props.EnumProperty( + items=[('Blue', 'Blue Channel', 'Convert to Blue Channel'), + ('Green', 'Green Channel', 'Convert to Green Channel'), + ('Red', 'Red Channel', 'Convert to Red Channel'), + ('Value', 'Value Channel', 'Convert to Grayscale'), + ('False Colors', 'False Colors', 'Convert to False Colors')], + name="Convert to", description="Choose how to convert vertex group", + default="Value", options={'LIBRARY_EDITABLE'}) + + invert : bpy.props.BoolProperty( + name="invert", default=False, description="invert color channel") - channel: EnumProperty( - items=[('Blue', 'Blue Channel', 'Convert to Blue Channel'), - ('Green', 'Green Channel', 'Convert to Green Channel'), - ('Red', 'Red Channel', 'Convert to Red Channel'), - ('Value', 'Value Channel', 'Convert to Grayscale'), - ('False Colors', 'False Colors', 'Convert to False Colors')], - name="Convert to", - description="Choose how to convert vertex group", - default="Value", - options={'LIBRARY_EDITABLE'} - ) - - invert: BoolProperty( - name="Invert", - default=False, - description="Invert Color Channel" - ) + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 def execute(self, context): obj = bpy.context.active_object group_id = obj.vertex_groups.active_index - if (group_id == -1): return {'FINISHED'} @@ -215,314 +1841,665 @@ class vertex_group_to_vertex_colors(Operator): colors_id = obj.data.vertex_colors.active_index colors_name = group_name - if(self.channel == 'False Colors'): - colors_name += "_false_colors" - elif(self.channel == 'Value'): - colors_name += "_value" - elif(self.channel == 'Red'): - colors_name += "_red" - elif(self.channel == 'Green'): - colors_name += "_green" - elif(self.channel == 'Blue'): - colors_name += "_blue" + if(self.channel == 'False Colors'): colors_name += "_false_colors" + elif(self.channel == 'Value'): colors_name += "_value" + elif(self.channel == 'Red'): colors_name += "_red" + elif(self.channel == 'Green'): colors_name += "_green" + elif(self.channel == 'Blue'): colors_name += "_blue" bpy.context.object.data.vertex_colors[colors_id].name = colors_name v_colors = obj.data.vertex_colors.active.data mult = 1 - if self.invert: - mult = -1 + if(self.invert): mult = -1 i = 0 for f in obj.data.polygons: for v in f.vertices: gr = obj.data.vertices[v].groups - if(self.channel == 'False Colors'): - v_colors[i].color = (0, 0, 1) - else: - v_colors[i].color = (0, 0, 0) + if(self.channel == 'False Colors'): v_colors[i].color = (0,0,0.5,1) + else: v_colors[i].color = (0,0,0,1) for g in gr: if g.group == group_id: + w = g.weight if(self.channel == 'False Colors'): - if g.weight < 0.25: - v_colors[i].color = (0, g.weight * 4, 1) - elif g.weight < 0.5: - v_colors[i].color = (0, 1, 1 - (g.weight - 0.25) * 4) - elif g.weight < 0.75: - v_colors[i].color = ((g.weight - 0.5) * 4, 1, 0) + mult = 0.6+0.4*w + if w < 0.25: + v_colors[i].color = (0, w*4*mult, 1*mult,1) + elif w < 0.5: + v_colors[i].color = (0, 1*mult, (1-(w-0.25)*4)*mult,1) + elif w < 0.75: + v_colors[i].color = ((w-0.5)*4*mult,1*mult,0,1) else: - v_colors[i].color = (1, 1 - (g.weight - 0.75) * 4, 0) + v_colors[i].color = (1*mult,(1-(w-0.75)*4)*mult,0,1) elif(self.channel == 'Value'): v_colors[i].color = ( - self.invert + mult * g.weight, - self.invert + mult * g.weight, - self.invert + mult * g.weight) + self.invert + mult * w, + self.invert + mult * w, + self.invert + mult * w, + 1) elif(self.channel == 'Red'): v_colors[i].color = ( - self.invert + mult * g.weight, 0, 0) + self.invert + mult * w,0,0,1) elif(self.channel == 'Green'): v_colors[i].color = ( - 0, self.invert + mult * g.weight, 0) + 0, self.invert + mult * w,0,1) elif(self.channel == 'Blue'): v_colors[i].color = ( - 0, 0, self.invert + mult * g.weight) - i += 1 + 0,0, self.invert + mult * w,1) + i+=1 bpy.ops.paint.vertex_paint_toggle() bpy.context.object.data.vertex_colors[colors_id].active_render = True - return {'FINISHED'} - -class curvature_to_vertex_groups(Operator): +class curvature_to_vertex_groups(bpy.types.Operator): bl_idname = "object.curvature_to_vertex_groups" bl_label = "Curvature" - bl_description = ("Generate a Vertex Group based on the curvature of the" - "mesh. Is based on Dirty Vertex Color") bl_options = {'REGISTER', 'UNDO'} + bl_description = ("Generate a Vertex Group based on the curvature of the" + "mesh. Is based on Dirty Vertex Color.") - invert: BoolProperty( - name="Invert", - default=False, - description="Invert Values" - ) - blur_strength: FloatProperty( - name="Blur Strength", - default=1, - min=0.001, - max=1, - description="Blur strength per iteration" - ) - blur_iterations: IntProperty( - name="Blur Iterations", - default=1, - min=0, - max=40, - description="Number of times to blur the values" - ) - min_angle: FloatProperty( - name="Min Angle", - default=0, - min=0, - max=pi / 2, - subtype='ANGLE', - description="Minimum angle" - ) - max_angle: FloatProperty( - name="Max Angle", - default=pi, - min=pi / 2, - max=pi, - subtype='ANGLE', - description="Maximum angle" - ) - invert: BoolProperty( - name="Invert", - default=False, - description="Invert the curvature map" - ) + invert : bpy.props.BoolProperty( + name="invert", default=False, description="invert values") - def execute(self, context): - # Fix T53350: Access of r, g, b values has changed since 2.79.1 - blender_version = bool(bpy.app.version >= (2, 79, 1)) + blur_strength : bpy.props.FloatProperty( + name="Blur Strength", default=1, min=0.001, + max=1, description="Blur strength per iteration") + + blur_iterations : bpy.props.IntProperty( + name="Blur Iterations", default=1, min=0, + max=40, description="Number of times to blur the values") + + min_angle : bpy.props.FloatProperty( + name="Min Angle", default=0, min=0, + max=pi/2, subtype='ANGLE', description="Minimum angle") + + max_angle : bpy.props.FloatProperty( + name="Max Angle", default=pi, min=pi/2, + max=pi, subtype='ANGLE', description="Maximum angle") + invert : bpy.props.BoolProperty( + name="Invert", default=False, + description="Invert the curvature map") + + def execute(self, context): bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.mesh.vertex_color_add() vertex_colors = bpy.context.active_object.data.vertex_colors vertex_colors[-1].active = True vertex_colors[-1].active_render = True vertex_colors[-1].name = "Curvature" - - for c in vertex_colors[-1].data: - if blender_version: - # white color, alpha 1 - c.color[0], c.color[1], c.color[2], c.color[3] = 1, 1, 1, 1 - else: - c.color.r, c.color.g, c.color.b = 1, 1, 1 - + for c in vertex_colors[-1].data: c.color = (1,1,1,1) bpy.ops.object.mode_set(mode='VERTEX_PAINT') - bpy.ops.paint.vertex_color_dirt( - blur_strength=self.blur_strength, - blur_iterations=self.blur_iterations, - clean_angle=self.max_angle, - dirt_angle=self.min_angle - ) + blur_strength=self.blur_strength, + blur_iterations=self.blur_iterations, clean_angle=self.max_angle, + dirt_angle=self.min_angle) bpy.ops.object.vertex_colors_to_vertex_groups(invert=self.invert) bpy.ops.mesh.vertex_color_remove() - return {'FINISHED'} -class face_area_to_vertex_groups(Operator): +class face_area_to_vertex_groups(bpy.types.Operator): bl_idname = "object.face_area_to_vertex_groups" bl_label = "Area" - bl_description = ("Generate a Vertex Group based on the area of individual" - "faces") bl_options = {'REGISTER', 'UNDO'} + bl_description = ("Generate a Vertex Group based on the area of individual" + "faces.") + + invert : bpy.props.BoolProperty( + name="invert", default=False, description="invert values") + bounds : bpy.props.EnumProperty( + items=(('MANUAL', "Manual Bounds", ""), + ('AUTOMATIC', "Automatic Bounds", "")), + default='AUTOMATIC', name="Bounds") + + min_area : bpy.props.FloatProperty( + name="Min", default=0.01, soft_min=0, soft_max=1, + description="Faces with 0 weight") - invert: BoolProperty( - name="Invert", - default=False, - description="Invert Values" - ) + max_area : bpy.props.FloatProperty( + name="Max", default=0.1, soft_min=0, soft_max=1, + description="Faces with 1 weight") + + def draw(self, context): + layout = self.layout + layout.label(text="Bounds") + layout.prop(self, "bounds", text="") + if self.bounds == 'MANUAL': + layout.prop(self, "min_area") + layout.prop(self, "max_area") def execute(self, context): - obj = bpy.context.active_object - id_value = len(obj.vertex_groups) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.object.vertex_group_add() - bpy.ops.object.vertex_group_assign() - obj.vertex_groups[id_value].name = 'faces_area' - mult = 1 + try: ob = context.object + except: + self.report({'ERROR'}, "Please select an Object") + return {'CANCELLED'} + ob.vertex_groups.new(name="Faces Area") - if self.invert: - mult = -1 + areas = [[] for v in ob.data.vertices] - bpy.ops.object.mode_set(mode='OBJECT') - min_area = False - max_area = False - n_values = [0] * len(obj.data.vertices) - values = [0] * len(obj.data.vertices) - for p in obj.data.polygons: + for p in ob.data.polygons: for v in p.vertices: - n_values[v] += 1 - values[v] += p.area + areas[v].append(p.area) + + for i in range(len(areas)): + areas[i] = mean(areas[i]) + print(areas) + if self.bounds == 'MANUAL': + min_area = self.min_area + max_area = self.max_area + elif self.bounds == 'AUTOMATIC': + min_area = min(areas) + max_area = max(areas) + elif self.bounds == 'COMPRESSION': + min_area = 1 + max_area = min(areas) + elif self.bounds == 'TENSION': + min_area = 1 + max_area = max(areas) + print(min_area) + print(max_area) + delta_area = max_area - min_area + if delta_area == 0: + delta_area = 0.0001 + if self.bounds == 'MANUAL': + delta_area = 0.0001 + else: + self.report({'ERROR'}, "The faces have the same areas") + #return {'CANCELLED'} + for i in range(len(areas)): + weight = (areas[i] - min_area)/delta_area + ob.vertex_groups[-1].add([i], weight, 'REPLACE') + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + return {'FINISHED'} - if min_area: - if min_area > p.area: - min_area = p.area - else: - min_area = p.area - if max_area: - if max_area < p.area: - max_area = p.area - else: - max_area = p.area +class harmonic_weight(bpy.types.Operator): + bl_idname = "object.harmonic_weight" + bl_label = "Harmonic" + bl_options = {'REGISTER', 'UNDO'} + bl_description = ("Create an harmonic variation of the active Vertex Group") - for v in obj.data.vertices: - gr = v.groups - index = v.index - try: - gr[min(len(gr) - 1, id_value)].weight = \ - self.invert + mult * \ - ((values[index] / n_values[index] - min_area) / - (max_area - min_area)) - except: - gr[min(len(gr) - 1, id_value)].weight = 0.5 + freq : bpy.props.FloatProperty( + name="Frequency", default=20, soft_min=0, + soft_max=100, description="Wave frequency") + + amp : bpy.props.FloatProperty( + name="Amplitude", default=1, soft_min=0, + soft_max=10, description="Wave amplitude") - bpy.ops.paint.weight_paint_toggle() + midlevel : bpy.props.FloatProperty( + name="Midlevel", default=0, min=-1, + max=1, description="Midlevel") + add : bpy.props.FloatProperty( + name="Add", default=0, min=-1, + max=1, description="Add to the Weight") + + mult : bpy.props.FloatProperty( + name="Multiply", default=0, min=0, + max=1, description="Multiply for he Weight") + + @classmethod + def poll(cls, context): + return len(context.object.vertex_groups) > 0 + + def execute(self, context): + ob = bpy.context.active_object + if len(ob.vertex_groups) > 0: + group_id = ob.vertex_groups.active_index + ob.vertex_groups.new(name="Harmonic") + for i in range(len(ob.data.vertices)): + try: val = ob.vertex_groups[group_id].weight(i) + except: val = 0 + weight = self.amp*(sin(val*self.freq) - self.midlevel)/2 + 0.5 + self.add*val*(1-(1-val)*self.mult) + ob.vertex_groups[-1].add([i], weight, 'REPLACE') + ob.data.update() + else: + self.report({'ERROR'}, "Active object doesn't have vertex groups") + return {'CANCELLED'} + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') return {'FINISHED'} -class harmonic_weight(Operator): - bl_idname = "object.harmonic_weight" - bl_label = "Harmonic" - bl_description = "Create an harmonic variation of the active Vertex Group" + +class TISSUE_PT_color(bpy.types.Panel): + bl_label = "Tissue Tools" + bl_category = "Tissue" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + #bl_options = {'DEFAULT_CLOSED'} + bl_context = "vertexpaint" + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.operator("object.vertex_colors_to_vertex_groups", + icon="GROUP_VERTEX", text="Convert to Weight") + +class TISSUE_PT_weight(bpy.types.Panel): + bl_label = "Tissue Tools" + bl_category = "Tissue" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + #bl_options = {'DEFAULT_CLOSED'} + bl_context = "weightpaint" + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + #if context.object.type == 'MESH' and context.mode == 'OBJECT': + #col.label(text="Transform:") + #col.separator() + #elif bpy.context.mode == 'PAINT_WEIGHT': + col.label(text="Weight Generate:") + #col.operator( + # "object.vertex_colors_to_vertex_groups", icon="GROUP_VCOL") + col.operator("object.face_area_to_vertex_groups", icon="FACESEL") + col.operator("object.curvature_to_vertex_groups", icon="SMOOTHCURVE") + try: col.operator("object.weight_formula", icon="CON_TRANSFORM") + except: col.operator("object.weight_formula")#, icon="CON_TRANSFORM") + #col.label(text="Weight Processing:") + col.separator() + + # TO BE FIXED + #col.operator("object.weight_laplacian", icon="SMOOTHCURVE") + + col.operator("object.harmonic_weight", icon="IPO_ELASTIC") + col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VCOL", + text="Convert to Colors") + col.separator() + col.label(text="Deformation Analysis:") + col.operator("object.edges_deformation", icon="DRIVER_DISTANCE")#FULLSCREEN_ENTER") + col.operator("object.edges_bending", icon="DRIVER_ROTATIONAL_DIFFERENCE")#"MOD_SIMPLEDEFORM") + col.separator() + col.label(text="Weight Contour:") + col.operator("object.weight_contour_curves", icon="MOD_CURVE") + col.operator("object.weight_contour_displace", icon="MOD_DISPLACE") + col.operator("object.weight_contour_mask", icon="MOD_MASK") + col.separator() + col.label(text="Simulations:") + #col.operator("object.reaction_diffusion", icon="MOD_OCEAN") + col.operator("object.start_reaction_diffusion", + icon="EXPERIMENTAL", + text="Reaction-Diffusion") + + #col.prop(context.object, "reaction_diffusion_run", icon="PLAY", text="Run Simulation") + ####col.prop(context.object, "reaction_diffusion_run") + #col.separator() + #col.label(text="Vertex Color from:") + #col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VERTEX") + + + + +class start_reaction_diffusion(bpy.types.Operator): + bl_idname = "object.start_reaction_diffusion" + bl_label = "Start Reaction Diffusion" + bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B") bl_options = {'REGISTER', 'UNDO'} - freq: FloatProperty( - name="Frequency", - default=20, - soft_min=0, - soft_max=100, - description="Wave frequency" - ) - amp: FloatProperty( - name="Amplitude", - default=1, - soft_min=0, - soft_max=10, - description="Wave amplitude" - ) - midlevel: FloatProperty( - name="Midlevel", - default=0, - min=-1, - max=1, - description="Midlevel" - ) - add: FloatProperty( - name="Add", - default=0, - min=-1, - max=1, - description="Add to the Weight" - ) - mult: FloatProperty( - name="Multiply", - default=0, - min=0, - max=1, - description="Multiply for he Weight" - ) + time_steps : bpy.props.IntProperty( + name="Steps", default=10, min=0, soft_max=50, + description="Number of Steps") + + dt : bpy.props.FloatProperty( + name="dt", default=1, min=0, soft_max=0.2, + description="Time Step") + + diff_a : bpy.props.FloatProperty( + name="Diff A", default=0.18, min=0, soft_max=2, + description="Diffusion A") + + diff_b : bpy.props.FloatProperty( + name="Diff B", default=0.09, min=0, soft_max=2, + description="Diffusion B") + + f : bpy.props.FloatProperty( + name="f", default=0.055, min=0, soft_max=0.5, precision=4, + description="Feed Rate") + + k : bpy.props.FloatProperty( + name="k", default=0.062, min=0, soft_max=0.5, precision=4, + description="Kill Rate") + + @classmethod + def poll(cls, context): + return context.object.type == 'MESH' def execute(self, context): - obj = bpy.context.active_object + reaction_diffusion_add_handler(self, context) + + ob = context.object + ''' + ob.reaction_diffusion_settings.dt = self.dt + ob.reaction_diffusion_settings.time_steps = self.time_steps + ob.reaction_diffusion_settings.f = self.f + ob.reaction_diffusion_settings.k = self.k + ob.reaction_diffusion_settings.diff_a = self.diff_a + ob.reaction_diffusion_settings.diff_b = self.diff_b + ''' + + # check vertex group A try: - group_id = obj.vertex_groups.active_index - for v in obj.data.vertices: - val = v.groups[group_id].weight - v.groups[group_id].weight = (self.amp * (sin(val * self.freq) - - self.midlevel) / 2 + 0.5 + self.add * val) * \ - (1 - (1 - val) * self.mult) + vg = ob.vertex_groups['A'] except: - self.report({'ERROR'}, "Active object doesn't have vertex groups") + ob.vertex_groups.new(name='A') + # check vertex group B + try: + vg = ob.vertex_groups['B'] + except: + ob.vertex_groups.new(name='B') - return {'CANCELLED'} + for v in ob.data.vertices: + ob.vertex_groups['A'].add([v.index], 1, 'REPLACE') + ob.vertex_groups['B'].add([v.index], 0, 'REPLACE') + ob.vertex_groups.update() + ob.data.update() bpy.ops.object.mode_set(mode='WEIGHT_PAINT') return {'FINISHED'} +class reset_reaction_diffusion_weight(bpy.types.Operator): + bl_idname = "object.reset_reaction_diffusion_weight" + bl_label = "Reset Reaction Diffusion Weight" + bl_description = ("Set A and B weight to default values") + bl_options = {'REGISTER', 'UNDO'} -class colors_groups_exchanger_panel(bpy.types.Panel): - bl_label = "Tissue Tools" - bl_category = "Tools" - bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" - bl_options = {'DEFAULT_CLOSED'} + @classmethod + def poll(cls, context): + return context.object.type == 'MESH' - def draw(self, context): + def execute(self, context): + reaction_diffusion_add_handler(self, context) + + ob = context.object + + # check vertex group A + try: + vg = ob.vertex_groups['A'] + except: + ob.vertex_groups.new(name='A') + # check vertex group B try: - if bpy.context.active_object.type == 'MESH': - layout = self.layout - col = layout.column(align=True) - col.label(text="Transform:") - col.operator("object.dual_mesh") - col.separator() - col.label(text="Weight from:") - col.operator( - "object.vertex_colors_to_vertex_groups", icon="GROUP_VCOL") - col.operator("object.face_area_to_vertex_groups", icon="SNAP_FACE") - col.operator("object.curvature_to_vertex_groups", icon="SMOOTHCURVE") - col.operator("object.harmonic_weight", icon="IPO_ELASTIC") - col.separator() - col.label(text="Vertex Color from:") - col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VERTEX") + vg = ob.vertex_groups['B'] except: - pass + ob.vertex_groups.new(name='B') + for v in ob.data.vertices: + ob.vertex_groups['A'].add([v.index], 1, 'REPLACE') + ob.vertex_groups['B'].add([v.index], 0, 'REPLACE') -def register(): - bpy.utils.register_class(vertex_colors_to_vertex_groups) - bpy.utils.register_class(vertex_group_to_vertex_colors) - bpy.utils.register_class(face_area_to_vertex_groups) - bpy.utils.register_class(colors_groups_exchanger_panel) - bpy.utils.register_class(harmonic_weight) + ob.vertex_groups.update() + ob.data.update() + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + + return {'FINISHED'} + +from bpy.app.handlers import persistent + +@persistent +def reaction_diffusion_def_blur(scene): + for ob in scene.objects: + if ob.reaction_diffusion_settings.run: + #try: + me = ob.data + bm = bmesh.new() + bm.from_mesh(me) + bm.edges.ensure_lookup_table() + + # store weight values + a = [] + b = [] + for v in me.vertices: + try: + a.append(ob.vertex_groups["A"].weight(v.index)) + except: + a.append(0) + try: + b.append(ob.vertex_groups["B"].weight(v.index)) + except: + b.append(0) + + a = array(a) + b = array(b) + props = ob.reaction_diffusion_settings + dt = props.dt + time_steps = props.time_steps + f = props.f + k = props.k + diff_a = props.diff_a * props.diff_mult + diff_b = props.diff_b * props.diff_mult + + n_verts = len(bm.verts) + #bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + #ob.data.use_paint_mask_vertex = True + + for i in range(time_steps): + ab2 = a*b**2 + ob.vertex_groups.active = ob.vertex_groups['A'] + bpy.ops.object.vertex_group_smooth(group_select_mode='ACTIVE', factor=diff_a) + ob.vertex_groups.active = ob.vertex_groups['B'] + bpy.ops.object.vertex_group_smooth(group_select_mode='ACTIVE', factor=diff_b) + + a = [] + b = [] + for v in me.vertices: + a.append(ob.vertex_groups["A"].weight(v.index)) + b.append(ob.vertex_groups["B"].weight(v.index)) + a = array(a) + b = array(b) + + a += - (ab2 + f*(1-a))*dt + b += (ab2 - (k+f)*b)*dt + + a = nan_to_num(a) + b = nan_to_num(b) + + for i in range(n_verts): + ob.vertex_groups['A'].add([i], a[i], 'REPLACE') + ob.vertex_groups['B'].add([i], b[i], 'REPLACE') + ob.vertex_groups.update() + ob.data.update() + #bpy.ops.object.mode_set(mode='EDIT') + #bpy.ops.object.mode_set(mode='WEIGHT_PAINT + #bpy.ops.paint.weight_paint_toggle() + #bpy.ops.paint.weight_paint_toggle() + + #bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + #except: + # pass + +def reaction_diffusion_def_(scene): + for ob in scene.objects: + if ob.reaction_diffusion_settings.run: + #try: + me = ob.data + bm = bmesh.new() + bm.from_mesh(me) + bm.edges.ensure_lookup_table() + + # store weight values + a = [] + b = [] + for v in me.vertices: + try: + a.append(ob.vertex_groups["A"].weight(v.index)) + except: + a.append(0) + try: + b.append(ob.vertex_groups["B"].weight(v.index)) + except: + b.append(0) + + a = array(a) + b = array(b) + props = ob.reaction_diffusion_settings + dt = props.dt + time_steps = props.time_steps + f = props.f + k = props.k + diff_a = props.diff_a * props.diff_mult + diff_b = props.diff_b * props.diff_mult + + n_verts = len(bm.verts) + for i in range(time_steps): + lap_a = zeros((n_verts))#[0]*n_verts + lap_b = zeros((n_verts))#[0]*n_verts + #print(len(lap)) + #print(len(weight)) + if i == 0: + lap_map = [[] for i in range(n_verts)] + lap_mult = [] + for e in bm.edges: + id0 = e.verts[0].index + id1 = e.verts[1].index + lap_map[id0].append(id1) + lap_map[id1].append(id0) + for id in range(n_verts): + lap_mult.append(len(lap_map[id])) + lap_mult = array(lap_mult) + lap_map = array(lap_map) + for id in range(n_verts): + map = lap_map[id] + lap_a[id] = a[lap_map[id]].sum() + lap_b[id] = b[lap_map[id]].sum() + lap_a -= a*lap_mult + lap_b -= b*lap_mult + ab2 = a*b**2 + + a += (diff_a*lap_a - ab2 + f*(1-a))*dt + b += (diff_b*lap_b + ab2 - (k+f)*b)*dt + + a = nan_to_num(a) + b = nan_to_num(b) + + for i in range(n_verts): + ob.vertex_groups['A'].add([i], a[i], 'REPLACE') + ob.vertex_groups['B'].add([i], b[i], 'REPLACE') + ob.vertex_groups.update() + ob.data.update() + #bpy.ops.object.mode_set(mode='EDIT') + #bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + bpy.ops.paint.weight_paint_toggle() + bpy.ops.paint.weight_paint_toggle() + #bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + #except: + # pass + +def reaction_diffusion_def(scene): + for ob in scene.objects: + if ob.reaction_diffusion_settings.run: + me = ob.data + n_edges = len(me.edges) + n_verts = len(me.vertices) + + # store weight values + a = np.zeros(n_verts) + b = np.zeros(n_verts) + for i in range(n_verts): + try: a[i] = ob.vertex_groups["A"].weight(i) + except: pass + try: b[i] = ob.vertex_groups["B"].weight(i) + except: pass + + props = ob.reaction_diffusion_settings + dt = props.dt + time_steps = props.time_steps + f = props.f + k = props.k + diff_a = props.diff_a * props.diff_mult + diff_b = props.diff_b * props.diff_mult + + edge_verts = [0]*n_edges*2 + me.edges.foreach_get("vertices", edge_verts) + + edge_verts = np.array(edge_verts) + arr = np.arange(n_edges)*2 + id0 = edge_verts[arr] # first vertex indices for each edge + id1 = edge_verts[arr+1] # second vertex indices for each edge -def unregister(): - bpy.utils.unregister_class(vertex_colors_to_vertex_groups) - bpy.utils.unregister_class(vertex_group_to_vertex_colors) - bpy.utils.unregister_class(face_area_to_vertex_groups) - bpy.utils.unregister_class(colors_groups_exchanger_panel) - bpy.utils.unregister_class(harmonic_weight) + try: + a, b = numba_reaction_diffusion(n_verts, a, b, diff_a, diff_b, f, k, dt, id0, id1, time_steps) + a = nan_to_num(a) + b = nan_to_num(b) + except: + for i in range(time_steps): + lap_a = np.zeros(n_verts) + lap_b = np.zeros(n_verts) + lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge + lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge + + for i, j, la0, lb0 in np.nditer([id0,id1,lap_a0,lap_b0]): + lap_a[i] += la0 + lap_b[i] += lb0 + lap_a[j] -= la0 + lap_b[j] -= lb0 + ab2 = a*b**2 + a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt") + b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt") + #a += (diff_a*lap_a - ab2 + f*(1-a))*dt + #b += (diff_b*lap_b + ab2 - (k+f)*b)*dt + + a = nan_to_num(a) + b = nan_to_num(b) + + for i in range(n_verts): + ob.vertex_groups['A'].add([i], a[i], 'REPLACE') + ob.vertex_groups['B'].add([i], b[i], 'REPLACE') + + for ps in ob.particle_systems: + if ps.vertex_group_density == 'B' or ps.vertex_group_density == 'A': + ps.invert_vertex_group_density = not ps.invert_vertex_group_density + ps.invert_vertex_group_density = not ps.invert_vertex_group_density + +class TISSUE_PT_reaction_diffusion(Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" + bl_label = "Tissue - Reaction-Diffusion" + bl_options = {'DEFAULT_CLOSED'} + @classmethod + def poll(cls, context): + return 'A' and 'B' in context.object.vertex_groups -if __name__ == "__main__": - register() + def draw(self, context): + reaction_diffusion_add_handler(self, context) + + ob = context.object + props = ob.reaction_diffusion_settings + layout = self.layout + col = layout.column(align=True) + row = col.row(align=True) + if not ("A" and "B" in ob.vertex_groups): + row.operator("object.start_reaction_diffusion", + icon="EXPERIMENTAL", + text="Reaction-Diffusion") + else: + row.operator("object.start_reaction_diffusion", + icon="EXPERIMENTAL", + text="Reset Reaction-Diffusion") + row = col.row(align=True) + row.prop(props, "run", text="Run Reaction-Diffusion") + col = layout.column(align=True) + row = col.row(align=True) + row.prop(props, "time_steps") + row.prop(props, "dt") + col.separator() + row = col.row(align=True) + row.prop(props, "diff_a") + row.prop(props, "diff_b") + row = col.row(align=True) + row.prop(props, "diff_mult") + #col.separator() + row = col.row(align=True) + row.prop(props, "f") + row.prop(props, "k") diff --git a/mesh_tissue/dual_mesh.py b/mesh_tissue/dual_mesh.py index 96f3b59b..eefd4c07 100644 --- a/mesh_tissue/dual_mesh.py +++ b/mesh_tissue/dual_mesh.py @@ -32,8 +32,8 @@ bl_info = { "name": "Dual Mesh", "author": "Alessandro Zomparelli (Co-de-iT)", - "version": (0, 3), - "blender": (2, 78, 0), + "version": (0, 4), + "blender": (2, 8, 0), "location": "", "description": "Convert a generic mesh to its dual", "warning": "", @@ -47,15 +47,105 @@ from bpy.props import ( EnumProperty, ) import bmesh +from .utils import * +class dual_mesh_tessellated(Operator): + bl_idname = "object.dual_mesh_tessellated" + bl_label = "Dual Mesh" + bl_description = ("Generate a polygonal mesh using Tessellate. (Non-destructive)") + bl_options = {'REGISTER', 'UNDO'} + + apply_modifiers : BoolProperty( + name="Apply Modifiers", + default=True, + description="Apply object's modifiers" + ) + + source_faces : EnumProperty( + items=[ + ('QUAD', 'Quad Faces', ''), + ('TRI', 'Triangles', '')], + name="Source Faces", + description="Source polygons", + default="QUAD", + options={'LIBRARY_EDITABLE'} + ) + + def execute(self, context): + auto_layer_collection() + ob0 = context.object + name1 = "DualMesh_{}_Component".format(self.source_faces) + # Generate component + if self.source_faces == 'QUAD': + verts = [(0.0, 0.0, 0.0), (0.0, 0.5, 0.0), + (0.0, 1.0, 0.0), (0.5, 1.0, 0.0), + (1.0, 1.0, 0.0), (1.0, 0.5, 0.0), + (1.0, 0.0, 0.0), (0.5, 0.0, 0.0), + (1/3, 1/3, 0.0), (2/3, 2/3, 0.0)] + edges = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,6), (6,7), + (7,0), (1,8), (8,7), (3,9), (9,5), (8,9)] + faces = [(7,8,1,0), (8,9,3,2,1), (9,5,4,3), (9,8,7,6,5)] + else: + verts = [(0.0,0.0,0.0), (0.5,0.0,0.0), (1.0,0.0,0.0), (0.0,1.0,0.0), (0.5,1.0,0.0), (1.0,1.0,0.0)] + edges = [(0,1), (1,2), (2,5), (5,4), (4,3), (3,0), (1,4)] + faces = [(0,1,4,3), (1,2,5,4)] + + # check pre-existing component + try: + _verts = [0]*len(verts)*3 + __verts = [c for co in verts for c in co] + ob1 = bpy.data.objects[name1] + ob1.data.vertices.foreach_get("co",_verts) + for a, b in zip(_verts, __verts): + if abs(a-b) > 0.0001: + raise ValueError + except: + me = bpy.data.meshes.new("Dual-Mesh") # add a new mesh + me.from_pydata(verts, edges, faces) + me.update(calc_edges=True, calc_edges_loose=True, calc_loop_triangles=True) + if self.source_faces == 'QUAD': n_seams = 8 + else: n_seams = 6 + for i in range(n_seams): me.edges[i].use_seam = True + ob1 = bpy.data.objects.new(name1, me) + bpy.context.collection.objects.link(ob1) + # fix visualization issue + bpy.context.view_layer.objects.active = ob1 + ob1.select_set(True) + bpy.ops.object.editmode_toggle() + bpy.ops.object.editmode_toggle() + ob1.select_set(False) + # hide component + ob1.hide_select = True + ob1.hide_render = True + ob1.hide_viewport = True + ob = convert_object_to_mesh(ob0,False,False) + ob.name = 'DualMesh' + #ob = bpy.data.objects.new("DualMesh", convert_object_to_mesh(ob0,False,False)) + #bpy.context.collection.objects.link(ob) + #bpy.context.view_layer.objects.active = ob + #ob.select_set(True) + ob.tissue_tessellate.component = ob1 + ob.tissue_tessellate.generator = ob0 + ob.tissue_tessellate.gen_modifiers = self.apply_modifiers + ob.tissue_tessellate.merge = True + ob.tissue_tessellate.bool_dissolve_seams = True + if self.source_faces == 'TRI': ob.tissue_tessellate.fill_mode = 'FAN' + print(ob1) + print(ob0) + print(bpy.context.object) + bpy.ops.object.update_tessellate() + ob.location = ob0.location + ob.matrix_world = ob0.matrix_world + return {'FINISHED'} + class dual_mesh(Operator): bl_idname = "object.dual_mesh" - bl_label = "Dual Mesh" - bl_description = ("Convert a generic mesh into a polygonal mesh") + bl_label = "Convert to Dual Mesh" + bl_description = ("Convert a generic mesh into a polygonal mesh. (Destructive)") bl_options = {'REGISTER', 'UNDO'} - quad_method: EnumProperty( + quad_method : EnumProperty( items=[('BEAUTY', 'Beauty', 'Split the quads in nice triangles, slower method'), ('FIXED', 'Fixed', @@ -70,7 +160,7 @@ class dual_mesh(Operator): default="FIXED", options={'LIBRARY_EDITABLE'} ) - polygon_method: EnumProperty( + polygon_method : EnumProperty( items=[ ('BEAUTY', 'Beauty', 'Arrange the new triangles evenly'), ('CLIP', 'Clip', @@ -80,12 +170,12 @@ class dual_mesh(Operator): default="BEAUTY", options={'LIBRARY_EDITABLE'} ) - preserve_borders: BoolProperty( + preserve_borders : BoolProperty( name="Preserve Borders", default=True, description="Preserve original borders" ) - apply_modifiers: BoolProperty( + apply_modifiers : BoolProperty( name="Apply Modifiers", default=True, description="Apply object's modifiers" @@ -103,16 +193,6 @@ class dual_mesh(Operator): sel = bpy.context.selected_objects doneMeshes = [] - ''' - if self.new_object: - bpy.ops.object.duplicate_move() - for i in range(len(sel)): - bpy.context.selected_objects[i].name = sel[i].name + "_dual" - if sel[i] == act: - bpy.context.scene.objects.active = bpy.context.selected_objects[i] - sel = bpy.context.selected_objects - ''' - for ob0 in sel: if ob0.type != 'MESH': continue @@ -153,16 +233,7 @@ class dual_mesh(Operator): ) bpy.ops.mesh.extrude_region_move( MESH_OT_extrude_region={"mirror": False}, - TRANSFORM_OT_translate={"value": (0, 0, 0), - "constraint_axis": (False, False, False), - "orient_type": 'GLOBAL', "mirror": False, - "proportional": 'DISABLED', - "proportional_edit_falloff": 'SMOOTH', "proportional_size": 1, - "snap": False, "snap_target": 'CLOSEST', - "snap_point": (0, 0, 0), "snap_align": False, - "snap_normal": (0, 0, 0), "gpencil_strokes": False, - "texture_space": False, "remove_on_cancel": False, - "release_confirm": False} + TRANSFORM_OT_translate={"value": (0, 0, 0)} ) bpy.ops.mesh.select_mode( @@ -185,19 +256,21 @@ class dual_mesh(Operator): bpy.ops.object.modifier_apply( apply_as='DATA', modifier='dual_mesh_subsurf' ) + bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') verts = ob.data.vertices bpy.ops.object.mode_set(mode='OBJECT') - verts[0].select = True + verts[-1].select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_more(use_face_step=False) bpy.ops.mesh.select_similar( type='EDGE', compare='EQUAL', threshold=0.01) bpy.ops.mesh.select_all(action='INVERT') + bpy.ops.mesh.dissolve_verts() bpy.ops.mesh.select_all(action='DESELECT') @@ -244,7 +317,7 @@ class dual_mesh(Operator): bm = bmesh.from_edit_mesh(ob.data) for v in bm.verts: if len(v.link_edges) == 2 and len(v.link_faces) < 3: - v.select_set(True) + v.select = True # dissolve bpy.ops.mesh.dissolve_verts() @@ -280,36 +353,3 @@ class dual_mesh(Operator): bpy.ops.object.mode_set(mode=mode) return {'FINISHED'} - - -""" -class dual_mesh_panel(bpy.types.Panel): - bl_label = "Dual Mesh" - bl_category = "Tools" - bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" - bl_context = "objectmode" - - def draw(self, context): - layout = self.layout - col = layout.column(align=True) - try: - if bpy.context.active_object.type == 'MESH': - col.operator("object.dual_mesh") - except: - pass -""" - - -def register(): - bpy.utils.register_class(dual_mesh) - # bpy.utils.register_class(dual_mesh_panel) - - -def unregister(): - bpy.utils.unregister_class(dual_mesh) - # bpy.utils.unregister_class(dual_mesh_panel) - - -if __name__ == "__main__": - register() diff --git a/mesh_tissue/lattice.py b/mesh_tissue/lattice.py index 2850c77a..fade7837 100644 --- a/mesh_tissue/lattice.py +++ b/mesh_tissue/lattice.py @@ -31,7 +31,7 @@ bl_info = { "name": "Lattice", "author": "Alessandro Zomparelli (Co-de-iT)", "version": (0, 3), - "blender": (2, 78, 0), + "blender": (2, 7, 8), "location": "", "description": "Generate a Lattice based on a grid mesh", "warning": "", @@ -42,12 +42,11 @@ bl_info = { import bpy import bmesh from bpy.types import Operator -from bpy.props import ( - BoolProperty, - FloatProperty, - ) +from bpy.props import (BoolProperty, StringProperty, FloatProperty) from mathutils import Vector +from .utils import * + def not_in(element, grid): output = True @@ -173,86 +172,93 @@ class lattice_along_surface(Operator): "Lattice's topology") bl_options = {'REGISTER', 'UNDO'} - set_parent: BoolProperty( + set_parent : BoolProperty( name="Set Parent", default=True, description="Automatically set the Lattice as parent" ) - flipNormals: BoolProperty( + flipNormals : BoolProperty( name="Flip Normals", default=False, description="Flip normals direction" ) - swapUV: BoolProperty( + swapUV : BoolProperty( name="Swap UV", default=False, description="Flip grid's U and V" ) - flipU: BoolProperty( + flipU : BoolProperty( name="Flip U", default=False, description="Flip grid's U") - flipV: BoolProperty( + flipV : BoolProperty( name="Flip V", default=False, description="Flip grid's V" ) - flipW: BoolProperty( + flipW : BoolProperty( name="Flip W", default=False, description="Flip grid's W" ) - use_groups: BoolProperty( + use_groups : BoolProperty( name="Vertex Group", default=False, description="Use active Vertex Group for lattice's thickness" ) - high_quality_lattice: BoolProperty( + high_quality_lattice : BoolProperty( name="High quality", default=True, description="Increase the the subdivisions in normal direction for a " "more correct result" ) - hide_lattice: BoolProperty( + hide_lattice : BoolProperty( name="Hide Lattice", default=True, description="Automatically hide the Lattice object" ) - scale_x: FloatProperty( + scale_x : FloatProperty( name="Scale X", default=1, min=0.001, max=1, description="Object scale" ) - scale_y: FloatProperty( + scale_y : FloatProperty( name="Scale Y", default=1, min=0.001, max=1, description="Object scale" ) - scale_z: FloatProperty( + scale_z : FloatProperty( name="Scale Z", default=1, min=0.001, max=1, description="Object scale" ) - thickness: FloatProperty( + thickness : FloatProperty( name="Thickness", default=1, soft_min=0, soft_max=5, description="Lattice thickness" ) - displace: FloatProperty( + displace : FloatProperty( name="Displace", default=0, soft_min=-1, soft_max=1, description="Lattice displace" ) + grid_object = "" + source_object = "" + + @classmethod + def poll(cls, context): + try: return bpy.context.object.mode == 'OBJECT' + except: return False def draw(self, context): layout = self.layout @@ -282,12 +288,6 @@ class lattice_along_surface(Operator): slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1 ) - """ - col.prop( - self, "scale_z", text="W", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - """ col.separator() col.label(text="Flip:") row = col.row() @@ -303,39 +303,48 @@ class lattice_along_surface(Operator): col.prop(self, "set_parent") def execute(self, context): - if len(bpy.context.selected_objects) != 2: - self.report({'ERROR'}, "Please, select two objects") - return {'CANCELLED'} - depsgraph = context.evaluated_depsgraph_get() - grid_obj = bpy.context.active_object - if grid_obj.type not in ('MESH', 'CURVE', 'SURFACE'): - self.report({'ERROR'}, "The surface object is not valid. Only Mesh," - "Curve and Surface objects are allowed.") - return {'CANCELLED'} - obj = None - for o in bpy.context.selected_objects: - if o.name != grid_obj.name and o.type in \ - ('MESH', 'CURVE', 'SURFACE', 'FONT'): - obj = o - o.select_set(False) - break - obj_eval = obj.evaluated_get(depsgraph) - try: - obj_dim = obj.dimensions - obj_me = obj_eval.to_mesh() - except: - self.report({'ERROR'}, "The object to deform is not valid. Only " - "Mesh, Curve, Surface and Font objects are allowed.") - return {'CANCELLED'} + if self.source_object == self.grid_object == "" or True: + if len(bpy.context.selected_objects) != 2: + self.report({'ERROR'}, "Please, select two objects") + return {'CANCELLED'} + grid_obj = bpy.context.object + if grid_obj.type not in ('MESH', 'CURVE', 'SURFACE'): + self.report({'ERROR'}, "The surface object is not valid. Only Mesh," + "Curve and Surface objects are allowed.") + return {'CANCELLED'} + obj = None + for o in bpy.context.selected_objects: + if o.name != grid_obj.name and o.type in \ + ('MESH', 'CURVE', 'SURFACE', 'FONT'): + obj = o + o.select_set(False) + break + try: + obj_dim = obj.dimensions + obj_me = simple_to_mesh(obj)#obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True) + except: + self.report({'ERROR'}, "The object to deform is not valid. Only " + "Mesh, Curve, Surface and Font objects are allowed.") + return {'CANCELLED'} + self.grid_object = grid_obj.name + self.source_object = obj.name + else: + grid_obj = bpy.data.objects[self.grid_object] + obj = bpy.data.objects[self.source_object] + obj_me = simple_to_mesh(obj)# obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True) + for o in bpy.context.selected_objects: o.select_set(False) + grid_obj.select_set(True) + bpy.context.view_layer.objects.active = grid_obj - bpy.ops.object.duplicate_move() - grid_obj = bpy.context.active_object - bpy.ops.object.convert(target='MESH') - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - grid_mesh = grid_obj.evaluated_get(depsgraph).to_mesh() + temp_grid_obj = grid_obj.copy() + temp_grid_obj.data = simple_to_mesh(grid_obj) + grid_mesh = temp_grid_obj.data + for v in grid_mesh.vertices: + v.co = grid_obj.matrix_world @ v.co + grid_mesh.calc_normals() if len(grid_mesh.polygons) > 64 * 64: - bpy.ops.object.delete(use_global=False) + bpy.data.objects.remove(temp_grid_obj) bpy.context.view_layer.objects.active = obj obj.select_set(True) self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64") @@ -346,7 +355,8 @@ class lattice_along_surface(Operator): max = Vector((0, 0, 0)) first = True for v in obj_me.vertices: - vert = obj.matrix_world * v.co + v0 = v.co.copy() + vert = obj.matrix_world @ v0 if vert[0] < min[0] or first: min[0] = vert[0] if vert[1] < min[1] or first: @@ -361,12 +371,10 @@ class lattice_along_surface(Operator): max[2] = vert[2] first = False - obj_eval.to_mesh_clear() - bb = max - min + print(bb) lattice_loc = (max + min) / 2 - bpy.ops.object.add(type='LATTICE', align='WORLD', - enter_editmode=False) + bpy.ops.object.add(type='LATTICE') lattice = bpy.context.active_object lattice.location = lattice_loc lattice.scale = Vector((bb.x / self.scale_x, bb.y / self.scale_y, @@ -409,10 +417,10 @@ class lattice_along_surface(Operator): for w in range(nw): if self.use_groups: try: - displace = grid_obj.vertex_groups.active.weight( + displace = temp_grid_obj.vertex_groups.active.weight( verts_grid[i][j]) * scale_normal * bb.z except: - displace = scale_normal * bb.z + displace = 0#scale_normal * bb.z else: displace = scale_normal * bb.z target_point = (grid_mesh.vertices[verts_grid[i][j]].co + @@ -424,15 +432,17 @@ class lattice_along_surface(Operator): i = nu - i - 1 if self.flipV: j = nv - j - 1 + lattice.data.points[i + j * nu + w * nu * nv].co_deform.x = \ target_point.x / bpy.data.objects[lattice.name].scale.x lattice.data.points[i + j * nu + w * nu * nv].co_deform.y = \ target_point.y / bpy.data.objects[lattice.name].scale.y lattice.data.points[i + j * nu + w * nu * nv].co_deform.z = \ target_point.z / bpy.data.objects[lattice.name].scale.z + except: bpy.ops.object.mode_set(mode='OBJECT') - grid_obj.select_set(True) + temp_grid_obj.select_set(True) lattice.select_set(True) obj.select_set(False) bpy.ops.object.delete(use_global=False) @@ -446,15 +456,11 @@ class lattice_along_surface(Operator): self.report({'ERROR'}, "The grid mesh is not correct") return {'CANCELLED'} - # grid_obj.data = old_grid_data - # print(old_grid_matrix) - # grid_obj.matrix_world = old_grid_matrix - bpy.ops.object.mode_set(mode='OBJECT') - grid_obj.select_set(True) - lattice.select_set(False) + #grid_obj.select_set(True) + #lattice.select_set(False) obj.select_set(False) - bpy.ops.object.delete(use_global=False) + #bpy.ops.object.delete(use_global=False) bpy.context.view_layer.objects.active = lattice lattice.select_set(True) @@ -478,38 +484,7 @@ class lattice_along_surface(Operator): bpy.ops.object.mode_set(mode='OBJECT') except: pass + bpy.data.meshes.remove(grid_mesh) + bpy.data.meshes.remove(obj_me) return {'FINISHED'} - - -""" -class lattice_along_surface_panel(bpy.types.Panel): - bl_label = "Modifiers Tools" - bl_category = "Tools" - bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" - bl_context = "objectmode" - - def draw(self, context): - layout = self.layout - col = layout.column(align=True) - - try: - col.operator("object.lattice_along_surface", icon="MOD_LATTICE") - except: - pass -""" - - -def register(): - bpy.utils.register_class(lattice_along_surface) - # bpy.utils.register_class(lattice_along_surface_panel) - - -def unregister(): - bpy.utils.unregister_class(lattice_along_surface) - # bpy.utils.unregister_class(lattice_along_surface_panel) - - -if __name__ == "__main__": - register() diff --git a/mesh_tissue/numba_functions.py b/mesh_tissue/numba_functions.py new file mode 100644 index 00000000..1cb8e6b8 --- /dev/null +++ b/mesh_tissue/numba_functions.py @@ -0,0 +1,40 @@ +# ##### 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 ##### + +from numba import jit +import numpy as np + +@jit +def numba_reaction_diffusion(n_verts, a, b, diff_a, diff_b, f, k, dt, id0, id1, time_steps): + for i in range(time_steps): + lap_a = np.zeros(n_verts) + lap_b = np.zeros(n_verts) + lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge + lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge + + for i, j, la0, lb0 in zip(id0,id1,lap_a0,lap_b0): + lap_a[i] += la0 + lap_b[i] += lb0 + lap_a[j] -= la0 + lap_b[j] -= lb0 + ab2 = a*b**2 + #a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt") + #b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt") + a += (diff_a*lap_a - ab2 + f*(1-a))*dt + b += (diff_b*lap_b + ab2 - (k+f)*b)*dt + return a, b diff --git a/mesh_tissue/tessellate_numpy.py b/mesh_tissue/tessellate_numpy.py index e5f6db5c..9526d115 100644 --- a/mesh_tissue/tessellate_numpy.py +++ b/mesh_tissue/tessellate_numpy.py @@ -42,78 +42,548 @@ from bpy.props import ( FloatProperty, IntProperty, StringProperty, + PointerProperty ) from mathutils import Vector import numpy as np from math import sqrt -import random - - -def lerp(a, b, t): - return a + (b - a) * t - - -def lerp2(v1, v2, v3, v4, v): - v12 = v1 + (v2 - v1) * v.x - v43 = v4 + (v3 - v4) * v.x - return v12 + (v43 - v12) * v.y - +import random, time +import bmesh +from .utils import * + +def anim_tessellate_active(self, context): + ob = context.object + props = ob.tissue_tessellate + if not props.bool_hold: + try: + props.generator.name + props.component.name + bpy.ops.object.update_tessellate() + except: pass + +def anim_tessellate_object(ob): + try: + #bpy.context.view_layer.objects.active = ob + bpy.ops.object.update_tessellate() + except: + return None + +#from bpy.app.handlers import persistent + +#@persistent +def anim_tessellate(scene): + # store selected objects + #scene = context.scene + try: active_object = bpy.context.object + except: active_object = None + try: selected_objects = bpy.context.selected_objects + except: selected_objects = [] + if bpy.context.mode in ('OBJECT', 'PAINT_WEIGHT'): + old_mode = bpy.context.mode + if old_mode == 'PAINT_WEIGHT': old_mode = 'WEIGHT_PAINT' + for ob in scene.objects: + if ob.tissue_tessellate.bool_run: + hidden = ob.hide_viewport + ob.hide_viewport = False + for o in scene.objects: + if not o.hide_viewport: ob.select_set(False) + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + try: + bpy.ops.object.update_tessellate() + except: pass + ob.hide_viewport = hidden + # restore selected objects + for o in scene.objects: + if not o.hide_viewport: o.select_set(False) + for o in selected_objects: + if not o.hide_viewport: o.select_set(True) + bpy.context.view_layer.objects.active = active_object + try: bpy.ops.object.mode_set(mode=old_mode) + except: pass + return + +def set_tessellate_handler(self, context): + old_handlers = [] + blender_handlers = bpy.app.handlers.frame_change_post + for h in blender_handlers: + if "anim_tessellate" in str(h): + old_handlers.append(h) + for h in old_handlers: blender_handlers.remove(h) + for o in context.scene.objects: + if o.tissue_tessellate.bool_run: + blender_handlers.append(anim_tessellate) + break + return -def lerp3(v1, v2, v3, v4, v): - loc = lerp2(v1.co, v2.co, v3.co, v4.co, v) - nor = lerp2(v1.normal, v2.normal, v3.normal, v4.normal, v) - nor.normalize() - return loc + nor * v.z +class tissue_tessellate_prop(PropertyGroup): + bool_hold : BoolProperty( + name="Hold Update", + description="Prevent automatic update while other properties are changed", + default=False + ) + bool_run : BoolProperty( + name="Animatable Tessellation", + description="Automatically recompute the tessellation when the frame is changed", + default = False, + update = set_tessellate_handler + ) + zscale : FloatProperty( + name="Scale", default=1, soft_min=0, soft_max=10, + description="Scale factor for the component thickness", + update = anim_tessellate_active + ) + scale_mode : EnumProperty( + items=( + ('CONSTANT', "Constant", "Uniform thinkness"), + ('ADAPTIVE', "Proportional", "Preserve component's proportions") + ), + default='ADAPTIVE', + name="Z-Scale according to faces size", + update = anim_tessellate_active + ) + offset : FloatProperty( + name="Surface Offset", + default=1, + min=-1, + max=1, + soft_min=-1, + soft_max=1, + description="Surface offset", + update = anim_tessellate_active + ) + mode : EnumProperty( + items=( + ('BOUNDS', "Bounds", "The component fits automatically the size of the target face"), + ('LOCAL', "Local", "Based on Local coordinates, from 0 to 1"), + ('GLOBAL', 'Global', "Based on Global coordinates, from 0 to 1")), + default='BOUNDS', + name="Component Mode", + update = anim_tessellate_active + ) + rotation_mode : EnumProperty( + items=(('RANDOM', "Random", "Random faces rotation"), + ('UV', "Active UV", "Rotate according to UV coordinates"), + ('DEFAULT', "Default", "Default rotation")), + default='DEFAULT', + name="Component Rotation", + update = anim_tessellate_active + ) + fill_mode : EnumProperty( + items=( + ('QUAD', 'Quad', 'Regular quad tessellation. Uses only 3 or 4 vertices'), + ('FAN', 'Fan', 'Radial tessellation for polygonal faces'), + ('PATCH', 'Patch', 'Curved tessellation according to the last ' + + 'Subsurf\n(or Multires) modifiers. Works only with 4 sides ' + + 'patches.\nAfter the last Subsurf (or Multires) only ' + + 'deformation\nmodifiers can be used')), + default='QUAD', + name="Fill Mode", + update = anim_tessellate_active + ) + combine_mode : EnumProperty( + items=( + ('LAST', 'Last', 'Show only the last iteration'), + ('UNUSED', 'Unused', 'Combine each iteration with the unused faces of the previous iteration. Used for branching systems'), + ('ALL', 'All', 'Combine the result of all iterations')), + default='LAST', + name="Combine Mode", + update = anim_tessellate_active + ) + gen_modifiers : BoolProperty( + name="Generator Modifiers", + default=False, + description="Apply Modifiers and Shape Keys to the base object", + update = anim_tessellate_active + ) + com_modifiers : BoolProperty( + name="Component Modifiers", + default=False, + description="Apply Modifiers and Shape Keys to the component object", + update = anim_tessellate_active + ) + merge : BoolProperty( + name="Merge", + default=False, + description="Merge vertices in adjacent duplicates", + update = anim_tessellate_active + ) + merge_thres : FloatProperty( + name="Distance", + default=0.001, + soft_min=0, + soft_max=10, + description="Limit below which to merge vertices", + update = anim_tessellate_active + ) + generator : PointerProperty( + type=bpy.types.Object, + name="", + description="Base object for the tessellation", + update = anim_tessellate_active + ) + component : PointerProperty( + type=bpy.types.Object, + name="", + description="Component object for the tessellation", + #default="", + update = anim_tessellate_active + ) + bool_random : BoolProperty( + name="Randomize", + default=False, + description="Randomize component rotation", + update = anim_tessellate_active + ) + random_seed : IntProperty( + name="Seed", + default=0, + soft_min=0, + soft_max=10, + description="Random seed", + update = anim_tessellate_active + ) + bool_vertex_group : BoolProperty( + name="Map Vertex Group", + default=False, + description="Transfer all Vertex Groups from Base object", + update = anim_tessellate_active + ) + bool_selection : BoolProperty( + name="On selected Faces", + default=False, + description="Create Tessellation only on selected faces", + update = anim_tessellate_active + ) + bool_shapekeys : BoolProperty( + name="Use Shape Keys", + default=False, + description="Transfer Component's Shape Keys. If the name of Vertex " + "Groups and Shape Keys are the same, they will be " + "automatically combined", + update = anim_tessellate_active + ) + bool_smooth : BoolProperty( + name="Smooth Shading", + default=False, + description="Output faces with smooth shading rather than flat shaded", + update = anim_tessellate_active + ) + bool_materials : BoolProperty( + name="Transfer Materials", + default=False, + description="Preserve component's materials", + update = anim_tessellate_active + ) + bool_material_id : BoolProperty( + name="Tessellation on Material ID", + default=False, + description="Apply the component only on the selected Material", + update = anim_tessellate_active + ) + material_id : IntProperty( + name="Material ID", + default=0, + min=0, + description="Material ID", + update = anim_tessellate_active + ) + bool_dissolve_seams : BoolProperty( + name="Dissolve Seams", + default=False, + description="Dissolve all seam edges", + update = anim_tessellate_active + ) + iterations : IntProperty( + name="Iterations", + default=1, + min=1, + soft_max=5, + description="Automatically repeat the Tessellation using the " + + "generated geometry as new base object.\nUsefull for " + + "for branching systems. Dangerous!", + update = anim_tessellate_active + ) + bool_combine : BoolProperty( + name="Combine unused", + default=False, + description="Combine the generated geometry with unused faces", + update = anim_tessellate_active + ) + bool_advanced : BoolProperty( + name="Advanced Settings", + default=False, + description="Show more settings" + ) + normals_mode : EnumProperty( + items=( + ('VERTS', 'Along Normals', 'Consistent direction based on vertices normal'), + ('FACES', 'Individual Faces', 'Based on individual faces normal')), + default='VERTS', + name="Direction", + update = anim_tessellate_active + ) + bool_multi_components : BoolProperty( + name="Multi Components", + default=False, + description="Combine different components according to materials name", + update = anim_tessellate_active + ) + error_message : StringProperty( + name="Error Message", + default="" + ) + warning_message : StringProperty( + name="Warning Message", + default="" + ) + bounds_x : EnumProperty( + items=( + ('EXTEND', 'Extend', 'Default X coordinates'), + ('CLIP', 'Clip', 'Trim out of bounds in X direction'), + ('CYCLIC', 'Cyclic', 'Cyclic components in X direction')), + default='EXTEND', + name="Bounds X", + update = anim_tessellate_active + ) + bounds_y : EnumProperty( + items=( + ('EXTEND', 'Extend', 'Default Y coordinates'), + ('CLIP', 'Clip', 'Trim out of bounds in Y direction'), + ('CYCLIC', 'Cyclic', 'Cyclic components in Y direction')), + default='EXTEND', + name="Bounds Y", + update = anim_tessellate_active + ) + cap_faces : BoolProperty( + name="Cap Holes", + default=False, + description="Cap open edges loops", + update = anim_tessellate_active + ) + open_edges_crease : FloatProperty( + name="Open Edges Crease", + default=0, + min=0, + max=1, + description="Automatically set crease for open edges", + update = anim_tessellate_active + ) +def store_parameters(operator, ob): + ob.tissue_tessellate.bool_hold = True + ob.tissue_tessellate.generator = bpy.data.objects[operator.generator] + ob.tissue_tessellate.component = bpy.data.objects[operator.component] + ob.tissue_tessellate.zscale = operator.zscale + ob.tissue_tessellate.offset = operator.offset + ob.tissue_tessellate.gen_modifiers = operator.gen_modifiers + ob.tissue_tessellate.com_modifiers = operator.com_modifiers + ob.tissue_tessellate.mode = operator.mode + ob.tissue_tessellate.rotation_mode = operator.rotation_mode + ob.tissue_tessellate.merge = operator.merge + ob.tissue_tessellate.merge_thres = operator.merge_thres + ob.tissue_tessellate.scale_mode = operator.scale_mode + ob.tissue_tessellate.bool_random = operator.bool_random + ob.tissue_tessellate.random_seed = operator.random_seed + ob.tissue_tessellate.fill_mode = operator.fill_mode + ob.tissue_tessellate.bool_vertex_group = operator.bool_vertex_group + ob.tissue_tessellate.bool_selection = operator.bool_selection + ob.tissue_tessellate.bool_shapekeys = operator.bool_shapekeys + ob.tissue_tessellate.bool_smooth = operator.bool_smooth + ob.tissue_tessellate.bool_materials = operator.bool_materials + ob.tissue_tessellate.bool_material_id = operator.bool_material_id + ob.tissue_tessellate.material_id = operator.material_id + ob.tissue_tessellate.bool_dissolve_seams = operator.bool_dissolve_seams + ob.tissue_tessellate.iterations = operator.iterations + ob.tissue_tessellate.bool_advanced = operator.bool_advanced + ob.tissue_tessellate.normals_mode = operator.normals_mode + ob.tissue_tessellate.bool_combine = operator.bool_combine + ob.tissue_tessellate.bool_multi_components = operator.bool_multi_components + ob.tissue_tessellate.combine_mode = operator.combine_mode + ob.tissue_tessellate.bounds_x = operator.bounds_x + ob.tissue_tessellate.bounds_y = operator.bounds_y + ob.tissue_tessellate.cap_faces = operator.cap_faces + ob.tissue_tessellate.bool_hold = False + return ob -def tassellate(ob0, ob1, offset, zscale, gen_modifiers, com_modifiers, mode, - scale_mode, rotation_mode, rand_seed, fill_mode, - bool_vertex_group, bool_selection, bool_shapekeys): +def tessellate_patch(_ob0, _ob1, offset, zscale, com_modifiers, mode, + scale_mode, rotation_mode, rand_seed, bool_vertex_group, + bool_selection, bool_shapekeys, bool_material_id, material_id, + bounds_x, bounds_y): random.seed(rand_seed) - old_me0 = ob0.data # Store generator mesh - - if gen_modifiers or com_modifiers: - depsgraph = bpy.context.evaluated_depsgraph_get() - else: - depsgraph = None - me0_owner = None - if gen_modifiers: # Apply generator modifiers - me0_owner = ob0.evaluated_get(depsgraph) - me0 = me0_owner.to_mesh() - else: - me0 = ob0.data - ob0.data = me0 - base_polygons = [] + ob0 = convert_object_to_mesh(_ob0) + me0 = _ob0.data # Check if zero faces are selected - if bool_selection: - for p in ob0.data.polygons: - if p.select: - base_polygons.append(p) - else: - base_polygons = ob0.data.polygons - if len(base_polygons) == 0: - return 0 + if _ob0.type == 'MESH': + bool_cancel = True + for p in me0.polygons: + check_sel = check_mat = False + if not bool_selection or p.select: check_sel = True + if not bool_material_id or p.material_index == material_id: check_mat = True + if check_sel and check_mat: + bool_cancel = False + break + if bool_cancel: + return 0 + + levels = 0 + sculpt_levels = 0 + render_levels = 0 + bool_multires = False + multires_name = "" + not_allowed = ['FLUID_SIMULATION', 'ARRAY', 'BEVEL', 'BOOLEAN', 'BUILD', + 'DECIMATE', 'EDGE_SPLIT', 'MASK', 'MIRROR', 'REMESH', + 'SCREW', 'SOLIDIFY', 'TRIANGULATE', 'WIREFRAME', 'SKIN', + 'EXPLODE', 'PARTICLE_INSTANCE', 'PARTICLE_SYSTEM', 'SMOKE'] + modifiers0 = list(_ob0.modifiers)#[m for m in ob0.modifiers] + show_modifiers = [m.show_viewport for m in _ob0.modifiers] + show_modifiers.reverse() + modifiers0.reverse() + for m in modifiers0: + visible = m.show_viewport + #m.show_viewport = False + if m.type in ('SUBSURF', 'MULTIRES') and visible: + levels = m.levels + multires_name = m.name + if m.type == 'MULTIRES': + bool_multires = True + multires_name = m.name + sculpt_levels = m.sculpt_levels + render_levels = m.render_levels + else: bool_multires = False + break + elif m.type in not_allowed: + #ob0.data = old_me0 + #bpy.data.meshes.remove(me0) + return "modifiers_error" + + before = _ob0.copy() + #if ob0.type == 'MESH': before.data = me0 + before_mod = list(before.modifiers) + before_mod.reverse() + for m in before_mod: + if m.type in ('SUBSURF', 'MULTIRES') and m.show_viewport: + before.modifiers.remove(m) + break + else: before.modifiers.remove(m) + + before_subsurf = simple_to_mesh(before) + + before_bm = bmesh.new() + before_bm.from_mesh(before_subsurf) + before_bm.faces.ensure_lookup_table() + for f in before_bm.faces: + if len(f.loops) != 4: + return "topology_error" + before_bm.edges.ensure_lookup_table() + for e in before_bm.edges: + if len(e.link_faces) == 0: + return "wires_error" + before_bm.verts.ensure_lookup_table() + for v in before_bm.verts: + if len(v.link_faces) == 0: + return "verts_error" + + me0 = ob0.data + verts0 = me0.vertices # Collect generator vertices - # Apply component modifiers - me1_owner = ob1.evaluated_get(depsgraph) - if com_modifiers: - me1 = me1_owner.to_mesh() - else: - me1 = ob1.data + if com_modifiers or _ob1.type != 'MESH': bool_shapekeys = False - verts0 = me0.vertices # Collect generator vertices + # set Shape Keys to zero + if bool_shapekeys: + try: + original_key_values = [] + for sk in _ob1.data.shape_keys.key_blocks: + original_key_values.append(sk.value) + sk.value = 0 + except: + bool_shapekeys = False + + if not com_modifiers and not bool_shapekeys: + mod_visibility = [] + for m in _ob1.modifiers: + mod_visibility.append(m.show_viewport) + m.show_viewport = False + com_modifiers = True + + ob1 = convert_object_to_mesh(_ob1, com_modifiers, False) + me1 = ob1.data + + if mode != 'BOUNDS': + bpy.context.object.active_shape_key_index = 0 + # Bound X + if bounds_x != 'EXTEND': + if mode == 'GLOBAL': + planes_co = ((0,0,0),(1,1,1)) + plane_no = (1,0,0) + if mode == 'LOCAL': + planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((1,0,0))) + plane_no = planes_co[0]-planes_co[1] + bpy.ops.object.mode_set(mode='EDIT') + for co in planes_co: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) + bpy.ops.mesh.mark_seam() + bpy.ops.object.mode_set(mode='OBJECT') + _faces = ob1.data.polygons + if mode == 'GLOBAL': + for f in [f for f in _faces if (ob1.matrix_world @ f.center).x > 1]: + f.select = True + for f in [f for f in _faces if (ob1.matrix_world @ f.center).x < 0]: + f.select = True + else: + for f in [f for f in _faces if f.center.x > 1]: + f.select = True + for f in [f for f in _faces if f.center.x < 0]: + f.select = True + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='FACE') + if bounds_x == 'CLIP': + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + if bounds_x == 'CYCLIC': + bpy.ops.mesh.split() + bpy.ops.object.mode_set(mode='OBJECT') + # Bound Y + if bounds_y != 'EXTEND': + if mode == 'GLOBAL': + planes_co = ((0,0,0),(1,1,1)) + plane_no = (0,1,0) + if mode == 'LOCAL': + planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((0,1,0))) + plane_no = planes_co[0]-planes_co[1] + bpy.ops.object.mode_set(mode='EDIT') + for co in planes_co: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) + bpy.ops.mesh.mark_seam() + bpy.ops.object.mode_set(mode='OBJECT') + _faces = ob1.data.polygons + if mode == 'GLOBAL': + for f in [f for f in _faces if (ob1.matrix_world @ f.center).y > 1]: + f.select = True + for f in [f for f in _faces if (ob1.matrix_world @ f.center).y < 0]: + f.select = True + else: + for f in [f for f in _faces if f.center.y > 1]: + f.select = True + for f in [f for f in _faces if f.center.y < 0]: + f.select = True + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='FACE') + if bounds_y == 'CLIP': + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + if bounds_y == 'CYCLIC': + bpy.ops.mesh.split() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='OBJECT') # Component statistics n_verts = len(me1.vertices) - # n_edges = len(me1.edges) - # n_faces = len(me1.polygons) - - # Component transformations - # loc = ob1.location - # dim = ob1.dimensions - # scale = ob1.scale # Create empty lists new_verts = [] @@ -145,112 +615,763 @@ def tassellate(ob0, ob1, offset, zscale, gen_modifiers, com_modifiers, mode, # adaptive XY verts1 = [] for v in me1.vertices: - if mode == "ADAPTIVE": + if mode == 'BOUNDS': vert = v.co - min_c # (ob1.matrix_world * v.co) - min_c vert[0] = (vert[0] / bb[0] if bb[0] != 0 else 0.5) vert[1] = (vert[1] / bb[1] if bb[1] != 0 else 0.5) vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale - else: + elif mode == 'LOCAL': vert = v.co.xyz - vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale - verts1.append(vert) + vert[2] *= zscale + #vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale + elif mode == 'GLOBAL': + vert = ob1.matrix_world @ v.co + vert[2] *= zscale + try: + for sk in me1.shape_keys.key_blocks: + sk.data[v.index].co = ob1.matrix_world @ sk.data[v.index].co + except: pass + #verts1.append(vert) + v.co = vert + + # Bounds X, Y + if mode != 'BOUNDS': + if bounds_x == 'CYCLIC': + move_verts = [] + for f in [f for f in me1.polygons if (f.center).x > 1]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.x -= 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.x -= 1 + except: pass + move_verts = [] + for f in [f for f in me1.polygons if (f.center).x < 0]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.x += 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.x += 1 + except: pass + if bounds_y == 'CYCLIC': + move_verts = [] + for f in [f for f in me1.polygons if (f.center).y > 1]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.y -= 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.y -= 1 + except: pass + move_verts = [] + for f in [f for f in me1.polygons if (f.center).y < 0]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.y += 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.y += 1 + except: pass + verts1 = [v.co for v in me1.vertices] + + patch_faces = 4**levels + sides = int(sqrt(patch_faces)) + sides0 = sides-2 + patch_faces0 = int((sides-2)**2) + n_patches = int(len(me0.polygons)/patch_faces) + if len(me0.polygons)%patch_faces != 0: + #ob0.data = old_me0 + return "topology_error" + + new_verts = [] + new_edges = [] + new_faces = [] + + for o in bpy.context.view_layer.objects: o.select_set(False) + new_patch = None + + # All vertex group + if bool_vertex_group: + try: + weight = [] + for vg in ob0.vertex_groups: + _weight = [] + for v in me0.vertices: + try: + _weight.append(vg.weight(v.index)) + except: + _weight.append(0) + weight.append(_weight) + except: + bool_vertex_group = False + + # Adaptive Z + if scale_mode == 'ADAPTIVE': + if mode == 'BOUNDS': com_area = (bb[0]*bb[1]) + else: com_area = 1 + mult = 1/com_area*patch_faces + verts_area = [] + bm = bmesh.new() + bm.from_mesh(me0) + bm.verts.ensure_lookup_table() + for v in bm.verts: + area = 0 + faces = v.link_faces + for f in faces: + area += f.calc_area() + area/=len(faces) + area*=mult + verts_area.append(sqrt(area)) + + random.seed(rand_seed) + bool_correct = False + + _faces = [[[0] for ii in range(sides)] for jj in range(sides)] + _verts = [[[0] for ii in range(sides+1)] for jj in range(sides+1)] + + for i in range(n_patches): + poly = me0.polygons[i*patch_faces] + if bool_selection and not poly.select: continue + if bool_material_id and not poly.material_index == material_id: continue + + bool_correct = True + new_patch = bpy.data.objects.new("patch", me1.copy()) + bpy.context.collection.objects.link(new_patch) + + new_patch.select_set(True) + bpy.context.view_layer.objects.active = new_patch + + for area in bpy.context.screen.areas: + for space in area.spaces: + try: new_patch.local_view_set(space, True) + except: pass + + # Vertex Group + if bool_vertex_group: + for vg in ob0.vertex_groups: + new_patch.vertex_groups.new(name=vg.name) + + # find patch faces + faces = _faces.copy() + verts = _verts.copy() + shift1 = sides + shift2 = sides*2-1 + shift3 = sides*3-2 + for j in range(patch_faces): + if j < patch_faces0: + if levels == 0: + u = j%sides0 + v = j//sides0 + else: + u = j%sides0+1 + v = j//sides0+1 + elif j < patch_faces0 + shift1: + u = j-patch_faces0 + v = 0 + elif j < patch_faces0 + shift2: + u = sides-1 + v = j-(patch_faces0 + sides)+1 + elif j < patch_faces0 + shift3: + jj = j-(patch_faces0 + shift2) + u = sides-jj-2 + v = sides-1 + else: + jj = j-(patch_faces0 + shift3) + u = 0 + v = sides-jj-2 + face = me0.polygons[j+i*patch_faces] + faces[u][v] = face + verts[u][v] = verts0[face.vertices[0]] + if u == sides-1: + verts[sides][v] = verts0[face.vertices[1]] + if v == sides-1: + verts[u][sides] = verts0[face.vertices[3]] + if u == v == sides-1: + verts[sides][sides] = verts0[face.vertices[2]] + + # Random rotation + if rotation_mode == 'RANDOM': + rand = random.randint(0, 3) + if rand == 1: + verts = [[verts[k][w] for w in range(sides,-1,-1)] for k in range(sides,-1,-1)] + elif rand == 2: + verts = [[verts[w][k] for w in range(sides,-1,-1)] for k in range(sides+1)] + elif rand == 3: + verts = [[verts[w][k] for w in range(sides+1)] for k in range(sides,-1,-1)] + + # UV rotation + elif rotation_mode == 'UV' and ob0.type == 'MESH': + if len(ob0.data.uv_layers) > 0: + uv0 = me0.uv_layers.active.data[faces[0][0].index*4].uv + uv1 = me0.uv_layers.active.data[faces[0][-1].index*4 + 3].uv + uv2 = me0.uv_layers.active.data[faces[-1][-1].index*4 + 2].uv + uv3 = me0.uv_layers.active.data[faces[-1][0].index*4 + 1].uv + v01 = (uv0 + uv1) + v32 = (uv3 + uv2) + v0132 = v32 - v01 + v0132.normalize() + v12 = (uv1 + uv2) + v03 = (uv0 + uv3) + v1203 = v03 - v12 + v1203.normalize() + + vertUV = [] + dot1203 = v1203.x + dot0132 = v0132.x + if(abs(dot1203) < abs(dot0132)): + if (dot0132 > 0): + pass + else: + verts = [[verts[k][w] for w in range(sides,-1,-1)] for k in range(sides,-1,-1)] + else: + if(dot1203 < 0): + verts = [[verts[w][k] for w in range(sides,-1,-1)] for k in range(sides+1)] + else: + verts = [[verts[w][k] for w in range(sides+1)] for k in range(sides,-1,-1)] + + step = 1/sides + for vert, patch_vert in zip(verts1, new_patch.data.vertices): + # grid coordinates + u = int(vert[0]//step) + v = int(vert[1]//step) + u1 = min(u+1, sides) + v1 = min(v+1, sides) + if mode != 'BOUNDS': + if u > sides-1: + u = sides-1 + u1 = sides + if u < 0: + u = 0 + u1 = 1 + if v > sides-1: + v = sides-1 + v1 = sides + if v < 0: + v = 0 + v1 = 1 + v00 = verts[u][v] + v10 = verts[u1][v] + v01 = verts[u][v1] + v11 = verts[u1][v1] + # factor coordinates + fu = (vert[0]-u*step)/step + fv = (vert[1]-v*step)/step + fw = vert.z + # interpolate Z scaling factor + fvec2d = Vector((fu,fv,0)) + if scale_mode == 'ADAPTIVE': + a00 = verts_area[v00.index] + a10 = verts_area[v10.index] + a01 = verts_area[v01.index] + a11 = verts_area[v11.index] + fw*=lerp2(a00,a10,a01,a11,fvec2d) + # build factor vector + fvec = Vector((fu,fv,fw)) + # interpolate vertex on patch + patch_vert.co = lerp3(v00, v10, v01, v11, fvec) + + # Vertex Group + if bool_vertex_group: + for _weight, vg in zip(weight, new_patch.vertex_groups): + w00 = _weight[v00.index] + w10 = _weight[v10.index] + w01 = _weight[v01.index] + w11 = _weight[v11.index] + wuv = lerp2(w00,w10,w01,w11, fvec2d) + vg.add([patch_vert.index], wuv, "ADD") + + if bool_shapekeys: + for sk in ob1.data.shape_keys.key_blocks: + source = sk.data + for sk_v, _v in zip(source, me1.vertices): + if mode == 'BOUNDS': + sk_vert = sk_v.co - min_c # (ob1.matrix_world * v.co) - min_c + sk_vert[0] = (sk_vert[0] / bb[0] if bb[0] != 0 else 0.5) + sk_vert[1] = (sk_vert[1] / bb[1] if bb[1] != 0 else 0.5) + sk_vert[2] = (sk_vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale + elif mode == 'LOCAL': + sk_vert = sk_v.co#.xyzco + #sk_vert[2] *= zscale + #sk_vert[2] = (sk_vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale + elif mode == 'GLOBAL': + #sk_vert = ob1.matrix_world @ sk_v.co + sk_vert = sk_v.co + #sk_vert[2] *= zscale + + # grid coordinates + u = int(sk_vert[0]//step) + v = int(sk_vert[1]//step) + u1 = min(u+1, sides) + v1 = min(v+1, sides) + if mode != 'BOUNDS': + if u > sides-1: + u = sides-1 + u1 = sides + if u < 0: + u = 0 + u1 = 1 + if v > sides-1: + v = sides-1 + v1 = sides + if v < 0: + v = 0 + v1 = 1 + v00 = verts[u][v] + v10 = verts[u1][v] + v01 = verts[u][v1] + v11 = verts[u1][v1] + # factor coordinates + fu = (sk_vert[0]-u*step)/step + fv = (sk_vert[1]-v*step)/step + fw = sk_vert.z + + if scale_mode == 'ADAPTIVE': + a00 = verts_area[v00.index] + a10 = verts_area[v10.index] + a01 = verts_area[v01.index] + a11 = verts_area[v11.index] + fw*=lerp2(a00,a10,a01,a11,Vector((fu,fv,0))) + + fvec = Vector((fu,fv,fw)) + sk_co = lerp3(v00, v10, v01, v11, fvec) + + new_patch.data.shape_keys.key_blocks[sk.name].data[_v.index].co = sk_co + + #if ob0.type == 'MESH': ob0.data = old_me0 + if not bool_correct: return 0 + + bpy.ops.object.join() + + if bool_shapekeys: + # set original values and combine Shape Keys and Vertex Groups + for sk, val in zip(_ob1.data.shape_keys.key_blocks, original_key_values): + sk.value = val + new_patch.data.shape_keys.key_blocks[sk.name].value = val + if bool_vertex_group: + for sk in new_patch.data.shape_keys.key_blocks: + for vg in new_patch.vertex_groups: + if sk.name == vg.name: + sk.vertex_group = vg.name + + new_name = ob0.name + "_" + ob1.name + new_patch.name = "tessellate_temp" + + if bool_multires: + for m in ob0.modifiers: + if m.type == 'MULTIRES' and m.name == multires_name: + m.levels = levels + m.sculpt_levels = sculpt_levels + m.render_levels = render_levels + # restore original modifiers visibility for component object + try: + for m, vis in zip(_ob1.modifiers, mod_visibility): + m.show_viewport = vis + except: pass + + bpy.data.objects.remove(before) + bpy.data.objects.remove(ob0) + bpy.data.objects.remove(ob1) + return new_patch + +def tessellate_original(_ob0, _ob1, offset, zscale, gen_modifiers, com_modifiers, mode, + scale_mode, rotation_mode, rand_seed, fill_mode, + bool_vertex_group, bool_selection, bool_shapekeys, + bool_material_id, material_id, normals_mode, bounds_x, bounds_y): + + if com_modifiers or _ob1.type != 'MESH': bool_shapekeys = False + random.seed(rand_seed) + + if bool_shapekeys: + try: + original_key_values = [] + for sk in _ob1.data.shape_keys.key_blocks: + original_key_values.append(sk.value) + sk.value = 0 + except: + bool_shapekeys = False + + ob0 = convert_object_to_mesh(_ob0, gen_modifiers, True) + me0 = ob0.data + ob1 = convert_object_to_mesh(_ob1, com_modifiers, True) + me1 = ob1.data + + base_polygons = [] + base_face_normals = [] + + n_faces0 = len(me0.polygons) + + # Check if zero faces are selected + if (bool_selection and ob0.type == 'MESH') or bool_material_id: + for p in me0.polygons: + if (bool_selection and ob0.type == 'MESH'): + is_sel = p.select + else: is_sel = True + if bool_material_id: + is_mat = p.material_index == material_id + else: is_mat = True + if is_sel and is_mat: + base_polygons.append(p) + base_face_normals.append(p.normal) + else: + base_polygons = me0.polygons + base_face_normals = [p.normal for p in me0.polygons] + + # numpy test: slower + #base_face_normals = np.zeros(n_faces0*3) + #me0.polygons.foreach_get("normal", base_face_normals) + #base_face_normals = base_face_normals.reshape((n_faces0,3)) + + if len(base_polygons) == 0: + return 0 + + if mode != 'BOUNDS': + + bpy.ops.object.select_all(action='DESELECT') + for o in bpy.context.view_layer.objects: o.select_set(False) + bpy.context.view_layer.objects.active = ob1 + ob1.select_set(True) + bpy.context.object.active_shape_key_index = 0 + # Bound X + if bounds_x != 'EXTEND': + if mode == 'GLOBAL': + planes_co = ((0,0,0),(1,1,1)) + plane_no = (1,0,0) + if mode == 'LOCAL': + planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((1,0,0))) + plane_no = planes_co[0]-planes_co[1] + bpy.ops.object.mode_set(mode='EDIT') + for co in planes_co: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) + bpy.ops.mesh.mark_seam() + bpy.ops.object.mode_set(mode='OBJECT') + _faces = ob1.data.polygons + if mode == 'GLOBAL': + for f in [f for f in _faces if (ob1.matrix_world @ f.center).x > 1]: + f.select = True + for f in [f for f in _faces if (ob1.matrix_world @ f.center).x < 0]: + f.select = True + else: + for f in [f for f in _faces if f.center.x > 1]: + f.select = True + for f in [f for f in _faces if f.center.x < 0]: + f.select = True + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='FACE') + if bounds_x == 'CLIP': + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + if bounds_x == 'CYCLIC': + bpy.ops.mesh.split() + bpy.ops.object.mode_set(mode='OBJECT') + # Bound Y + if bounds_y != 'EXTEND': + if mode == 'GLOBAL': + planes_co = ((0,0,0),(1,1,1)) + plane_no = (0,1,0) + if mode == 'LOCAL': + planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((0,1,0))) + plane_no = planes_co[0]-planes_co[1] + bpy.ops.object.mode_set(mode='EDIT') + for co in planes_co: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) + bpy.ops.mesh.mark_seam() + bpy.ops.object.mode_set(mode='OBJECT') + _faces = ob1.data.polygons + if mode == 'GLOBAL': + for f in [f for f in _faces if (ob1.matrix_world @ f.center).y > 1]: + f.select = True + for f in [f for f in _faces if (ob1.matrix_world @ f.center).y < 0]: + f.select = True + else: + for f in [f for f in _faces if f.center.y > 1]: + f.select = True + for f in [f for f in _faces if f.center.y < 0]: + f.select = True + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='FACE') + if bounds_y == 'CLIP': + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + if bounds_y == 'CYCLIC': + bpy.ops.mesh.split() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='OBJECT') + #ob1 = new_ob1 + + me1 = ob1.data - # component vertices - vs1 = np.array([v for v in verts1]).reshape(len(verts1), 3, 1) - vx = vs1[:, 0] - vy = vs1[:, 1] - vz = vs1[:, 2] + verts0 = me0.vertices # Collect generator vertices + + # Component statistics + n_verts1 = len(me1.vertices) + n_edges1 = len(me1.edges) + n_faces1 = len(me1.polygons) + + # Create empty lists + new_verts = [] + new_edges = [] + new_faces = [] + new_verts_np = np.array(()) + + # Component Coordinates + co1 = [0]*n_verts1*3 + + if mode == 'GLOBAL': + for v in me1.vertices: + v.co = ob1.matrix_world @ v.co + try: + for sk in me1.shape_keys.key_blocks: + sk.data[v.index].co = ob1.matrix_world @ sk.data[v.index].co + except: pass + if mode != 'BOUNDS': + if bounds_x == 'CYCLIC': + move_verts = [] + for f in [f for f in me1.polygons if (f.center).x > 1]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.x -= 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.x -= 1 + except: pass + move_verts = [] + for f in [f for f in me1.polygons if (f.center).x < 0]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.x += 1 + try: + _ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.x += 1 + except: pass + if bounds_y == 'CYCLIC': + move_verts = [] + for f in [f for f in me1.polygons if (f.center).y > 1]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.y -= 1 + try: + #new_ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.y -= 1 + except: pass + move_verts = [] + for f in [f for f in me1.polygons if (f.center).y < 0]: + for v in f.vertices: + if v not in move_verts: move_verts.append(v) + for v in move_verts: + me1.vertices[v].co.y += 1 + try: + #new_ob1.active_shape_key_index = 0 + for sk in me1.shape_keys.key_blocks: + sk.data[v].co.y += 1 + except: pass + + + me1.vertices.foreach_get("co", co1) + co1 = np.array(co1) + vx = co1[0::3].reshape((n_verts1,1)) + vy = co1[1::3].reshape((n_verts1,1)) + vz = co1[2::3].reshape((n_verts1,1)) + min_c = Vector((vx.min(), vy.min(), vz.min())) # Min BB Corner + max_c = Vector((vx.max(), vy.max(), vz.max())) # Max BB Corner + bb = max_c - min_c # Bounding Box + + # Component Coordinates + if mode == 'BOUNDS': + vx = (vx - min_c[0]) / bb[0] if bb[0] != 0 else 0.5 + vy = (vy - min_c[1]) / bb[1] if bb[1] != 0 else 0.5 + vz = ((vz - min_c[2]) + (-0.5 + offset * 0.5) * bb[2]) * zscale + else: + vz *= zscale # Component polygons fs1 = [[i for i in p.vertices] for p in me1.polygons] new_faces = fs1[:] # Component edges - es1 = [[i for i in e.vertices] for e in me1.edges if e.is_loose] + es1 = np.array([[i for i in e.vertices] for e in me1.edges]) + #es1 = [[i for i in e.vertices] for e in me1.edges if e.is_loose] new_edges = es1[:] # SHAPE KEYS - shapekeys = [] - do_shapekeys = False - if me1.shape_keys is not None and bool_shapekeys: - if len(me1.shape_keys.key_blocks) > 1: + if bool_shapekeys: + basis = True #com_modifiers + vx_key = [] + vy_key = [] + vz_key = [] + sk_np = [] + for sk in ob1.data.shape_keys.key_blocks: do_shapekeys = True + # set all keys to 0 + for _sk in ob1.data.shape_keys.key_blocks: _sk.value = 0 + sk.value = 1 + + if basis: + basis = False + continue - # Read active key - active_key = ob1.active_shape_key_index - if active_key == 0: - active_key = 1 + # Apply component modifiers + if com_modifiers: + sk_ob = convert_object_to_mesh(_ob1) + sk_data = sk_ob.data + source = sk_data.vertices + else: + source = sk.data - for v in me1.shape_keys.key_blocks[active_key].data: - if mode == "ADAPTIVE": + shapekeys = [] + for v in source: + if mode == 'BOUNDS': vert = v.co - min_c vert[0] = vert[0] / bb[0] vert[1] = vert[1] / bb[1] vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale - else: + elif mode == 'LOCAL': + vert = v.co.xyz + vert[2] *= zscale + #vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * \ + # zscale + elif mode == 'GLOBAL': vert = v.co.xyz - vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * \ - zscale + #vert = ob1.matrix_world @ v.co + vert[2] *= zscale shapekeys.append(vert) # Component vertices key1 = np.array([v for v in shapekeys]).reshape(len(shapekeys), 3, 1) - vx_key = key1[:, 0] - vy_key = key1[:, 1] - vz_key = key1[:, 2] + vx_key.append(key1[:, 0]) + vy_key.append(key1[:, 1]) + vz_key.append(key1[:, 2]) + #sk_np.append([]) - # Active vertex group + # All vertex group if bool_vertex_group: try: weight = [] - group_index = ob0.vertex_groups.active_index - active_vertex_group = ob0.vertex_groups[group_index] - for v in me0.vertices: - try: - weight.append(active_vertex_group.weight(v.index)) - except: - weight.append(0) + vertex_groups = ob0.vertex_groups + for vg in vertex_groups: + _weight = [] + for v in me0.vertices: + try: + _weight.append(vg.weight(v.index)) + #print(vg.weight(v.index)) + #_weight.append(v.groups[0]) + except: + _weight.append(0) + weight.append(_weight) except: bool_vertex_group = False + # Adaptive Z + if scale_mode == 'ADAPTIVE': + if mode == 'BOUNDS': com_area = (bb[0]*bb[1]) + else: com_area = 1 + if com_area == 0: mult = 1 + else: mult = 1/com_area + verts_area = [] + bm = bmesh.new() + bm.from_mesh(me0) + bm.verts.ensure_lookup_table() + for v in bm.verts: + area = 0 + faces = v.link_faces + for f in faces: + area += f.calc_area() + try: + area/=len(faces) + area*=mult + verts_area.append(sqrt(area)) + except: + verts_area.append(1) + # FAN tessellation mode if fill_mode == 'FAN': fan_verts = [v.co.to_tuple() for v in me0.vertices] fan_polygons = [] + fan_select = [] + fan_material = [] + fan_normals = [] # selected_faces = [] for p in base_polygons: - # if bool_selection and not p.select: continue fan_center = Vector((0, 0, 0)) + center_area = 0 for v in p.vertices: fan_center += me0.vertices[v].co + if scale_mode == 'ADAPTIVE': + center_area += verts_area[v] fan_center /= len(p.vertices) + center_area /= len(p.vertices) + last_vert = len(fan_verts) fan_verts.append(fan_center.to_tuple()) + #fan_verts.append(fan_center) + if scale_mode == 'ADAPTIVE': + verts_area.append(center_area) # Vertex Group if bool_vertex_group: - center_weight = sum([weight[i] for i in p.vertices]) / len(p.vertices) - weight.append(center_weight) + for w in weight: + center_weight = sum([w[i] for i in p.vertices]) / len(p.vertices) + w.append(center_weight) for i in range(len(p.vertices)): fan_polygons.append((p.vertices[i], p.vertices[(i + 1) % len(p.vertices)], last_vert, last_vert)) - # if bool_selection: selected_faces.append(p.select) + + if bool_material_id: fan_material.append(p.material_index) + if bool_selection: fan_select.append(p.select) + if normals_mode == 'FACES': + fan_normals.append(p.normal) + fan_me = bpy.data.meshes.new('Fan.Mesh') fan_me.from_pydata(tuple(fan_verts), [], tuple(fan_polygons)) - me0 = fan_me + me0 = fan_me.copy() + bpy.data.meshes.remove(fan_me) verts0 = me0.vertices base_polygons = me0.polygons - """ - for i in range(len(selected_faces)): - fan_me.polygons[i].select = selected_faces[i] - """ + if normals_mode == 'FACES': base_face_normals = fan_normals + count = 0 # necessary for UV calculation # TESSELLATION j = 0 + jj = -1 + bool_correct = False + + # optimization test + n_faces = len(base_polygons) + _vs0 = [0]*n_faces + _nvs0 = [0]*n_faces + _sz = [0]*n_faces + _w0 = [[0]*n_faces]*len(ob0.vertex_groups) + np_faces = [np.array(p) for p in fs1] + new_faces = [0]*n_faces*n_faces1 + face1_count = 0 + for p in base_polygons: + + bool_correct = True + if rotation_mode == 'UV' and ob0.type != 'MESH': + rotation_mode = 'DEFAULT' + # Random rotation if rotation_mode == 'RANDOM': shifted_vertices = [] @@ -258,211 +1379,333 @@ def tassellate(ob0, ob1, offset, zscale, gen_modifiers, com_modifiers, mode, rand = random.randint(0, n_poly_verts) for i in range(n_poly_verts): shifted_vertices.append(p.vertices[(i + rand) % n_poly_verts]) + if scale_mode == 'ADAPTIVE': + verts_area0 = np.array([verts_area[i] for i in shifted_vertices]) vs0 = np.array([verts0[i].co for i in shifted_vertices]) nvs0 = np.array([verts0[i].normal for i in shifted_vertices]) + if normals_mode == 'VERTS': + nvs0 = np.array([verts0[i].normal for i in shifted_vertices]) # vertex weight if bool_vertex_group: ws0 = [] - for i in shifted_vertices: - try: - ws0.append(weight[i]) - except: - ws0.append(0) - - ws0 = np.array(ws0) + for w in weight: + _ws0 = [] + for i in shifted_vertices: + try: + _ws0.append(w[i]) + except: + _ws0.append(0) + ws0.append(np.array(_ws0)) # UV rotation - elif rotation_mode == 'UV' and len(ob0.data.uv_layers) > 0 and \ - fill_mode != 'FAN': - i = p.index - v01 = (me0.uv_layers.active.data[count].uv + - me0.uv_layers.active.data[count + 1].uv) - if len(p.vertices) > 3: - v32 = (me0.uv_layers.active.data[count + 3].uv + - me0.uv_layers.active.data[count + 2].uv) - else: - v32 = (me0.uv_layers.active.data[count].uv + + elif rotation_mode == 'UV': + if len(ob0.data.uv_layers) > 0 and fill_mode != 'FAN': + i = p.index + if bool_material_id: + count = sum([len(p.vertices) for p in me0.polygons[:i]]) + #if i == 0: count = 0 + v01 = (me0.uv_layers.active.data[count].uv + + me0.uv_layers.active.data[count + 1].uv) + if len(p.vertices) > 3: + v32 = (me0.uv_layers.active.data[count + 3].uv + + me0.uv_layers.active.data[count + 2].uv) + else: + v32 = (me0.uv_layers.active.data[count].uv + + me0.uv_layers.active.data[count + 2].uv) + v0132 = v32 - v01 + v0132.normalize() + + v12 = (me0.uv_layers.active.data[count + 1].uv + me0.uv_layers.active.data[count + 2].uv) - v0132 = v32 - v01 - v0132.normalize() - - v12 = (me0.uv_layers.active.data[count + 1].uv + - me0.uv_layers.active.data[count + 2].uv) - if len(p.vertices) > 3: - v03 = (me0.uv_layers.active.data[count].uv + - me0.uv_layers.active.data[count + 3].uv) - else: - v03 = (me0.uv_layers.active.data[count].uv + - me0.uv_layers.active.data[count].uv) - v1203 = v03 - v12 - v1203.normalize() - - vertUV = [] - dot1203 = v1203.x - dot0132 = v0132.x - if(abs(dot1203) < abs(dot0132)): - if (dot0132 > 0): - vertUV = p.vertices[1:] + p.vertices[:1] + if len(p.vertices) > 3: + v03 = (me0.uv_layers.active.data[count].uv + + me0.uv_layers.active.data[count + 3].uv) else: - vertUV = p.vertices[3:] + p.vertices[:3] - else: - if(dot1203 < 0): - vertUV = p.vertices[:] + v03 = (me0.uv_layers.active.data[count].uv + + me0.uv_layers.active.data[count].uv) + v1203 = v03 - v12 + v1203.normalize() + + vertUV = [] + dot1203 = v1203.x + dot0132 = v0132.x + if(abs(dot1203) < abs(dot0132)): + if (dot0132 > 0): + vertUV = p.vertices[1:] + p.vertices[:1] + else: + vertUV = p.vertices[3:] + p.vertices[:3] else: - vertUV = p.vertices[2:] + p.vertices[:2] - vs0 = np.array([verts0[i].co for i in vertUV]) - nvs0 = np.array([verts0[i].normal for i in vertUV]) - - # Vertex weight - if bool_vertex_group: - ws0 = [] - for i in vertUV: - try: - ws0.append(weight[i]) - except: - ws0.append(0) - ws0 = np.array(ws0) - - count += len(p.vertices) + if(dot1203 < 0): + vertUV = p.vertices[:] + else: + vertUV = p.vertices[2:] + p.vertices[:2] + vs0 = np.array([verts0[i].co for i in vertUV]) + nvs0 = np.array([verts0[i].normal for i in vertUV]) + + # Vertex weight + if bool_vertex_group: + ws0 = [] + for w in weight: + _ws0 = [] + for i in vertUV: + try: + _ws0.append(w[i]) + except: + _ws0.append(0) + ws0.append(np.array(_ws0)) + + count += len(p.vertices) + else: rotation_mode = 'DEFAULT' # Default rotation - else: + if rotation_mode == 'DEFAULT': vs0 = np.array([verts0[i].co for i in p.vertices]) nvs0 = np.array([verts0[i].normal for i in p.vertices]) # Vertex weight if bool_vertex_group: ws0 = [] - for i in p.vertices: - try: - ws0.append(weight[i]) - except: - ws0.append(0) - ws0 = np.array(ws0) - - # considering only 4 vertices - vs0 = np.array((vs0[0], vs0[1], vs0[2], vs0[-1])) - nvs0 = np.array((nvs0[0], nvs0[1], nvs0[2], nvs0[-1])) + for w in weight: + _ws0 = [] + for i in p.vertices: + try: + _ws0.append(w[i]) + except: + _ws0.append(0) + ws0.append(np.array(_ws0)) + + # optimization test + _vs0[j] = (vs0[0], vs0[1], vs0[2], vs0[-1]) + if normals_mode == 'VERTS': + _nvs0[j] = (nvs0[0], nvs0[1], nvs0[2], nvs0[-1]) + #else: + # _nvs0[j] = base_face_normals[j] - # remapped vertex coordinates - v0 = vs0[0] + (vs0[1] - vs0[0]) * vx - v1 = vs0[3] + (vs0[2] - vs0[3]) * vx - v2 = v0 + (v1 - v0) * vy - - # remapped vertex normal - nv0 = nvs0[0] + (nvs0[1] - nvs0[0]) * vx - nv1 = nvs0[3] + (nvs0[2] - nvs0[3]) * vx - nv2 = nv0 + (nv1 - nv0) * vy # vertex z to normal - v3 = v2 + nv2 * vz * (sqrt(p.area) if scale_mode == "ADAPTIVE" else 1) + if scale_mode == 'ADAPTIVE': + poly_faces = (p.vertices[0], p.vertices[1], p.vertices[2], p.vertices[-1]) + if rotation_mode == 'RANDOM': sz = verts_area0 + else: sz = np.array([verts_area[i] for i in poly_faces]) + + _sz[j] = sz if bool_vertex_group: - ws0 = np.array((ws0[0], ws0[1], ws0[2], ws0[-1])) - # Interpolate vertex weight - w0 = ws0[0] + (ws0[1] - ws0[0]) * vx - w1 = ws0[3] + (ws0[2] - ws0[3]) * vx - w2 = w0 + (w1 - w0) * vy - - # Shapekeys - if do_shapekeys: - # remapped vertex coordinates - v0 = vs0[0] + (vs0[1] - vs0[0]) * vx_key - v1 = vs0[3] + (vs0[2] - vs0[3]) * vx_key - v2 = v0 + (v1 - v0) * vy_key - # remapped vertex normal - nv0 = nvs0[0] + (nvs0[1] - nvs0[0]) * vx_key - nv1 = nvs0[3] + (nvs0[2] - nvs0[3]) * vx_key - nv2 = nv0 + (nv1 - nv0) * vy_key - # vertex z to normal - v3_key = v2 + nv2 * vz_key * (sqrt(p.area) if - scale_mode == "ADAPTIVE" else 1) - v3 = v3 + (v3_key - v3) * w2 - - if j == 0: - new_verts_np = v3 - if bool_vertex_group: - new_vertex_group_np = w2 - else: - # Appending vertices - new_verts_np = np.concatenate((new_verts_np, v3), axis=0) - # Appending vertex group - if bool_vertex_group: - new_vertex_group_np = np.concatenate((new_vertex_group_np, w2), - axis=0) - # Appending faces - for p in fs1: - new_faces.append([i + n_verts * j for i in p]) - # Appending edges - for e in es1: - new_edges.append([i + n_verts * j for i in e]) + vg_count = 0 + for _ws0 in ws0: + _w0[vg_count][j] = (_ws0[0], _ws0[1], _ws0[2], _ws0[-1]) + vg_count += 1 + + for p in fs1: + new_faces[face1_count] = [i + n_verts1 * j for i in p] + face1_count += 1 j += 1 + # build edges list + n_edges1 = new_edges.shape[0] + new_edges = new_edges.reshape((1, n_edges1, 2)) + new_edges = new_edges.repeat(n_faces,axis=0) + new_edges = new_edges.reshape((n_edges1*n_faces, 2)) + increment = np.arange(n_faces)*n_verts1 + increment = increment.repeat(n_edges1, axis=0) + increment = increment.reshape((n_faces*n_edges1,1)) + new_edges = new_edges + increment + + # optimization test + _vs0 = np.array(_vs0) + _sz = np.array(_sz) + + _vs0_0 = _vs0[:,0].reshape((n_faces,1,3)) + _vs0_1 = _vs0[:,1].reshape((n_faces,1,3)) + _vs0_2 = _vs0[:,2].reshape((n_faces,1,3)) + _vs0_3 = _vs0[:,3].reshape((n_faces,1,3)) + + # remapped vertex coordinates + v0 = _vs0_0 + (_vs0_1 - _vs0_0) * vx + v1 = _vs0_3 + (_vs0_2 - _vs0_3) * vx + v2 = v0 + (v1 - v0) * vy + + # remapped vertex normal + if normals_mode == 'VERTS': + _nvs0 = np.array(_nvs0) + _nvs0_0 = _nvs0[:,0].reshape((n_faces,1,3)) + _nvs0_1 = _nvs0[:,1].reshape((n_faces,1,3)) + _nvs0_2 = _nvs0[:,2].reshape((n_faces,1,3)) + _nvs0_3 = _nvs0[:,3].reshape((n_faces,1,3)) + nv0 = _nvs0_0 + (_nvs0_1 - _nvs0_0) * vx + nv1 = _nvs0_3 + (_nvs0_2 - _nvs0_3) * vx + nv2 = nv0 + (nv1 - nv0) * vy + else: + nv2 = np.array(base_face_normals).reshape((n_faces,1,3)) + + if bool_vertex_group: + n_vg = len(_w0) + w = np.array(_w0) + #for w in _w0: + #w = np.array(w) + w_0 = w[:,:,0].reshape((n_vg, n_faces,1,1)) + w_1 = w[:,:,1].reshape((n_vg, n_faces,1,1)) + w_2 = w[:,:,2].reshape((n_vg, n_faces,1,1)) + w_3 = w[:,:,3].reshape((n_vg, n_faces,1,1)) + # remapped weight + w0 = w_0 + (w_1 - w_0) * vx + w1 = w_3 + (w_2 - w_3) * vx + w = w0 + (w1 - w0) * vy + w = w.reshape((n_vg, n_faces*n_verts1)) + #w = w2.tolist() + + if scale_mode == 'ADAPTIVE': + _sz_0 = _sz[:,0].reshape((n_faces,1,1)) + _sz_1 = _sz[:,1].reshape((n_faces,1,1)) + _sz_2 = _sz[:,2].reshape((n_faces,1,1)) + _sz_3 = _sz[:,3].reshape((n_faces,1,1)) + # remapped z scale + sz0 = _sz_0 + (_sz_1 - _sz_0) * vx + sz1 = _sz_3 + (_sz_2 - _sz_3) * vx + sz2 = sz0 + (sz1 - sz0) * vy + v3 = v2 + nv2 * vz * sz2 + else: + v3 = v2 + nv2 * vz + + new_verts_np = v3.reshape((n_faces*n_verts1,3)) + + if bool_shapekeys: + n_sk = len(vx_key) + sk_np = [0]*n_sk + for i in range(n_sk): + vx = np.array(vx_key) + vy = np.array(vy_key) + vz = np.array(vz_key) + + # remapped vertex coordinates + v0 = _vs0_0 + (_vs0_1 - _vs0_0) * vx + v1 = _vs0_3 + (_vs0_2 - _vs0_3) * vx + v2 = v0 + (v1 - v0) * vy + + # remapped vertex normal + if normals_mode == 'VERTS': + nv0 = _nvs0_0 + (_nvs0_1 - _nvs0_0) * vx + nv1 = _nvs0_3 + (_nvs0_2 - _nvs0_3) * vx + nv2 = nv0 + (nv1 - nv0) * vy + else: + nv2 = np.array(base_face_normals).reshape((n_faces,1,3)) + + if scale_mode == 'ADAPTIVE': + # remapped z scale + sz0 = _sz_0 + (_sz_1 - _sz_0) * vx + sz1 = _sz_3 + (_sz_2 - _sz_3) * vx + sz2 = sz0 + (sz1 - sz0) * vy + v3 = v2 + nv2 * vz * sz2 + else: + v3 = v2 + nv2 * vz + + sk_np[i] = v3.reshape((n_faces*n_verts1,3)) + + #if ob0.type == 'MESH': ob0.data = old_me0 + + if not bool_correct: return 0 + new_verts = new_verts_np.tolist() new_name = ob0.name + "_" + ob1.name new_me = bpy.data.meshes.new(new_name) - new_me.from_pydata(new_verts, new_edges, new_faces) + new_me.from_pydata(new_verts, new_edges.tolist(), new_faces) new_me.update(calc_edges=True) new_ob = bpy.data.objects.new("tessellate_temp", new_me) + # vertex group + if bool_vertex_group and False: + for vg in ob0.vertex_groups: + new_ob.vertex_groups.new(name=vg.name) + for i in range(len(vg_np[vg.index])): + new_ob.vertex_groups[vg.name].add([i], vg_np[vg.index][i],"ADD") # vertex group if bool_vertex_group: - new_ob.vertex_groups.new(name="generator_group") - for i in range(len(new_vertex_group_np)): - new_ob.vertex_groups["generator_group"].add([i], - new_vertex_group_np[i], - "ADD") - ob0.data = old_me0 - - if me0_owner: - me0_owner.to_mesh_clear() - - if me1_owner: - me1_owner.to_mesh_clear() - - return new_ob + for vg in ob0.vertex_groups: + new_ob.vertex_groups.new(name=vg.name) + for i in range(len(w[vg.index])): + new_ob.vertex_groups[vg.name].add([i], w[vg.index,i],"ADD") + + if bool_shapekeys: + basis = com_modifiers + sk_count = 0 + for sk, val in zip(_ob1.data.shape_keys.key_blocks, original_key_values): + sk.value = val + new_ob.shape_key_add(name=sk.name) + new_ob.data.shape_keys.key_blocks[sk.name].value = val + # set shape keys vertices + sk_data = new_ob.data.shape_keys.key_blocks[sk.name].data + if sk_count == 0: + sk_count += 1 + continue + for id in range(len(sk_data)): + sk_data[id].co = sk_np[sk_count-1][id] + sk_count += 1 + if bool_vertex_group: + for sk in new_ob.data.shape_keys.key_blocks: + for vg in new_ob.vertex_groups: + if sk.name == vg.name: + sk.vertex_group = vg.name + + # EDGES SEAMS + edge_data = [0]*n_edges1 + me1.edges.foreach_get("use_seam",edge_data) + if any(edge_data): + edge_data = edge_data*n_faces + new_ob.data.edges.foreach_set("use_seam",edge_data) + + # EDGES SHARP + edge_data = [0]*n_edges1 + me1.edges.foreach_get("use_edge_sharp",edge_data) + if any(edge_data): + edge_data = edge_data*n_faces + new_ob.data.edges.foreach_set("use_edge_sharp",edge_data) + + bpy.ops.object.select_all(action='DESELECT') + bpy.context.collection.objects.link(new_ob) + new_ob.select_set(True) + bpy.context.view_layer.objects.active = new_ob + + # EDGES BEVEL + edge_data = [0]*n_edges1 + me1.edges.foreach_get("bevel_weight",edge_data) + if any(edge_data): + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.transform.edge_bevelweight(value=1) + bpy.ops.object.mode_set(mode='OBJECT') + edge_data = edge_data*n_faces + new_ob.data.edges.foreach_set("bevel_weight",edge_data) + # EDGE CREASES + edge_data = [0]*n_edges1 + me1.edges.foreach_get("crease",edge_data) + if any(edge_data): + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.transform.edge_crease(value=1) + bpy.ops.object.mode_set(mode='OBJECT') + edge_data = edge_data*n_faces + new_ob.data.edges.foreach_set('crease', edge_data) -def store_parameters(operator, ob): - ob.tissue_tessellate.generator = operator.generator - ob.tissue_tessellate.component = operator.component - ob.tissue_tessellate.zscale = operator.zscale - ob.tissue_tessellate.offset = operator.offset - ob.tissue_tessellate.gen_modifiers = operator.gen_modifiers - ob.tissue_tessellate.com_modifiers = operator.com_modifiers - ob.tissue_tessellate.mode = operator.mode - ob.tissue_tessellate.rotation_mode = operator.rotation_mode - ob.tissue_tessellate.merge = operator.merge - ob.tissue_tessellate.merge_thres = operator.merge_thres - ob.tissue_tessellate.scale_mode = operator.scale_mode - ob.tissue_tessellate.bool_random = operator.bool_random - ob.tissue_tessellate.random_seed = operator.random_seed - ob.tissue_tessellate.fill_mode = operator.fill_mode - ob.tissue_tessellate.bool_vertex_group = operator.bool_vertex_group - ob.tissue_tessellate.bool_selection = operator.bool_selection - ob.tissue_tessellate.bool_shapekeys = operator.bool_shapekeys - return ob + # MATERIALS + for slot in ob1.material_slots: new_ob.data.materials.append(slot.material) -class tissue_tessellate_prop(PropertyGroup): - generator: StringProperty() - component: StringProperty() - offset: FloatProperty() - zscale: FloatProperty(default=1) - merge: BoolProperty() - merge_thres: FloatProperty() - gen_modifiers: BoolProperty() - com_modifiers: BoolProperty() - mode: StringProperty() - rotation_mode: StringProperty() - scale_mode: StringProperty() - fill_mode: StringProperty() - bool_random: BoolProperty() - random_seed: IntProperty() - vertexgroup: StringProperty() - bool_vertex_group: BoolProperty() - bool_selection: BoolProperty() - bool_shapekeys: BoolProperty() + polygon_materials = [0]*n_faces1 + me1.polygons.foreach_get("material_index", polygon_materials) + polygon_materials *= n_faces + new_ob.data.polygons.foreach_set("material_index", polygon_materials) + new_ob.data.update() ### + + try: + bpy.data.objects.remove(new_ob1) + except: pass + + bpy.data.objects.remove(ob0) + bpy.data.meshes.remove(me0) + bpy.data.objects.remove(ob1) + bpy.data.meshes.remove(me1) + return new_ob class tessellate(Operator): @@ -472,179 +1715,277 @@ class tessellate(Operator): "faces, adapting the shape to the different faces") bl_options = {'REGISTER', 'UNDO'} - object_name: StringProperty( + + object_name : StringProperty( name="", description="Name of the generated object" ) - zscale: FloatProperty( + zscale : FloatProperty( name="Scale", default=1, soft_min=0, soft_max=10, description="Scale factor for the component thickness" ) - scale_mode: EnumProperty( - items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Proportional", "")), - default='CONSTANT', + scale_mode : EnumProperty( + items=( + ('CONSTANT', "Constant", "Uniform thickness"), + ('ADAPTIVE', "Proportional", "Preserve component's proportions") + ), + default='ADAPTIVE', name="Z-Scale according to faces size" ) - offset: FloatProperty( + offset : FloatProperty( name="Surface Offset", - default=0, + default=1, min=-1, max=1, soft_min=-1, soft_max=1, description="Surface offset" ) - mode: EnumProperty( - items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Adaptive", "")), - default='ADAPTIVE', + mode : EnumProperty( + items=( + ('BOUNDS', "Bounds", "The component fits automatically the size of the target face"), + ('LOCAL', "Local", "Based on Local coordinates, from 0 to 1"), + ('GLOBAL', 'Global', "Based on Global coordinates, from 0 to 1")), + default='BOUNDS', name="Component Mode" ) - rotation_mode: EnumProperty( - items=(('RANDOM', "Random", ""), - ('UV', "Active UV", ""), - ('DEFAULT', "Default", "")), + rotation_mode : EnumProperty( + items=(('RANDOM', "Random", "Random faces rotation"), + ('UV', "Active UV", "Face rotation is based on UV coordinates"), + ('DEFAULT', "Default", "Default rotation")), default='DEFAULT', name="Component Rotation" ) - fill_mode: EnumProperty( - items=(('QUAD', "Quad", ""), ('FAN', "Fan", "")), + fill_mode : EnumProperty( + items=( + ('QUAD', 'Quad', 'Regular quad tessellation. Uses only 3 or 4 vertices'), + ('FAN', 'Fan', 'Radial tessellation for polygonal faces'), + ('PATCH', 'Patch', 'Curved tessellation according to the last ' + + 'Subsurf\n(or Multires) modifiers. Works only with 4 sides ' + + 'patches.\nAfter the last Subsurf (or Multires) only ' + + 'deformation\nmodifiers can be used')), default='QUAD', name="Fill Mode" ) - gen_modifiers: BoolProperty( + combine_mode : EnumProperty( + items=( + ('LAST', 'Last', 'Show only the last iteration'), + ('UNUSED', 'Unused', 'Combine each iteration with the unused faces of the previous iteration. Used for branching systems'), + ('ALL', 'All', 'Combine the result of all iterations')), + default='LAST', + name="Combine Mode", + ) + gen_modifiers : BoolProperty( name="Generator Modifiers", default=False, - description="Apply modifiers to base object" + description="Apply Modifiers and Shape Keys to the base object" ) - com_modifiers: BoolProperty( + com_modifiers : BoolProperty( name="Component Modifiers", default=False, - description="Apply modifiers to component object" + description="Apply Modifiers and Shape Keys to the component object" ) - merge: BoolProperty( + merge : BoolProperty( name="Merge", default=False, description="Merge vertices in adjacent duplicates" ) - merge_thres: FloatProperty( + merge_thres : FloatProperty( name="Distance", default=0.001, soft_min=0, soft_max=10, description="Limit below which to merge vertices" ) - generator: StringProperty( - name="", - description="Base object for the tessellation" - ) - component: StringProperty( - name="", - description="Component object for the tessellation" - ) - bool_random: BoolProperty( + bool_random : BoolProperty( name="Randomize", default=False, description="Randomize component rotation" ) - random_seed: IntProperty( + random_seed : IntProperty( name="Seed", default=0, soft_min=0, soft_max=10, description="Random seed" ) - bool_vertex_group: BoolProperty( - name="Map Vertex Group", + bool_vertex_group : BoolProperty( + name="Map Vertex Groups", default=False, - description="Map the active " - "Vertex Group from the Base object to generated geometry" + description="Transfer all Vertex Groups from Base object" ) - bool_selection: BoolProperty( + bool_selection : BoolProperty( name="On selected Faces", default=False, description="Create Tessellation only on selected faces" ) - bool_shapekeys: BoolProperty( + bool_shapekeys : BoolProperty( name="Use Shape Keys", default=False, - description="Use component's active Shape Key according to " - "active Vertex Group of the base object" + description="Transfer Component's Shape Keys. If the name of Vertex " + "Groups and Shape Keys are the same, they will be " + "automatically combined" ) - working_on = "" - - @staticmethod - def check_gen_comp(checking): - # note pass the stored name key in here to check it out - return checking in bpy.data.objects.keys() + bool_smooth : BoolProperty( + name="Smooth Shading", + default=False, + description="Output faces with smooth shading rather than flat shaded" + ) + bool_materials : BoolProperty( + name="Transfer Materials", + default=True, + description="Preserve component's materials" + ) + generator : StringProperty( + name="", + description="Base object for the tessellation", + default = "" + ) + component : StringProperty( + name="", + description="Component object for the tessellation", + default = "" + ) + bool_material_id : BoolProperty( + name="Tessellation on Material ID", + default=False, + description="Apply the component only on the selected Material" + ) + bool_dissolve_seams : BoolProperty( + name="Dissolve Seams", + default=False, + description="Dissolve all seam edges" + ) + material_id : IntProperty( + name="Material ID", + default=0, + min=0, + description="Material ID" + ) + iterations : IntProperty( + name="Iterations", + default=1, + min=1, + soft_max=5, + description="Automatically repeat the Tessellation using the " + + "generated geometry as new base object.\nUsefull for " + + "for branching systems. Dangerous!" + ) + bool_combine : BoolProperty( + name="Combine unused", + default=False, + description="Combine the generated geometry with unused faces" + ) + bool_advanced : BoolProperty( + name="Advanced Settings", + default=False, + description="Show more settings" + ) + normals_mode : EnumProperty( + items=( + ('VERTS', 'Along Normals', 'Consistent direction based on vertices normal'), + ('FACES', 'Individual Faces', 'Based on individual faces normal')), + default='VERTS', + name="Direction" + ) + bool_multi_components : BoolProperty( + name="Multi Components", + default=False, + description="Combine different components according to materials name" + ) + bounds_x : EnumProperty( + items=( + ('EXTEND', 'Extend', 'Default X coordinates'), + ('CLIP', 'Clip', 'Trim out of bounds in X direction'), + ('CYCLIC', 'Cyclic', 'Cyclic components in X direction')), + default='EXTEND', + name="Bounds X", + ) + bounds_y : EnumProperty( + items=( + ('EXTEND', 'Extend', 'Default Y coordinates'), + ('CLIP', 'Clip', 'Trim out of bounds in Y direction'), + ('CYCLIC', 'Cyclic', 'Cyclic components in Y direction')), + default='EXTEND', + name="Bounds Y", + ) + cap_faces : BoolProperty( + name="Cap Holes", + default=False, + description="Cap open edges loops" + ) + open_edges_crease : FloatProperty( + name="Open Edges Crease", + default=0, + min=0, + max=1, + description="Automatically set crease for open edges" + ) + working_on : "" def draw(self, context): + allowed_obj = ('MESH', 'CURVE', 'SURFACE', 'FONT', 'META') + ''' try: bool_working = self.working_on == self.object_name and \ self.working_on != "" except: bool_working = False + ''' + + bool_working = False + bool_allowed = False + ob0 = None + ob1 = None sel = bpy.context.selected_objects + if len(sel) == 1: + try: + ob0 = sel[0].tissue_tessellate.generator + ob1 = sel[0].tissue_tessellate.component + self.generator = ob0.name + self.component = ob1.name + bool_working = True + bool_allowed = True + except: + pass - bool_meshes = False if len(sel) == 2: - bool_meshes = True + bool_allowed = True for o in sel: - if o.type != 'MESH': - bool_meshes = False + if o.type not in allowed_obj: + bool_allowed = False if len(sel) != 2 and not bool_working: layout = self.layout layout.label(icon='INFO') layout.label(text="Please, select two different objects") layout.label(text="Select first the Component object, then select") - layout.label(text="the Base mesh.") - elif not bool_meshes and not bool_working: + layout.label(text="the Base object.") + elif not bool_allowed and not bool_working: layout = self.layout layout.label(icon='INFO') - layout.label(text="Please, select two Mesh objects") + layout.label(text="Only Mesh, Curve, Surface or Text objects are allowed") else: - try: - ob0 = bpy.data.Objects[self.generator] - except: + if ob0 == ob1 == None: ob0 = bpy.context.active_object self.generator = ob0.name - - for o in sel: - if (o.name == ob0.name or o.type != 'MESH'): - continue - else: - ob1 = o - self.component = o.name - self.no_component = False - break - - # Checks for Tool Shelf panel, it lost the original Selection - if bpy.context.active_object.name == self.object_name: - # checks if the objects were deleted - if self.check_gen_comp( - bpy.context.active_object.tissue_tessellate.component): - ob1 = bpy.data.objects[ - bpy.context.active_object.tissue_tessellate.component - ] - self.component = ob1.name - - if self.check_gen_comp( - bpy.context.active_object.tissue_tessellate.generator): - ob0 = bpy.data.objects[ - bpy.context.active_object.tissue_tessellate.generator - ] - self.generator = ob0.name - self.no_component = False + for o in sel: + if o != ob0: + ob1 = o + self.component = o.name + self.no_component = False + break # new object name if self.object_name == "": if self.generator == "": self.object_name = "Tessellation" else: - self.object_name = self.generator + "_Tessellation" + #self.object_name = self.generator + "_Tessellation" + self.object_name = "Tessellation" layout = self.layout # Base and Component @@ -652,100 +1993,54 @@ class tessellate(Operator): row = col.row(align=True) row.label(text="BASE : " + self.generator) row.label(text="COMPONENT : " + self.component) + + # Base Modifiers row = col.row(align=True) col2 = row.column(align=True) - col2.prop(self, "gen_modifiers", text="Use Modifiers") - - if not self.check_gen_comp(self.generator) or \ - len(bpy.data.objects[self.generator].modifiers) == 0: + col2.prop(self, "gen_modifiers", text="Use Modifiers", icon='MODIFIER') + base = bpy.data.objects[self.generator] + try: + if not (base.modifiers or base.data.shape_keys): + col2.enabled = False + self.gen_modifiers = False + except: col2.enabled = False self.gen_modifiers = False - col2 = row.column(align=True) - col2.prop(self, "com_modifiers", text="Use Modifiers") - - if not self.check_gen_comp(self.component) or \ - len(bpy.data.objects[self.component].modifiers) == 0: - col2.enabled = False - self.com_modifiers = False - - # On selected faces - row = col.row(align=True) - row.prop(self, "bool_selection", text="On selected Faces") - col.separator() - - # General - col = layout.column(align=True) - col.label(text="New Object Name:") - col.prop(self, "object_name") - # Count number of faces - if self.gen_modifiers or self.com_modifiers: - depsgraph = context.evaluated_depsgraph_get() - else: - depsgraph = None + # Component Modifiers + row.separator() + col3 = row.column(align=True) + col3.prop(self, "com_modifiers", text="Use Modifiers", icon='MODIFIER') + component = bpy.data.objects[self.component] try: - polygons = 0 - me_temp_owner = None - if self.gen_modifiers: - me_temp_owner = ob0.evaluated_get(depsgraph) - me_temp = me_temp_owner.to_mesh() - else: - me_temp_owner = None - me_temp = ob0.data - - for p in me_temp.polygons: - if not self.bool_selection or p.select: - if self.fill_mode == "FAN": - polygons += len(p.vertices) - else: - polygons += 1 - - if me_temp_owner: - me_temp_owner.to_mesh_clear() - - if self.com_modifiers: - me_temp_owner = bpy.data.objects[self.component].evaluated_get(depsgraph) - me_temp = me_temp_owner.to_mesh() - else: - me_temp_owner = None - me_temp = bpy.data.objects[self.component].data - polygons *= len(me_temp.polygons) - if me_temp_owner: - me_temp_owner.to_mesh_clear() - - str_polygons = '{:0,.0f}'.format(polygons) - if polygons > 200000: - col.label(text=str_polygons + " polygons will be created!", - icon='ERROR') - else: - col.label(text=str_polygons + " faces will be created!", - icon='INFO') + if not (component.modifiers or component.data.shape_keys): + col3.enabled = False + self.com_modifiers = False except: - pass + col3.enabled = False + self.com_modifiers = False col.separator() - # Fill and Rotation row = col.row(align=True) row.label(text="Fill Mode:") - row.separator() row.label(text="Rotation:") row = col.row(align=True) - - # Fill + #col2 = row.column(align=True) row.prop( self, "fill_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) - row.separator() # Rotation - row.prop( + row.separator() + col2 = row.column(align=True) + col2.prop( self, "rotation_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if self.rotation_mode == 'RANDOM': - row = col.row(align=True) - row.prop(self, "random_seed") + col2.prop(self, "random_seed") + if self.rotation_mode == 'UV': uv_error = False if self.fill_mode == 'FAN': @@ -754,14 +2049,19 @@ class tessellate(Operator): icon='ERROR') uv_error = True - if not self.check_gen_comp(self.generator) or \ - len(bpy.data.objects[self.generator].data.uv_layers) == 0: + if ob0.type != 'MESH': row = col.row(align=True) - check_name = bpy.data.objects[self.generator].name if \ - self.check_gen_comp(self.generator) else "None" - row.label(text="'" + check_name + - "' doesn't have UV Maps", icon='ERROR') + row.label( + text="UV rotation supported only for Mesh objects", + icon='ERROR') uv_error = True + else: + if len(ob0.data.uv_layers) == 0: + row = col.row(align=True) + check_name = self.generator + row.label(text="'" + check_name + + "' doesn't have UV Maps", icon='ERROR') + uv_error = True if uv_error: row = col.row(align=True) row.label(text="Default rotation will be used instead", @@ -769,15 +2069,31 @@ class tessellate(Operator): # Component XY row = col.row(align=True) - row.label(text="Component XY:") + row.label(text="Component Coordinates:") row = col.row(align=True) row.prop( self, "mode", text="Component XY", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) + if self.mode != 'BOUNDS': + col.separator() + row = col.row(align=True) + row.label(text="X:") + row.prop( + self, "bounds_x", text="Bounds X", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + + row = col.row(align=True) + row.label(text="Y:") + row.prop( + self, "bounds_y", text="Bounds X", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + # Component Z - col.label(text="Component Z:") + col.label(text="Thickness:") row = col.row(align=True) row.prop( self, "scale_mode", text="Scale Mode", icon='NONE', expand=True, @@ -787,166 +2103,184 @@ class tessellate(Operator): self, "zscale", text="Scale", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) - col.prop( - self, "offset", text="Offset", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, + if self.mode == 'BOUNDS': + col.prop( + self, "offset", text="Offset", icon='NONE', expand=False, + slider=True, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + + # Direction + row = col.row(align=True) + row.label(text="Direction:") + row = col.row(align=True) + row.prop( + self, "normals_mode", text="Direction", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) + row.enabled = self.fill_mode != 'PATCH' # Merge col = layout.column(align=True) row = col.row(align=True) row.prop(self, "merge") - if self.merge: row.prop(self, "merge_thres") row = col.row(align=True) - # ADVANCED - col = layout.column(align=True) - col.label(text="Advanced Settings:") - # vertex group + shape keys row = col.row(align=True) - col2 = row.column(align=True) - col2.prop(self, "bool_vertex_group") - - if self.check_gen_comp(self.generator) and \ - len(bpy.data.objects[self.generator].vertex_groups) == 0: - col2.enabled = False - self.bool_vertex_group = False - - col2 = row.column(align=True) - - if not self.check_gen_comp(self.generator) or \ - not self.check_gen_comp(self.component): - return + row.prop(self, "bool_smooth") + if self.merge: + col2 = row.column(align=True) + col2.prop(self, "bool_dissolve_seams") + #if ob1.type != 'MESH': col2.enabled = False - col2.prop(self, "bool_shapekeys", text="Use Shape Keys") + row = col.row(align=True) + row.prop(self, "cap_faces") + if self.cap_faces: + col2 = row.column(align=True) + col2.prop(self, "open_edges_crease", text="Crease") - if len(bpy.data.objects[self.generator].vertex_groups) == 0 or \ - bpy.data.objects[self.component].data.shape_keys is None: - col2.enabled = False - self.bool_shapekeys = False - elif len(bpy.data.objects[self.generator].vertex_groups) == 0 or \ - bpy.data.objects[self.component].data.shape_keys is not None: - if len(bpy.data.objects[ - self.component].data.shape_keys.key_blocks) < 2: + # Advanced Settings + col = layout.column(align=True) + col.separator() + col.separator() + row = col.row(align=True) + row.prop(self, "bool_advanced", icon='SETTINGS') + if self.bool_advanced: + allow_multi = False + allow_shapekeys = not self.com_modifiers + for m in ob0.data.materials: + try: + o = bpy.data.objects[m.name] + allow_multi = True + try: + if o.data.shape_keys is None: continue + elif len(o.data.shape_keys.key_blocks) < 2: continue + else: allow_shapekeys = not self.com_modifiers + except: pass + except: pass + # DATA # + col = layout.column(align=True) + col.label(text="Morphing:") + # vertex group + shape keys + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(self, "bool_vertex_group", icon='GROUP_VERTEX') + #col2.prop_search(props, "vertex_group", props.generator, "vertex_groups") + try: + if len(ob0.vertex_groups) == 0: + col2.enabled = False + except: + col2.enabled = False + row.separator() + col2 = row.column(align=True) + row2 = col2.row(align=True) + row2.prop(self, "bool_shapekeys", text="Use Shape Keys", icon='SHAPEKEY_DATA') + row2.enabled = allow_shapekeys + + # LIMITED TESSELLATION + col = layout.column(align=True) + col.label(text="Limited Tessellation:") + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(self, "bool_multi_components", icon='MOD_TINT') + if not allow_multi: + col2.enabled = False + self.bool_multi_components = False + col.separator() + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(self, "bool_selection", text="On selected Faces", icon='RESTRICT_SELECT_OFF') + #if self.bool_material_id or self.bool_selection or self.bool_multi_components: + #col2 = row.column(align=True) + # col2.prop(self, "bool_combine") + row.separator() + if ob0.type != 'MESH': col2.enabled = False - self.bool_shapekeys = False + col2 = row.column(align=True) + col2.prop(self, "bool_material_id", icon='MATERIAL_DATA', text="Material ID") + if self.bool_material_id and not self.bool_multi_components: + #col2 = row.column(align=True) + col2.prop(self, "material_id") + col2.enabled = not self.bool_multi_components + + col.separator() + row = col.row(align=True) + row.label(text='Reiterate Tessellation:', icon='FILE_REFRESH') + row.prop(self, 'iterations', text='Repeat', icon='SETTINGS') + + col.separator() + row = col.row(align=True) + row.label(text='Combine Iterations:') + row = col.row(align=True) + row.prop( + self, "combine_mode", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) def execute(self, context): + allowed_obj = ('MESH', 'CURVE', 'META', 'SURFACE', 'FONT') try: - ob0 = bpy.context.active_object - self.generator = ob0.name + ob0 = bpy.data.objects[self.generator] + ob1 = bpy.data.objects[self.component] except: - self.report({'ERROR'}, "A Generator mesh object must be selected") - - # component object - sel = bpy.context.selected_objects - no_component = True - for o in sel: - if (o.name == ob0.name or o.type != 'MESH'): - continue - else: - ob1 = o - self.component = o.name - no_component = False - break - - # Checks for Tool Shelf panel, it lost the original Selection - if bpy.context.active_object == self.object_name: - ob1 = bpy.data.objects[ - bpy.context.active_object.tissue_tessellate.component] - self.component = ob1.name - ob0 = bpy.data.objects[ - bpy.context.active_object.tissue_tessellate.generator] - self.generator = ob0.name - no_component = False - - if no_component: - # self.report({'ERROR'}, "A component mesh object must be selected") return {'CANCELLED'} - # new object name - if self.object_name == "": - if self.generator == "": - self.object_name = "Tessellation" - else: - self.object_name = self.generator + "_Tessellation" + self.object_name = "Tessellation" + # 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 bpy.data.objects[self.component].type != 'MESH': - message = "Component must be Mesh Objects!" + if ob1.type not in allowed_obj: + message = "Component must be Mesh, Curve, Surface, Text or Meta object!" self.report({'ERROR'}, message) - self.component = "" + self.component = None - if bpy.data.objects[self.generator].type != 'MESH': - message = "Generator must be Mesh Objects!" + if ob0.type not in allowed_obj: + message = "Generator must be Mesh, Curve, Surface, Text or Meta object!" self.report({'ERROR'}, message) self.generator = "" - if self.component != "" and self.generator != "": + if True:#self.component not in ("",None) and self.generator not in ("",None): if bpy.ops.object.select_all.poll(): bpy.ops.object.select_all(action='TOGGLE') + bpy.ops.object.mode_set(mode='OBJECT') - new_ob = tassellate( - ob0, ob1, self.offset, self.zscale, self.gen_modifiers, - self.com_modifiers, self.mode, self.scale_mode, - self.rotation_mode, self.random_seed, self.fill_mode, - self.bool_vertex_group, self.bool_selection, - self.bool_shapekeys - ) - - if new_ob == 0: - message = "Zero faces selected in the Base mesh!" - self.report({'ERROR'}, message) - return {'CANCELLED'} - - new_ob.name = self.object_name - # new_ob = bpy.data.objects.new(self.object_name, new_me) - - new_ob.location = ob0.location - new_ob.matrix_world = ob0.matrix_world - - bpy.context.collection.objects.link(new_ob) - new_ob.select_set(True) - bpy.context.view_layer.objects.active = new_ob - if self.merge: - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_mode( - use_extend=False, use_expand=False, type='VERT') - bpy.ops.mesh.select_non_manifold( - extend=False, use_wire=False, use_boundary=True, - use_multi_face=False, use_non_contiguous=False, - use_verts=False) - bpy.ops.mesh.remove_doubles( - threshold=self.merge_thres, use_unselected=False) - bpy.ops.object.mode_set(mode='OBJECT') + #data0 = ob0.to_mesh(False) + #data0 = ob0.data.copy() + bool_update = False + if bpy.context.object == ob0: + auto_layer_collection() + #new_ob = bpy.data.objects.new(self.object_name, data0) + new_ob = convert_object_to_mesh(ob0,False,False) + new_ob.data.name = self.object_name + #bpy.context.collection.objects.link(new_ob) + #bpy.context.view_layer.objects.active = new_ob + new_ob.name = self.object_name + #new_ob.select_set(True) + else: + new_ob = bpy.context.object + bool_update = True new_ob = store_parameters(self, new_ob) - self.object_name = new_ob.name - self.working_on = self.object_name - - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='OBJECT') - - # MATERIALS - try: - # create materials list - polygon_materials = [p.material_index for p in ob1.data.polygons] * int( - len(new_ob.data.polygons) / len(ob1.data.polygons)) - # assign old material - component_materials = [slot.material for slot in ob1.material_slots] - for i in range(len(component_materials)): - bpy.ops.object.material_slot_add() - bpy.context.object.material_slots[i].material = \ - component_materials[i] - for i in range(len(new_ob.data.polygons)): - new_ob.data.polygons[i].material_index = polygon_materials[i] - except: - pass - - return {'FINISHED'} + try: bpy.ops.object.update_tessellate() + except RuntimeError as e: + bpy.data.objects.remove(new_ob) + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + if not bool_update: + self.object_name = new_ob.name + #self.working_on = self.object_name + new_ob.location = ob0.location + new_ob.matrix_world = ob0.matrix_world - def check(self, context): - return True + return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -963,9 +2297,10 @@ class update_tessellate(Operator): @classmethod def poll(cls, context): - try: - return context.active_object.tissue_tessellate.generator != "" and \ - context.active_object.tissue_tessellate.component != "" + #try: + try: #context.object == None: return False + return context.object.tissue_tessellate.generator != None and \ + context.object.tissue_tessellate.component != None except: return False @@ -975,7 +2310,9 @@ class update_tessellate(Operator): return checking in bpy.data.objects.keys() def execute(self, context): - ob = bpy.context.active_object + start_time = time.time() + + ob = bpy.context.object if not self.go: generator = ob.tissue_tessellate.generator component = ob.tissue_tessellate.component @@ -994,35 +2331,287 @@ class update_tessellate(Operator): bool_selection = ob.tissue_tessellate.bool_selection bool_shapekeys = ob.tissue_tessellate.bool_shapekeys mode = ob.tissue_tessellate.mode + bool_smooth = ob.tissue_tessellate.bool_smooth + bool_materials = ob.tissue_tessellate.bool_materials + bool_dissolve_seams = ob.tissue_tessellate.bool_dissolve_seams + bool_material_id = ob.tissue_tessellate.bool_material_id + material_id = ob.tissue_tessellate.material_id + iterations = ob.tissue_tessellate.iterations + bool_combine = ob.tissue_tessellate.bool_combine + normals_mode = ob.tissue_tessellate.normals_mode + bool_advanced = ob.tissue_tessellate.bool_advanced + bool_multi_components = ob.tissue_tessellate.bool_multi_components + combine_mode = ob.tissue_tessellate.combine_mode + bounds_x = ob.tissue_tessellate.bounds_x + bounds_y = ob.tissue_tessellate.bounds_y + cap_faces = ob.tissue_tessellate.cap_faces + open_edges_crease = ob.tissue_tessellate.open_edges_crease - if not self.check_gen_comp(generator) or \ - not self.check_gen_comp(component): - self.report({'ERROR'}, - "Base or Component Objects are missing from the data " - "(Most likely deleted or renamed)") - return {'CANCELLED'} - - if (generator == "" or component == ""): + try: + generator.name + component.name + except: self.report({'ERROR'}, "Active object must be Tessellate before Update") return {'CANCELLED'} - ob0 = bpy.data.objects[generator] - ob1 = bpy.data.objects[component] + # Solve Local View issues + local_spaces = [] + local_ob0 = [] + local_ob1 = [] + for area in bpy.context.screen.areas: + for space in area.spaces: + print(space) + try: + if ob.local_view_get(space): + local_spaces.append(space) + local_ob0 = ob0.local_view_get(space) + ob0.local_view_set(space, True) + local_ob1 = ob1.local_view_get(space) + ob1.local_view_set(space, True) + except: + pass + + starting_mode = bpy.context.object.mode + #if starting_mode == 'PAINT_WEIGHT': starting_mode = 'WEIGHT_PAINT' + bpy.ops.object.mode_set(mode='OBJECT') + + ob0 = generator + ob1 = component + auto_layer_collection() + + ob0_hide = ob0.hide_get() + ob0_hidev = ob0.hide_viewport + ob0_hider = ob0.hide_render + ob1_hide = ob1.hide_get() + ob1_hidev = ob1.hide_viewport + ob1_hider = ob1.hide_render + ob0.hide_set(False) + ob0.hide_viewport = False + ob0.hide_render = False + ob1.hide_set(False) + ob1.hide_viewport = False + ob1.hide_render = False + + if ob0.type == 'META': + base_ob = convert_object_to_mesh(ob0, False, True) + else: + base_ob = ob0.copy() + base_ob.data = ob0.data.copy() + bpy.context.collection.objects.link(base_ob) + + # In Blender 2.80 cache of copied objects is lost, must be re-baked + bool_update_cloth = False + for m in base_ob.modifiers: + if m.type == 'CLOTH': + m.point_cache.frame_end = bpy.context.scene.frame_current + bool_update_cloth = True + if bool_update_cloth: + bpy.ops.ptcache.free_bake_all() + bpy.ops.ptcache.bake_all() + + #new_ob.location = ob.location + #new_ob.matrix_world = ob.matrix_world + base_ob.modifiers.update() + bpy.ops.object.select_all(action='DESELECT') + iter_objects = [base_ob] + #base_ob = new_ob#.copy() + + for iter in range(iterations): + same_iteration = [] + matched_materials = [] + if bool_multi_components: mat_iter = len(base_ob.material_slots) + else: mat_iter = 1 + for m_id in range(mat_iter): + if bool_multi_components: + try: + mat = base_ob.material_slots[m_id].material + ob1 = bpy.data.objects[mat.name] + material_id = m_id + matched_materials.append(m_id) + bool_material_id = True + except: + continue + if com_modifiers: + data1 = simple_to_mesh(ob1) + else: data1 = ob1.data.copy() + n_edges1 = len(data1.edges) + + if iter != 0: gen_modifiers = True + if fill_mode == 'PATCH': + new_ob = tessellate_patch( + base_ob, ob1, offset, zscale, com_modifiers, mode, scale_mode, + rotation_mode, random_seed, bool_vertex_group, + bool_selection, bool_shapekeys, bool_material_id, material_id, + bounds_x, bounds_y + ) + else: + new_ob = tessellate_original( + base_ob, ob1, offset, zscale, gen_modifiers, + com_modifiers, mode, scale_mode, rotation_mode, + random_seed, fill_mode, bool_vertex_group, + bool_selection, bool_shapekeys, bool_material_id, + material_id, normals_mode, bounds_x, bounds_y + ) + if type(new_ob) is bpy.types.Object: + bpy.context.view_layer.objects.active = new_ob + else: + continue + n_components = int(len(new_ob.data.edges) / n_edges1) + # SELECTION + if bool_selection: + try: + # create selection list + polygon_selection = [p.select for p in ob1.data.polygons] * int( + len(new_ob.data.polygons) / len(ob1.data.polygons)) + new_ob.data.polygons.foreach_set("select", polygon_selection) + except: + pass + + if type(new_ob) == str: break + + if bool_multi_components and type(new_ob) not in (int,str): + same_iteration.append(new_ob) + new_ob.select_set(True) + bpy.context.view_layer.objects.active = new_ob + + if type(new_ob) == str: break + + #bpy.data.objects.remove(base_ob) + if bool_multi_components: + bpy.context.view_layer.update() + bpy.context.view_layer.objects.active.select_set(True) + for o in bpy.data.objects: + if o in same_iteration: + o.select_set(True) + o.location = ob.location + else: + try: + o.select_set(False) + except: pass + bpy.ops.object.join() + new_ob = bpy.context.view_layer.objects.active + new_ob.select_set(True) + new_ob.data.update() + + #try: + # combine object + if (bool_selection or bool_material_id) and combine_mode == 'UNUSED': + # remove faces from last mesh + bm = bmesh.new() + + last_mesh = iter_objects[-1].data.copy() + + bm.from_mesh(last_mesh) + bm.faces.ensure_lookup_table() + if bool_multi_components: + remove_materials = matched_materials + elif bool_material_id: + remove_materials = [material_id] + else: remove_materials = [] + if bool_selection: + remove_faces = [f for f in bm.faces if f.material_index in remove_materials and f.select] + else: + remove_faces = [f for f in bm.faces if f.material_index in remove_materials] + bmesh.ops.delete(bm, geom=remove_faces, context='FACES') + bm.to_mesh(last_mesh) + last_mesh.update() + + if len(last_mesh.vertices) > 0: + iter_objects[-1].data = last_mesh.copy() + iter_objects[-1].data.update() + else: + bpy.data.objects.remove(iter_objects[-1]) + iter_objects = iter_objects[:-1] + + base_ob = convert_object_to_mesh(new_ob,True,True) + #bpy.context.collection.objects.unlink(base_ob) + if iter < iterations-1: new_ob.data = base_ob.data - temp_ob = tassellate( - ob0, ob1, offset, zscale, gen_modifiers, com_modifiers, - mode, scale_mode, rotation_mode, random_seed, fill_mode, - bool_vertex_group, bool_selection, bool_shapekeys - ) + iter_objects.append(new_ob) + new_ob.location = ob.location + new_ob.matrix_world = ob.matrix_world + try: + bpy.data.objects.remove(bpy.data.objects['_Tessellation_Base']) + except: pass + base_ob.name = "_Tessellation_Base" + elif combine_mode == 'ALL': + base_ob = new_ob.copy() + iter_objects.append(new_ob) + new_ob.location = ob.location + new_ob.matrix_world = ob.matrix_world + else: + if base_ob != new_ob: + bpy.data.objects.remove(base_ob) + base_ob = new_ob + iter_objects = [new_ob] - if temp_ob == 0: + if new_ob == 0: + #for m, vis in zip(ob.modifiers, mod_visibility): m.show_viewport = vis message = "Zero faces selected in the Base mesh!" + bpy.ops.object.mode_set(mode=starting_mode) + self.report({'ERROR'}, message) + return {'CANCELLED'} + errors = {} + errors["modifiers_error"] = "Modifiers that change the topology of the mesh \n" \ + "after the last Subsurf (or Multires) are not allowed." + errors["topology_error"] = "Make sure that the topology of the mesh before \n" \ + "the last Subsurf (or Multires) is quads only." + errors["wires_error"] = "Please remove all wire edges in the base object." + errors["verts_error"] = "Please remove all floating vertices in the base object" + if new_ob in errors: + for o in iter_objects: bpy.data.objects.remove(o) + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + message = errors[new_ob] + ob.tissue_tessellate.error_message = message + bpy.ops.object.mode_set(mode=starting_mode) self.report({'ERROR'}, message) return {'CANCELLED'} - ob.data = temp_ob.data - bpy.data.objects.remove(temp_ob) + new_ob.location = ob.location + new_ob.matrix_world = ob.matrix_world + + ### REPEAT + if combine_mode != 'LAST' and len(iter_objects)>0: + if base_ob not in iter_objects: bpy.data.objects.remove(base_ob) + for o in iter_objects: + o.location = ob.location + o.select_set(True) + bpy.ops.object.join() + new_ob.data.update() + + # update data and preserve name + if ob.type != 'MESH': + loc, matr = ob.location, ob.matrix_world + ob = convert_object_to_mesh(ob,False,True) + ob.location, ob.matrix_world = loc, matr + data_name = ob.data.name + old_data = ob.data + ob.data = new_ob.data + bpy.data.meshes.remove(old_data) + ob.data.name = data_name + + # copy vertex group + if bool_vertex_group: + for vg in new_ob.vertex_groups: + if not vg.name in ob.vertex_groups.keys(): + ob.vertex_groups.new(name=vg.name) + new_vg = ob.vertex_groups[vg.name] + for i in range(len(ob.data.vertices)): + try: + weight = vg.weight(i) + except: + weight = 0 + new_vg.add([i], weight, 'REPLACE') + + selected_objects = [o for o in bpy.context.selected_objects] + for o in selected_objects: o.select_set(False) + + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + bpy.data.objects.remove(new_ob) + if merge: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode( @@ -1030,516 +2619,378 @@ class update_tessellate(Operator): bpy.ops.mesh.select_non_manifold( extend=False, use_wire=False, use_boundary=True, use_multi_face=False, use_non_contiguous=False, use_verts=False) + bpy.ops.mesh.remove_doubles( threshold=merge_thres, use_unselected=False) + bpy.ops.object.mode_set(mode='OBJECT') + if bool_dissolve_seams: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='EDGE') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + for e in ob.data.edges: + e.select = e.use_seam + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.dissolve_edges() + if cap_faces: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode( + use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.select_non_manifold( + extend=False, use_wire=False, use_boundary=True, + use_multi_face=False, use_non_contiguous=False, use_verts=False) + bpy.ops.mesh.edge_face_add() + if open_edges_crease != 0: + bpy.ops.transform.edge_crease(value=open_edges_crease) + bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT') - # MATERIALS - try: - # create materials list - polygon_materials = [p.material_index for p in ob1.data.polygons] * int( - len(ob.data.polygons) / len(ob1.data.polygons)) - # assign old material - component_materials = [slot.material for slot in ob1.material_slots] - for i in range(len(component_materials)): - bpy.ops.object.material_slot_add() - bpy.context.object.material_slots[i].material = \ - component_materials[i] - for i in range(len(ob.data.polygons)): - ob.data.polygons[i].material_index = polygon_materials[i] - except: - pass + if bool_smooth: bpy.ops.object.shade_smooth() + ####values = [True] * len(ob.data.polygons) + ####ob.data.polygons.foreach_set("use_smooth", values) + + #for m, vis in zip(ob.modifiers, mod_visibility): m.show_viewport = vis + + end_time = time.time() + print('Tissue: object "{}" tessellated in {:.4f} sec'.format(ob.name, end_time-start_time)) + + for mesh in bpy.data.meshes: + if not mesh.users: bpy.data.meshes.remove(mesh) + + for o in selected_objects: + try: o.select_set(True) + except: pass + + bpy.ops.object.mode_set(mode=starting_mode) + + # clean objects + for o in bpy.data.objects: + if o.name not in context.view_layer.objects and "temp" in o.name: + bpy.data.objects.remove(o) + + ob.tissue_tessellate.error_message = "" + + # Restore Base visibility + ob0.hide_set(ob0_hide) + ob0.hide_viewport = ob0_hidev + ob0.hide_render = ob0_hider + # Restore Component visibility + ob1.hide_set(ob1_hide) + ob1.hide_viewport = ob1_hidev + ob1.hide_render = ob1_hider + # Restore Local visibility + for space, local0, local1 in zip(local_spaces, local_ob0, local_ob1): + ob0.local_view_set(space, local0) + ob1.local_view_set(space, local1) + print(local_ob0) + print(local_ob1) return {'FINISHED'} def check(self, context): return True - -class settings_tessellate(Operator): - bl_idname = "object.settings_tessellate" - bl_label = "Settings" - bl_description = ("Update the tessellated mesh according to base and component changes\n" - "Allow also to change tessellation's parameters") - bl_options = {'REGISTER', 'UNDO'} - - object_name: StringProperty( - name="", - description="Name of the generated object" - ) - zscale: FloatProperty( - name="Scale", - default=1, - soft_min=0, - soft_max=10, - description="Scale factor for the component thickness" - ) - scale_mode: EnumProperty( - items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Proportional", "")), - default='ADAPTIVE', - name="Scale variation" - ) - offset: FloatProperty( - name="Surface Offset", - default=0, - min=-1, max=1, - soft_min=-1, - soft_max=1, - description="Surface offset" - ) - mode: EnumProperty( - items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Adaptive", "")), - default='ADAPTIVE', - name="Component Mode" - ) - rotation_mode: EnumProperty( - items=(('RANDOM', "Random", ""), ('UV', "Active UV", ""), - ('DEFAULT', "Default", "")), - default='DEFAULT', - name="Component Rotation" - ) - fill_mode: EnumProperty( - items=(('QUAD', "Quad", ""), ('FAN', "Fan", "")), - default='QUAD', - name="Fill Mode" - ) - gen_modifiers: BoolProperty( - name="Generator Modifiers", - default=False, - description="Apply modifiers to base object" - ) - com_modifiers: BoolProperty( - name="Component Modifiers", - default=False, - description="Apply modifiers to component object" - ) - merge: BoolProperty( - name="Merge", - default=False, - description="Merge vertices in adjacent duplicates" - ) - merge_thres: FloatProperty( - name="Distance", - default=0.001, - soft_min=0, - soft_max=10, - description="Limit below which to merge vertices" - ) - generator: StringProperty( - name="", - description="Base object for the tessellation" - ) - component: StringProperty( - name="", - description="Component object for the tessellation" - ) - bool_random: BoolProperty( - name="Randomize", - default=False, - description="Randomize component rotation" - ) - random_seed: IntProperty( - name="Seed", - default=0, - soft_min=0, - soft_max=10, - description="Random seed" - ) - bool_vertex_group: BoolProperty( - name="Map Vertex Group", - default=False, - description="Map on generated " - "geometry the active Vertex Group from the base object" - ) - bool_selection: BoolProperty( - name="On selected Faces", - default=False, - description="Create Tessellation only on select faces" - ) - bool_shapekeys: BoolProperty( - name="Use Shape Keys", - default=False, - description="Use component's active Shape Key according to active " - "Vertex Group of the base object" - ) - go = False +class TISSUE_PT_tessellate(Panel): + bl_label = "Tissue Tools" + bl_category = "Tissue" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + #bl_options = {'DEFAULT_OPEN'} @classmethod def poll(cls, context): - try: - return context.active_object.tissue_tessellate.generator != "" and \ - context.active_object.tissue_tessellate.component != "" - except: - return False - - @staticmethod - def check_gen_comp(checking): - # note pass the stored name key in here to check it out - return checking in bpy.data.objects.keys() + return context.mode in {'OBJECT', 'EDIT_MESH'} def draw(self, context): layout = self.layout - ob0 = bpy.context.active_object - - if not self.go: - self.generator = ob0.tissue_tessellate.generator - self.component = ob0.tissue_tessellate.component - self.zscale = ob0.tissue_tessellate.zscale - self.scale_mode = ob0.tissue_tessellate.scale_mode - self.rotation_mode = ob0.tissue_tessellate.rotation_mode - self.offset = ob0.tissue_tessellate.offset - self.merge = ob0.tissue_tessellate.merge - self.merge_thres = ob0.tissue_tessellate.merge_thres - self.gen_modifiers = ob0.tissue_tessellate.gen_modifiers - self.com_modifiers = ob0.tissue_tessellate.com_modifiers - self.bool_random = ob0.tissue_tessellate.bool_random - self.random_seed = ob0.tissue_tessellate.random_seed - self.fill_mode = ob0.tissue_tessellate.fill_mode - self.bool_vertex_group = ob0.tissue_tessellate.bool_vertex_group - self.bool_selection = ob0.tissue_tessellate.bool_selection - self.bool_shapekeys = ob0.tissue_tessellate.bool_shapekeys - self.mode = ob0.tissue_tessellate.mode - - # start drawing - layout = self.layout - # check for keys in data - as the objects can be deleted or renamed - if not self.check_gen_comp(self.generator) or \ - not self.check_gen_comp(self.component): - - layout.label(text="Base or Component Objects are missing from the data", - icon="INFO") - layout.label(text="(Most likely deleted or renamed)", - icon="BLANK1") - layout.label(text="Settings could not be altered anymore", - icon="BLANK1") - layout.label(text="Please re-run Tessellate with two new selected objects", - icon="BLANK1") - return - - # ob0 = bpy.context.active_object - # Base and Component col = layout.column(align=True) - row = col.row(align=True) - row.label(text="BASE :") - row.label(text="COMPONENT :") - row = col.row(align=True) - - col2 = row.column(align=True) - col2.prop_search(self, "generator", bpy.data, "objects") - row.separator() - col2 = row.column(align=True) - col2.prop_search(self, "component", bpy.data, "objects") - - row = col.row(align=True) - col2 = row.column(align=True) - col2.prop(self, "gen_modifiers", text="Use Modifiers") - - if len(bpy.data.objects[self.generator].modifiers) == 0: - col2.enabled = False - self.gen_modifiers = False - - col2 = row.column(align=True) - col2.prop(self, "com_modifiers", text="Use Modifiers") - - if len(bpy.data.objects[self.component].modifiers) == 0: - col2.enabled = False - self.com_modifiers = False - - # On selected faces - row = col.row(align=True) - row.prop(self, "bool_selection", text="On selected Faces") + col.label(text="Tessellate:") + col.operator("object.tessellate") + col.operator("object.dual_mesh_tessellated") col.separator() + #col = layout.column(align=True) + #col.label(text="Tessellate Edit:") + #col.operator("object.settings_tessellate") + col.operator("object.update_tessellate", icon='FILE_REFRESH') - if self.gen_modifiers or self.com_modifiers: - depsgraph = context.evaluated_depsgraph_get() - - # Count number of faces - try: - polygons = 0 - me_temp_owner = None - if self.gen_modifiers: - me_temp_owner = bpy.data.objects[self.generator].evaluated_get(depsgraph) - me_temp = me_temp_owner.to_mesh() - else: - me_temp_owner = None - me_temp = bpy.data.objects[self.generator].data - - for p in me_temp.polygons: - if not self.bool_selection or p.select: - if self.fill_mode == "FAN": - polygons += len(p.vertices) - else: - polygons += 1 - - if me_temp_owner: - me_temp_owner.to_mesh_clear() + #col = layout.column(align=True) + col.operator("mesh.rotate_face", icon='NDOF_TURN') - if self.com_modifiers: - me_temp_owner = bpy.data.objects[self.component].evaluated_get(depsgraph) - me_temp = me_temp_owner.to_mesh() - else: - me_temp_owner = None - me_temp = bpy.data.objects[self.component].data - polygons *= len(me_temp.polygons) - - if me_temp_owner: - me_temp_owner.to_mesh_clear() - - str_polygons = '{:0,.0f}'.format(polygons) - if polygons > 200000: - col.label(text=str_polygons + " polygons will be created!", - icon='ERROR') - else: - col.label(text=str_polygons + " faces will be created!", - icon='INFO') - except: - pass col.separator() + col.label(text="Other:") + col.operator("object.dual_mesh") + col.operator("object.lattice_along_surface", icon="OUTLINER_OB_LATTICE") - # Fill and Rotation - row = col.row(align=True) - row.label(text="Fill Mode:") - row.separator() - row.label(text="Rotation:") - row = col.row(align=True) - - # fill - row.prop(self, "fill_mode", text="", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - row.separator() - - # rotation - row.prop(self, "rotation_mode", text="", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - - if self.rotation_mode == 'RANDOM': - row = col.row(align=True) - row.prop(self, "random_seed") - - if self.rotation_mode == 'UV': - uv_error = False - if self.fill_mode == 'FAN': - row = col.row(align=True) - row.label(text="UV rotation doesn't work in FAN mode", - icon='ERROR') - uv_error = True - - if len(bpy.data.objects[self.generator].data.uv_layers) == 0: - row = col.row(align=True) - row.label(text="'" + bpy.data.objects[self.generator].name + - " doesn't have UV Maps", icon='ERROR') - uv_error = True - if uv_error: - row = col.row(align=True) - row.label(text="Default rotation will be used instead", - icon='INFO') - - # component XY - row = col.row(align=True) - row.label(text="Component XY:") - row = col.row(align=True) - row.prop(self, "mode", text="Component XY", icon='NONE', expand=True, - slider=False, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - - # component Z - col.label(text="Component Z:") - row = col.row(align=True) - row.prop(self, "scale_mode", text="Scale Mode", icon='NONE', - expand=True, slider=False, toggle=False, icon_only=False, - event=False, full_event=False, emboss=True, index=-1) - col.prop(self, "zscale", text="Scale", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - col.prop(self, "offset", text="Offset", icon='NONE', expand=False, - slider=True, toggle=False, icon_only=False, event=False, - full_event=False, emboss=True, index=-1) - - # merge - col = layout.column(align=True) - row = col.row(align=True) - row.prop(self, "merge") - if self.merge: - row.prop(self, "merge_thres") - row = col.row(align=True) - - # ADVANCED # - col = layout.column(align=True) - tessellate.rotation_mode - - col.label(text="Advanced Settings:") - # vertex group + shape keys - row = col.row(align=True) - col2 = row.column(align=True) - col2.prop(self, "bool_vertex_group") - - if len(bpy.data.objects[self.generator].vertex_groups) == 0: - col2.enabled = False - self.bool_vertex_group = False - col2 = row.column(align=True) - col2.prop(self, "bool_shapekeys", text="Use Shape Keys") - - if len(bpy.data.objects[self.generator].vertex_groups) == 0 or \ - bpy.data.objects[self.component].data.shape_keys is None: - col2.enabled = False - self.bool_shapekeys = False - elif len(bpy.data.objects[self.generator].vertex_groups) == 0 or \ - bpy.data.objects[self.component].data.shape_keys is not None: - if len(bpy.data.objects[self.component].data.shape_keys.key_blocks) < 2: - col2.enabled = False - self.bool_shapekeys = False - self.go = True - - def execute(self, context): - self.ob = bpy.context.active_object - - if not self.go: - self.generator = self.ob.tissue_tessellate.generator - self.component = self.ob.tissue_tessellate.component - self.zscale = self.ob.tissue_tessellate.zscale - self.scale_mode = self.ob.tissue_tessellate.scale_mode - self.rotation_mode = self.ob.tissue_tessellate.rotation_mode - self.offset = self.ob.tissue_tessellate.offset - self.merge = self.ob.tissue_tessellate.merge - self.merge_thres = self.ob.tissue_tessellate.merge_thres - self.gen_modifiers = self.ob.tissue_tessellate.gen_modifiers - self.com_modifiers = self.ob.tissue_tessellate.com_modifiers - self.bool_random = self.ob.tissue_tessellate.bool_random - self.random_seed = self.ob.tissue_tessellate.random_seed - self.fill_mode = self.ob.tissue_tessellate.fill_mode - self.bool_vertex_group = self.ob.tissue_tessellate.bool_vertex_group - self.bool_selection = self.ob.tissue_tessellate.bool_selection - self.bool_shapekeys = self.ob.tissue_tessellate.bool_shapekeys - - if not self.check_gen_comp(self.generator) or \ - not self.check_gen_comp(self.component): - # do nothing as the Warning was already done in it UI - return {'CANCELLED'} - - if (self.generator == "" or self.component == ""): - self.report({'ERROR'}, - "Active object must be Tessellated before Update") - return {'CANCELLED'} - - if (bpy.data.objects[self.generator].type != 'MESH'): - self.report({'ERROR'}, "Base object must be a Mesh") - return {'CANCELLED'} - - if (bpy.data.objects[self.component].type != 'MESH'): - self.report({'ERROR'}, "Component object must be a Mesh") - return {'CANCELLED'} - - ob0 = bpy.data.objects[self.generator] - ob1 = bpy.data.objects[self.component] - - temp_ob = tassellate( - ob0, ob1, self.offset, self.zscale, self.gen_modifiers, - self.com_modifiers, self.mode, self.scale_mode, self.rotation_mode, - self.random_seed, self.fill_mode, self.bool_vertex_group, - self.bool_selection, self.bool_shapekeys - ) + act = context.active_object + if act and act.type == 'MESH': + col.operator("object.uv_to_mesh", icon="UV") - if temp_ob == 0: - message = "Zero faces selected in the Base mesh" - self.report({'ERROR'}, message) - return {'CANCELLED'} +class TISSUE_PT_tessellate_object(Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" + bl_label = "Tissue - Tessellate" + bl_options = {'DEFAULT_CLOSED'} - # Transfer mesh data - self.ob.data = temp_ob.data + @classmethod + def poll(cls, context): + try: return context.object.type == 'MESH' + except: return False - # Create object in order to transfer vertex group - bpy.context.collection.objects.link(temp_ob) - temp_ob.select_set(True) - bpy.context.view_layer.objects.active = temp_ob + def draw(self, context): + ob = context.object + props = ob.tissue_tessellate + allowed_obj = ('MESH','CURVE','SURFACE','FONT', 'META') try: - bpy.ops.object.vertex_group_copy_to_linked() - except: - pass - - bpy.context.scene.objects.unlink(temp_ob) - bpy.data.objects.remove(temp_ob) - bpy.context.view_layer.objects.active = self.ob - - if self.merge: - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_mode( - use_extend=False, use_expand=False, type='VERT') - bpy.ops.mesh.select_non_manifold( - extend=False, use_wire=False, use_boundary=True, - use_multi_face=False, use_non_contiguous=False, use_verts=False) - bpy.ops.mesh.remove_doubles( - threshold=self.merge_thres, use_unselected=False) - bpy.ops.object.mode_set(mode='OBJECT') - self.ob = store_parameters(self, self.ob) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='OBJECT') + bool_tessellated = props.generator or props.component != None + ob0 = props.generator + ob1 = props.component + except: bool_tessellated = False + layout = self.layout + if not bool_tessellated: + layout.label(text="The selected object is not a Tessellated 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) + row.prop(props, "bool_run", text="Animatable") + row.operator("object.update_tessellate", icon='FILE_REFRESH') - # MATERIALS - try: - # create materials list - polygon_materials = [p.material_index for p in ob1.data.polygons] * \ - int(len(self.ob.data.polygons) / len(ob1.data.polygons)) - # assign old material - component_materials = [slot.material for slot in ob1.material_slots] - for i in range(len(component_materials)): - bpy.ops.object.material_slot_add() - bpy.context.object.material_slots[i].material = \ - component_materials[i] - for i in range(len(self.ob.data.polygons)): - self.ob.data.polygons[i].material_index = polygon_materials[i] - except: - pass + col = layout.column(align=True) + row = col.row(align=True) + row.label(text="BASE :") + row.label(text="COMPONENT :") + row = col.row(align=True) - return {'FINISHED'} + col2 = row.column(align=True) + col2.prop_search(props, "generator", context.scene, "objects") + row.separator() + col2 = row.column(align=True) + col2.prop_search(props, "component", context.scene, "objects") + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(props, "gen_modifiers", text="Use Modifiers", icon='MODIFIER') + row.separator() + try: + if not (ob0.modifiers or ob0.data.shape_keys) or props.fill_mode == 'PATCH': + col2.enabled = False + except: + col2.enabled = False + col2 = row.column(align=True) + col2.prop(props, "com_modifiers", text="Use Modifiers", icon='MODIFIER') + try: + if not (props.component.modifiers or props.component.data.shape_keys): + col2.enabled = False + except: + col2.enabled = False + col.separator() - def check(self, context): - return True + # Fill and Rotation + row = col.row(align=True) + row.label(text="Fill Mode:") + row.separator() + row.label(text="Rotation:") + row = col.row(align=True) - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width=400) + # fill + row.prop(props, "fill_mode", text="", icon='NONE', expand=False, + slider=True, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + row.separator() + # rotation + col2 = row.column(align=True) + col2.prop(props, "rotation_mode", text="", icon='NONE', expand=False, + slider=True, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) -class tessellate_panel(Panel): - bl_label = "Tissue" - bl_category = "Create" - bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" - bl_options = {'DEFAULT_CLOSED'} + if props.rotation_mode == 'RANDOM': + #row = col.row(align=True) + col2.prop(props, "random_seed") - @classmethod - def poll(cls, context): - return context.mode in {'OBJECT', 'EDIT_MESH'} + if props.rotation_mode == 'UV': + uv_error = False + if props.fill_mode == 'FAN': + row = col.row(align=True) + row.label(text="UV rotation doesn't work in FAN mode", + icon='ERROR') + uv_error = True + if props.generator.type != 'MESH': + row = col.row(align=True) + row.label( + text="UV rotation supported only for Mesh objects", + icon='ERROR') + uv_error = True + else: + if len(props.generator.data.uv_layers) == 0: + row = col.row(align=True) + row.label(text="'" + props.generator.name + + " doesn't have UV Maps", icon='ERROR') + uv_error = True + if uv_error: + row = col.row(align=True) + row.label(text="Default rotation will be used instead", + icon='INFO') - def draw(self, context): - layout = self.layout + # component XY + row = col.row(align=True) + row.label(text="Component Coordinates:") + row = col.row(align=True) + row.prop(props, "mode", expand=True) - col = layout.column(align=True) - col.label(text="Tessellate Add:") - col.operator("object.tessellate") + if props.mode != 'BOUNDS': + col.separator() + row = col.row(align=True) + row.label(text="X:") + row.prop( + props, "bounds_x", text="Bounds X", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) - col = layout.column(align=True) - col.label(text="Tessellate Edit:") - col.operator("object.settings_tessellate") - col.operator("object.update_tessellate") + row = col.row(align=True) + row.label(text="Y:") + row.prop( + props, "bounds_y", text="Bounds X", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + + # component Z + col.label(text="Thickness:") + row = col.row(align=True) + row.prop(props, "scale_mode", expand=True) + col.prop(props, "zscale", text="Scale", icon='NONE', expand=False, + slider=True, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + if props.mode == 'BOUNDS': + col.prop(props, "offset", text="Offset", icon='NONE', expand=False, + slider=True, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + + # Direction + row = col.row(align=True) + row.label(text="Direction:") + row = col.row(align=True) + row.prop( + props, "normals_mode", text="Direction", icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) + row.enabled = props.fill_mode != 'PATCH' - col = layout.column(align=True) - col.operator("mesh.rotate_face") + # merge + col = layout.column(align=True) + row = col.row(align=True) + row.prop(props, "merge") + if props.merge: + row.prop(props, "merge_thres") + row = col.row(align=True) + row.prop(props, "bool_smooth") + if props.merge: + col2 = row.column(align=True) + col2.prop(props, "bool_dissolve_seams") + #if props.component.type != 'MESH': col2.enabled = False - col.separator() - col.label(text="Other:") - col.operator("object.lattice_along_surface", icon="OUTLINER_OB_LATTICE") + row = col.row(align=True) + row.prop(props, "cap_faces") + if props.cap_faces: + col2 = row.column(align=True) + col2.prop(props, "open_edges_crease", text="Crease") - act = context.active_object - if act and act.type == 'MESH': - col.operator("object.uv_to_mesh", icon="GROUP_UVS") + # Advanced Settings + col = layout.column(align=True) + col.separator() + col.separator() + row = col.row(align=True) + row.prop(props, "bool_advanced", icon='SETTINGS') + if props.bool_advanced: + allow_multi = False + allow_shapekeys = not props.com_modifiers + for m in ob0.data.materials: + try: + o = bpy.data.objects[m.name] + allow_multi = True + try: + if o.data.shape_keys is None: continue + elif len(o.data.shape_keys.key_blocks) < 2: continue + else: allow_shapekeys = not props.com_modifiers + except: pass + except: pass + # DATA # + col = layout.column(align=True) + col.label(text="Morphing:") + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(props, "bool_vertex_group", icon='GROUP_VERTEX') + #col2.prop_search(props, "vertex_group", props.generator, "vertex_groups") + try: + if len(props.generator.vertex_groups) == 0: + col2.enabled = False + except: + col2.enabled = False + row.separator() + col2 = row.column(align=True) + row2 = col2.row(align=True) + row2.prop(props, "bool_shapekeys", text="Use Shape Keys", icon='SHAPEKEY_DATA') + row2.enabled = allow_shapekeys + + # LIMITED TESSELLATION + col = layout.column(align=True) + col.label(text="Limited Tessellation:") + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(props, "bool_multi_components", icon='MOD_TINT') + if not allow_multi: + col2.enabled = False + col.separator() + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(props, "bool_selection", text="On selected Faces", icon='RESTRICT_SELECT_OFF') + #if props.bool_material_id or props.bool_selection or props.bool_multi_components: + #col2 = row.column(align=True) + # col2.prop(props, "bool_combine") + row.separator() + if props.generator.type != 'MESH': + col2.enabled = False + col2 = row.column(align=True) + col2.prop(props, "bool_material_id", icon='MATERIAL_DATA', text="Material ID") + if props.bool_material_id and not props.bool_multi_components: + #col2 = row.column(align=True) + col2.prop(props, "material_id") + if props.bool_multi_components: + col2.enabled = False + # TRANFER DATA ### OFF + if props.fill_mode != 'PATCH' and False: + col = layout.column(align=True) + col.label(text="Component Data:") + row = col.row(align=True) + col2 = row.column(align=True) + col2.prop(props, "bool_materials", icon='MATERIAL_DATA') + row.separator() + col2 = row.column(align=True) + if props.fill_mode == 'PATCH': + col.enabled = False + col.label(text='Not needed in Patch mode', icon='INFO') + + col.separator() + row = col.row(align=True) + row.label(text='Reiterate Tessellation:', icon='FILE_REFRESH') + row.prop(props, 'iterations', text='Repeat', icon='SETTINGS') + col.separator() + row = col.row(align=True) + row.label(text='Combine Iterations:') + row = col.row(align=True) + row.prop( + props, "combine_mode", text="Combine:",icon='NONE', expand=True, + slider=False, toggle=False, icon_only=False, event=False, + full_event=False, emboss=True, index=-1) class rotate_face(Operator): bl_idname = "mesh.rotate_face" @@ -1554,45 +3005,35 @@ class rotate_face(Operator): def execute(self, context): ob = bpy.context.active_object me = ob.data - bpy.ops.object.mode_set(mode='OBJECT') - for p in [f for f in me.polygons if f.select]: - p.vertices = p.vertices[1:] + p.vertices[:1] + bm = bmesh.from_edit_mesh(me) + mesh_select_mode = [sm for sm in context.tool_settings.mesh_select_mode] - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.flip_normals() - bpy.ops.mesh.flip_normals() - me.update(calc_edges=True) + for face in bm.faces: + if (face.select): + vs = face.verts[:] + vs2 = vs[1:]+vs[:1] + material_index = face.material_index + bm.faces.remove(face) + f2 = bm.faces.new(vs2) + f2.select = True + f2.material_index = material_index + bm.normal_update() + + # trigger UI update + bmesh.update_edit_mesh(me) + ob.select_set(False) # update tessellated meshes bpy.ops.object.mode_set(mode='OBJECT') for o in [obj for obj in bpy.data.objects if - obj.tissue_tessellate.generator == ob.name]: + obj.tissue_tessellate.generator == ob and obj.visible_get()]: bpy.context.view_layer.objects.active = o bpy.ops.object.update_tessellate() + o.select_set(False) + ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') + context.tool_settings.mesh_select_mode = mesh_select_mode return {'FINISHED'} - - -def register(): - bpy.utils.register_class(tissue_tessellate_prop) - bpy.utils.register_class(tessellate) - bpy.utils.register_class(update_tessellate) - bpy.utils.register_class(settings_tessellate) - bpy.utils.register_class(tessellate_panel) - bpy.utils.register_class(rotate_face) - - -def unregister(): - bpy.utils.unregister_class(tissue_tessellate_prop) - bpy.utils.unregister_class(tessellate) - bpy.utils.unregister_class(update_tessellate) - bpy.utils.unregister_class(settings_tessellate) - bpy.utils.unregister_class(tessellate_panel) - bpy.utils.unregister_class(rotate_face) - - -if __name__ == "__main__": - register() diff --git a/mesh_tissue/utils.py b/mesh_tissue/utils.py new file mode 100644 index 00000000..e9aa894b --- /dev/null +++ b/mesh_tissue/utils.py @@ -0,0 +1,110 @@ +import bpy + +#Recursivly transverse layer_collection for a particular name +def recurLayerCollection(layerColl, collName): + found = None + if (layerColl.name == collName): + return layerColl + for layer in layerColl.children: + found = recurLayerCollection(layer, collName) + if found: + return found + +def auto_layer_collection(): + # automatically change active layer collection + layer = bpy.context.view_layer.active_layer_collection + layer_collection = bpy.context.view_layer.layer_collection + if layer.hide_viewport or layer.collection.hide_viewport: + collections = bpy.context.object.users_collection + for c in collections: + lc = recurLayerCollection(layer_collection, c.name) + if not c.hide_viewport and not lc.hide_viewport: + bpy.context.view_layer.active_layer_collection = lc + +def lerp(a, b, t): + return a + (b - a) * t + +def _lerp2(v1, v2, v3, v4, v): + v12 = v1.lerp(v2,v.x) # + (v2 - v1) * v.x + v34 = v3.lerp(v4,v.x) # + (v4 - v3) * v.x + return v12.lerp(v34, v.y)# + (v34 - v12) * v.y + +def lerp2(v1, v2, v3, v4, v): + v12 = v1 + (v2 - v1) * v.x + v34 = v3 + (v4 - v3) * v.x + return v12 + (v34 - v12) * v.y + +def lerp3(v1, v2, v3, v4, v): + loc = lerp2(v1.co, v2.co, v3.co, v4.co, v) + nor = lerp2(v1.normal, v2.normal, v3.normal, v4.normal, v) + nor.normalize() + return loc + nor * v.z + +def _convert_object_to_mesh(ob, apply_modifiers=True, preserve_status=True): + if not apply_modifiers: + mod_visibility = [m.show_viewport for m in ob.modifiers] + for m in ob.modifiers: + m.show_viewport = False + if preserve_status: + # store status + mode = bpy.context.object.mode + selected = bpy.context.selected_objects + active = bpy.context.object + # change status + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + new_ob = ob.copy() + new_ob.data = ob.data.copy() + bpy.context.collection.objects.link(new_ob) + bpy.context.view_layer.objects.active = new_ob + new_ob.select_set(True) + bpy.ops.object.convert(target='MESH') + if preserve_status: + # restore status + bpy.ops.object.select_all(action='DESELECT') + for o in selected: o.select_set(True) + bpy.context.view_layer.objects.active = active + bpy.ops.object.mode_set(mode=mode) + if not apply_modifiers: + for m,vis in zip(ob.modifiers,mod_visibility): + m.show_viewport = vis + return new_ob + +def convert_object_to_mesh(ob, apply_modifiers=True, preserve_status=True): + if not ob.name: return None + if ob.type != 'MESH': + if not apply_modifiers: + mod_visibility = [m.show_viewport for m in ob.modifiers] + for m in ob.modifiers: m.show_viewport = False + #ob.modifiers.update() + #dg = bpy.context.evaluated_depsgraph_get() + #ob_eval = ob.evaluated_get(dg) + #me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg) + me = simple_to_mesh(ob) + new_ob = bpy.data.objects.new(ob.data.name, me) + new_ob.location, new_ob.matrix_world = ob.location, ob.matrix_world + if not apply_modifiers: + for m,vis in zip(ob.modifiers,mod_visibility): m.show_viewport = vis + else: + if apply_modifiers: + new_ob = ob.copy() + new_ob.data = simple_to_mesh(ob) + else: + new_ob = ob.copy() + new_ob.data = ob.data.copy() + new_ob.modifiers.clear() + bpy.context.collection.objects.link(new_ob) + if preserve_status: + new_ob.select_set(False) + else: + for o in bpy.context.view_layer.objects: o.select_set(False) + new_ob.select_set(True) + bpy.context.view_layer.objects.active = new_ob + return new_ob + +def simple_to_mesh(ob): + dg = bpy.context.evaluated_depsgraph_get() + ob_eval = ob.evaluated_get(dg) + me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg) + me.calc_normals() + return me diff --git a/mesh_tissue/uv_to_mesh.py b/mesh_tissue/uv_to_mesh.py index bccee0ba..87179a92 100644 --- a/mesh_tissue/uv_to_mesh.py +++ b/mesh_tissue/uv_to_mesh.py @@ -32,7 +32,7 @@ bl_info = { "name": "UV to Mesh", "author": "Alessandro Zomparelli (Co-de-iT)", "version": (0, 1, 1), - "blender": (2, 79, 0), + "blender": (2, 7, 9), "location": "", "description": "Create a new Mesh based on active UV", "warning": "", @@ -53,22 +53,22 @@ class uv_to_mesh(Operator): bl_description = ("Create a new Mesh based on active UV") bl_options = {'REGISTER', 'UNDO'} - apply_modifiers: BoolProperty( + apply_modifiers : BoolProperty( name="Apply Modifiers", default=False, description="Apply object's modifiers" ) - vertex_groups: BoolProperty( + vertex_groups : BoolProperty( name="Keep Vertex Groups", default=False, description="Transfer all the Vertex Groups" ) - materials: BoolProperty( + materials : BoolProperty( name="Keep Materials", default=True, description="Transfer all the Materials" ) - auto_scale: BoolProperty( + auto_scale : BoolProperty( name="Resize", default=True, description="Scale the new object in order to preserve the average surface area" @@ -76,7 +76,7 @@ class uv_to_mesh(Operator): def execute(self, context): bpy.ops.object.mode_set(mode='OBJECT') - for o in bpy.data.objects: + for o in bpy.data.objects and bpy.context.view_layer.objects: o.select_set(False) bpy.context.object.select_set(True) @@ -85,11 +85,7 @@ class uv_to_mesh(Operator): bpy.ops.object.convert(target='MESH') ob0 = bpy.context.object - if self.apply_modifiers: - depsgraph = context.evaluated_depsgraph_get() - me0 = bpy.data.meshes.new_from_object(ob0.evaluated_get(depsgraph)) - else: - me0 = bpy.data.new_from_meshed(ob0) + me0 = ob0.to_mesh(bpy.context.depsgraph, apply_modifiers=self.apply_modifiers) area = 0 verts = [] @@ -122,8 +118,8 @@ class uv_to_mesh(Operator): # Link object to scene and make active scn = bpy.context.scene - scn.objects.link(ob) - scn.objects.active = ob + bpy.context.collection.objects.link(ob) + bpy.context.view_layer.objects.active = ob ob.select_set(True) # Create mesh from given verts, faces. @@ -144,7 +140,7 @@ class uv_to_mesh(Operator): try: for group in ob0.vertex_groups: index = group.index - ob.vertex_groups.new(name=group.name) + ob.vertex_groups.new(group.name) for p in me0.polygons: for vert, loop in zip(p.vertices, p.loop_indices): ob.vertex_groups[index].add([loop], group.weight(vert), "ADD") @@ -183,15 +179,3 @@ class uv_to_mesh(Operator): bpy.context.view_layer.objects.active = ob return {'FINISHED'} - - -def register(): - bpy.utils.register_class(uv_to_mesh) - - -def unregister(): - bpy.utils.unregister_class(uv_to_mesh) - - -if __name__ == "__main__": - register() -- cgit v1.2.3