# SPDX-License-Identifier: GPL-2.0-or-later # --------------------------- LATTICE ALONG SURFACE -------------------------- # # -------------------------------- version 0.3 ------------------------------- # # # # Automatically generate and assign a lattice that follows the active surface. # # # # (c) Alessandro Zomparelli # # (2017) # # # # http://www.co-de-it.com/ # # # # ############################################################################ # import bpy import bmesh from bpy.types import Operator from bpy.props import (BoolProperty, StringProperty, FloatProperty) from mathutils import Vector from .utils import * def not_in(element, grid): output = True for loop in grid: if element in loop: output = False break return output def grid_from_mesh(mesh, swap_uv): bm = bmesh.new() bm.from_mesh(mesh) verts_grid = [] edges_grid = [] faces_grid = [] running_grid = True while running_grid: verts_loop = [] edges_loop = [] faces_loop = [] # storing first point verts_candidates = [] if len(faces_grid) == 0: # for first loop check all vertices verts_candidates = bm.verts else: # for other loops start form the vertices of the first face # the last loop, skipping already used vertices verts_candidates = [v for v in bm.faces[faces_grid[-1][0]].verts if not_in(v.index, verts_grid)] # check for last loop is_last = False for vert in verts_candidates: if len(vert.link_faces) == 1: # check if corner vertex vert.select = True verts_loop.append(vert.index) is_last = True break if not is_last: for vert in verts_candidates: new_link_faces = [f for f in vert.link_faces if not_in(f.index, faces_grid)] if len(new_link_faces) < 2: # check if corner vertex vert.select = True verts_loop.append(vert.index) break running_loop = len(verts_loop) > 0 while running_loop: bm.verts.ensure_lookup_table() id = verts_loop[-1] link_edges = bm.verts[id].link_edges # storing second point if len(verts_loop) == 1: # only one vertex stored in the loop if len(faces_grid) == 0: # first loop # edge = link_edges[swap_uv] # chose direction for vert in edge.verts: if vert.index != id: vert.select = True verts_loop.append(vert.index) # new vertex edges_loop.append(edge.index) # chosen edge faces_loop.append(edge.link_faces[0].index) # only one face # edge.link_faces[0].select = True else: # other loops # # start from the edges of the first face of the last loop for edge in bm.faces[faces_grid[-1][0]].edges: # chose an edge starting from the first vertex that is not returning back if bm.verts[verts_loop[0]] in edge.verts and \ bm.verts[verts_grid[-1][0]] not in edge.verts: for vert in edge.verts: if vert.index != id: vert.select = True verts_loop.append(vert.index) edges_loop.append(edge.index) for face in edge.link_faces: if not_in(face.index, faces_grid): faces_loop.append(face.index) # continuing the loop else: for edge in link_edges: for vert in edge.verts: store_data = False if not_in(vert.index, verts_grid) and vert.index not in verts_loop: if len(faces_loop) > 0: bm.faces.ensure_lookup_table() if vert not in bm.faces[faces_loop[-1]].verts: store_data = True else: store_data = True if store_data: vert.select = True verts_loop.append(vert.index) edges_loop.append(edge.index) for face in edge.link_faces: if not_in(face.index, faces_grid): faces_loop.append(face.index) break # ending condition if verts_loop[-1] == id or verts_loop[-1] == verts_loop[0]: running_loop = False verts_grid.append(verts_loop) edges_grid.append(edges_loop) faces_grid.append(faces_loop) if len(faces_loop) == 0: running_grid = False bm.free() return verts_grid, edges_grid, faces_grid class lattice_along_surface(Operator): bl_idname = "object.lattice_along_surface" bl_label = "Lattice along Surface" bl_description = ("Automatically add a Lattice modifier to the selected " "object, adapting it to the active one.\nThe active " "object must be a rectangular grid compatible with the " "Lattice's topology") bl_options = {'REGISTER', 'UNDO'} set_parent : BoolProperty( name="Set Parent", default=True, description="Automatically set the Lattice as parent" ) flipNormals : BoolProperty( name="Flip Normals", default=False, description="Flip normals direction" ) swapUV : BoolProperty( name="Swap UV", default=False, description="Flip grid's U and V" ) flipU : BoolProperty( name="Flip U", default=False, description="Flip grid's U") flipV : BoolProperty( name="Flip V", default=False, description="Flip grid's V" ) flipW : BoolProperty( name="Flip W", default=False, description="Flip grid's W" ) use_groups : BoolProperty( name="Vertex Group", default=False, description="Use active Vertex Group for lattice's thickness" ) 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( name="Hide Lattice", default=True, description="Automatically hide the Lattice object" ) scale_x : FloatProperty( name="Scale X", default=1, min=0.001, max=1, description="Object scale" ) scale_y : FloatProperty( name="Scale Y", default=1, min=0.001, max=1, description="Object scale" ) scale_z : FloatProperty( name="Scale Z", default=1, min=0.001, max=1, description="Object scale" ) thickness : FloatProperty( name="Thickness", default=1, soft_min=0, soft_max=5, description="Lattice thickness" ) displace : FloatProperty( name="Displace", default=0, soft_min=-1, soft_max=1, description="Lattice displace" ) weight_factor : FloatProperty( name="Factor", default=0, min=0.000, max=1.000, precision=3, description="Thickness factor to use for zero vertex group influence" ) grid_object = "" source_object = "" @classmethod def poll(cls, context): try: return context.object.mode == 'OBJECT' except: return False def draw(self, context): layout = self.layout col = layout.column(align=True) col.label(text="Thickness:") col.prop( self, "thickness", text="Thickness", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1 ) col.prop( self, "displace", text="Offset", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1 ) row = col.row() row.prop(self, "use_groups") if self.use_groups: row = col.row() row.prop(self, "weight_factor") col.separator() col.label(text="Scale:") col.prop( self, "scale_x", text="U", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1 ) col.prop( self, "scale_y", text="V", 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() row.prop(self, "flipU", text="U") row.prop(self, "flipV", text="V") row.prop(self, "flipW", text="W") col.prop(self, "swapUV") col.prop(self, "flipNormals") col.separator() col.label(text="Lattice Options:") col.prop(self, "high_quality_lattice") col.prop(self, "hide_lattice") col.prop(self, "set_parent") def execute(self, context): if self.source_object == self.grid_object == "" or True: if len(context.selected_objects) != 2: self.report({'ERROR'}, "Please, select two objects") return {'CANCELLED'} grid_obj = 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 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 context.selected_objects: o.select_set(False) grid_obj.select_set(True) context.view_layer.objects.active = grid_obj 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.data.objects.remove(temp_grid_obj) context.view_layer.objects.active = obj obj.select_set(True) self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64") return {'CANCELLED'} # CREATING LATTICE min = Vector((0, 0, 0)) max = Vector((0, 0, 0)) first = True for v in obj_me.vertices: 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: min[1] = vert[1] if vert[2] < min[2] or first: min[2] = vert[2] if vert[0] > max[0] or first: max[0] = vert[0] if vert[1] > max[1] or first: max[1] = vert[1] if vert[2] > max[2] or first: max[2] = vert[2] first = False bb = max - min lattice_loc = (max + min) / 2 bpy.ops.object.add(type='LATTICE') lattice = context.active_object lattice.location = lattice_loc lattice.scale = Vector((bb.x / self.scale_x, bb.y / self.scale_y, bb.z / self.scale_z)) if bb.x == 0: lattice.scale.x = 1 if bb.y == 0: lattice.scale.y = 1 if bb.z == 0: lattice.scale.z = 1 context.view_layer.objects.active = obj bpy.ops.object.modifier_add(type='LATTICE') obj.modifiers[-1].object = lattice # set as parent if self.set_parent: override = {'active_object': obj, 'selected_objects' : [lattice,obj]} bpy.ops.object.parent_set(override, type='OBJECT', keep_transform=False) # reading grid structure verts_grid, edges_grid, faces_grid = grid_from_mesh( grid_mesh, swap_uv=self.swapUV ) nu = len(verts_grid) nv = len(verts_grid[0]) nw = 2 scale_normal = self.thickness try: lattice.data.points_u = nu lattice.data.points_v = nv lattice.data.points_w = nw if self.use_groups: vg = temp_grid_obj.vertex_groups.active weight_factor = self.weight_factor for i in range(nu): for j in range(nv): for w in range(nw): if self.use_groups: try: weight_influence = vg.weight(verts_grid[i][j]) except: weight_influence = 0 weight_influence = weight_influence * (1 - weight_factor) + weight_factor displace = weight_influence * scale_normal * bb.z else: displace = scale_normal * bb.z target_point = (grid_mesh.vertices[verts_grid[i][j]].co + grid_mesh.vertices[verts_grid[i][j]].normal * (w + self.displace / 2 - 0.5) * displace) - lattice.location if self.flipW: w = 1 - w if self.flipU: 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') temp_grid_obj.select_set(True) lattice.select_set(True) obj.select_set(False) bpy.ops.object.delete(use_global=False) context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.modifier_remove(modifier=obj.modifiers[-1].name) if nu > 64 or nv > 64: self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64") return {'CANCELLED'} else: self.report({'ERROR'}, "The grid mesh is not correct") return {'CANCELLED'} bpy.ops.object.mode_set(mode='OBJECT') #grid_obj.select_set(True) #lattice.select_set(False) obj.select_set(False) #bpy.ops.object.delete(use_global=False) context.view_layer.objects.active = lattice lattice.select_set(True) if self.high_quality_lattice: context.object.data.points_w = 8 else: context.object.data.use_outside = True if self.hide_lattice: bpy.ops.object.hide_view_set(unselected=False) context.view_layer.objects.active = obj obj.select_set(True) lattice.select_set(False) if self.flipNormals: try: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.flip_normals() bpy.ops.object.mode_set(mode='OBJECT') except: pass bpy.data.meshes.remove(grid_mesh) bpy.data.meshes.remove(obj_me) return {'FINISHED'}