Skip to content
Snippets Groups Projects
wetted_mesh.py 12.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    bl_info = {
        "name": "Add Wetted Mesh",
        "author": "freejack",
    
    Kai Schwebke's avatar
    Kai Schwebke committed
        "version": (0, 2, 1),
    
        "blender": (2, 58, 0),
    
        "location": "View3D > Tool Shelf > Wetted Mesh Panel",
        "description": "Adds separated fluid, dry and wetted mesh for selected pair.",
        "warning": "",
    
        "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
    
        "tracker_url": "https://developer.blender.org/T27156",
    
    import bpy
    import collections
    import math
    
    ### Tool Panel ###
    class VIEW3D_PT_tools_WettedMesh(bpy.types.Panel):
    
        """Wetted Mesh Tool Panel"""
    
        bl_space_type = 'VIEW_3D'
        bl_region_type = 'TOOLS'
        bl_label = 'Wetted Mesh'
        bl_context = 'objectmode'
    
    Brendon Murphy's avatar
    Brendon Murphy committed
        bl_options = {'DEFAULT_CLOSED'}
    
    
        def draw(self, context):
            layout = self.layout
            col = layout.column(align=True)
            slcnt = len(context.selected_objects)
    
            if slcnt != 2:
    
    Campbell Barton's avatar
    Campbell Barton committed
                col.label(text = 'Select two mesh objects')
                col.label(text = 'to generate separated')
                col.label(text = 'fluid, dry and wetted')
                col.label(text = 'meshes.')
    
    Campbell Barton's avatar
    Campbell Barton committed
                (solid, fluid) = getSelectedPair(context)
                col.label(text = 'solid = '+solid.name)
                col.label(text = 'fluid = '+fluid.name)
                col.operator('mesh.primitive_wetted_mesh_add', text='Generate Meshes')
    
    
    ### Operator ###
    class AddWettedMesh(bpy.types.Operator):
    
        """Add wetted mesh for selected mesh pair"""
    
        bl_idname = "mesh.primitive_wetted_mesh_add"
        bl_label = "Add Wetted Mesh"
        bl_options = {'REGISTER', 'UNDO'}
        statusMessage = ''
    
        def draw(self, context):
            layout = self.layout
            col = layout.column(align=True)
            col.label(text = self.statusMessage)
    
        def execute(self, context):
            # make sure a pair of objects is selected
            if len(context.selected_objects) != 2:
                # should not happen if called from tool panel
                self.report({'WARNING'}, "no mesh pair selected, operation cancelled")
                return {'CANCELLED'}
    
            print("add_wetted_mesh begin")
    
            # super-selected object is solid, other object is fluid
            (solid, fluid) = getSelectedPair(context)
            print("   solid = "+solid.name)
            print("   fluid = "+fluid.name)
    
            # make a copy of fluid object, convert to mesh if required
            print("   copy fluid")
            bpy.ops.object.select_all(action='DESELECT')
            fluid.select = True
            context.scene.objects.active = fluid
            bpy.ops.object.duplicate()
            bpy.ops.object.convert(target='MESH', keep_original=False)
    
    Kai Schwebke's avatar
    Kai Schwebke committed
            bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    
            # substract solid from fluidCopy
            print("   bool: fluidCopy DIFFERENCE solid")
            bpy.ops.object.modifier_add(type='BOOLEAN')
            bop = fluidCopy.modifiers.items()[0]
            bop[1].operation = 'DIFFERENCE'
            bop[1].object = solid
            bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
            fluidMinusSolid = fluidCopy
            fluidMinusSolid.name = "fluidMinusSolid"
    
            # make a second copy of fluid object
            print("   copy fluid")
            bpy.ops.object.select_all(action='DESELECT')
            fluid.select = True
            context.scene.objects.active = fluid
            bpy.ops.object.duplicate()
            bpy.ops.object.convert(target='MESH', keep_original=False)
    
    Kai Schwebke's avatar
    Kai Schwebke committed
            bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    
            # make union from fluidCopy and solid
            print("   bool: fluidCopy UNION solid")
            bpy.ops.object.modifier_add(type='BOOLEAN')
            bop = fluidCopy.modifiers.items()[0]
            bop[1].operation = 'UNION'
            bop[1].object = solid
            bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
            fluidUnionSolid = fluidCopy
            fluidUnionSolid.name = "fluidUnionSolid"
    
            # index meshes
            print("   KDTree index fluidMinusSolid")
            fluidMinusSolidKDT = KDTree(3, fluidMinusSolid.data.vertices)
            print("   KDTree index fluidUnionSolid")
            fluidUnionSolidKDT = KDTree(3, fluidUnionSolid.data.vertices)
            kdtrees = (fluidMinusSolidKDT, fluidUnionSolidKDT)
    
            # build mesh face sets
            faceDict = { }
            vertDict = { }
    
            print("   processing fluidMinusSolid faces")
            cacheDict = { }
            setFMSfaces = set()
            numFaces = len(fluidUnionSolid.data.faces)
            i = 0
            for f in fluidMinusSolid.data.faces:
                if i % 500 == 0:
    
    Campbell Barton's avatar
    Campbell Barton committed
                    print("      ", i, " / ", numFaces)
    
                i += 1
                fuid = unifiedFaceId(kdtrees, f, fluidMinusSolid.data.vertices, \
                                     faceDict, vertDict, cacheDict)
                setFMSfaces.add(fuid)
    
            print("   processing fluidUnionSolid faces")
            cacheDict = { }
            setFUSfaces = set()
            numFaces = len(fluidUnionSolid.data.faces)
            i = 0
            for f in fluidUnionSolid.data.faces:
                if i % 500 == 0:
    
    Campbell Barton's avatar
    Campbell Barton committed
                    print("      ", i, " / ", numFaces)
    
                i += 1
                fuid = unifiedFaceId(kdtrees, f, fluidUnionSolid.data.vertices, \
                                     faceDict, vertDict, cacheDict)
                setFUSfaces.add(fuid)
    
            # remove boolean helpers
            print("   delete helper objects")
            bpy.ops.object.select_all(action='DESELECT')
            fluidUnionSolid.select = True
            fluidMinusSolid.select = True
            bpy.ops.object.delete()
    
            # wetted = FMS - FUS
            print("   set operation FMS diff FUS")
            setWetFaces = setFMSfaces.difference(setFUSfaces)
            print("   build wetted mesh")
            verts, faces = buildMesh(setWetFaces, faceDict, vertDict)
            print("   create wetted mesh")
            wetted = createMesh("Wetted", verts, faces)
    
            # fluid = FMS x FUS
            print("   set operation FMS intersect FUS")
            setFluidFaces = setFMSfaces.intersection(setFUSfaces)
            print("   build fluid mesh")
            verts, faces = buildMesh(setFluidFaces, faceDict, vertDict)
            print("   create fluid mesh")
            fluid = createMesh("Fluid", verts, faces)
    
            # solid = FUS - FMS
            print("   set operation FUS diff FMS")
            setSolidFaces = setFUSfaces.difference(setFMSfaces)
            print("   build solid mesh")
            verts, faces = buildMesh(setSolidFaces, faceDict, vertDict)
            print("   create solid mesh")
            solid = createMesh("Solid", verts, faces)
    
            # parent wetted mesh
            print("   parent mesh")
            bpy.ops.object.add(type='EMPTY')
            wettedMesh = context.object
            solid.select = True
            fluid.select = True
            wetted.select = True
            wettedMesh.select = True
            bpy.ops.object.parent_set(type='OBJECT')
            wettedMesh.name = 'WettedMesh'
    
            print("add_wetted_mesh done")
            self.statusMessage = 'created '+wettedMesh.name
    
            return {'FINISHED'}
    
    
    ### Registration ###
    def register():
        bpy.utils.register_class(VIEW3D_PT_tools_WettedMesh)
        bpy.utils.register_class(AddWettedMesh)
    
    
    def unregister():
        bpy.utils.unregister_class(VIEW3D_PT_tools_WettedMesh)
        bpy.utils.unregister_class(AddWettedMesh)
    
    if __name__ == "__main__":
        register()
    
    
    #
    # KD tree (used to create a geometric index of mesh vertices)
    #
    
    def distance(a, b):
        return (a-b).length
    
    Node = collections.namedtuple("Node", 'point axis label left right')
    
    class KDTree(object):
        """A tree for nearest neighbor search in a k-dimensional space.
    
        For information about the implementation, see
        http://en.wikipedia.org/wiki/Kd-tree
    
        Usage:
        objects is an iterable of (co, index) tuples (so MeshVertex is useable)
        k is the number of dimensions (=3)
    
        t = KDTree(k, objects)
        point, label, distance = t.nearest_neighbor(destination)
        """
    
        def __init__(self, k, objects=[]):
    
            def build_tree(objects, axis=0):
    
                if not objects:
                    return None
    
                objects.sort(key=lambda o: o.co[axis])
                median_idx = len(objects) // 2
                median_point = objects[median_idx].co
                median_label = objects[median_idx].index
    
                next_axis = (axis + 1) % k
                return Node(median_point, axis, median_label,
                            build_tree(objects[:median_idx], next_axis),
                            build_tree(objects[median_idx + 1:], next_axis))
    
            self.root = build_tree(list(objects))
            self.size = len(objects)
    
    
        def nearest_neighbor(self, destination):
    
            best = [None, None, float('inf')]
            # state of search: best point found, its label,
            # lowest distance
    
            def recursive_search(here):
    
                if here is None:
                    return
                point, axis, label, left, right = here
    
                here_sd = distance(point, destination)
                if here_sd < best[2]:
                    best[:] = point, label, here_sd
    
                diff = destination[axis] - point[axis]
                close, away = (left, right) if diff <= 0 else (right, left)
    
                recursive_search(close)
                if math.fabs(diff) < best[2]:
                    recursive_search(away)
    
            recursive_search(self.root)
            return best[0], best[1], best[2]
    
    
    #
    # helper functions
    #
    
    # get super-selected object and other object from selected pair
    def getSelectedPair(context):
        objA = context.object
        objB = context.selected_objects[0]
        if objA == objB:
    
    Campbell Barton's avatar
    Campbell Barton committed
            objB = context.selected_objects[1]
    
        return (objA, objB)
    
    # get a unified vertex id for given coordinates
    def unifiedVertexId(kdtrees, location, vertDict):
        eps = 0.0001
        offset = 0
        for t in kdtrees:
            co, index, d = t.nearest_neighbor(location)
            if d < eps:
                uvid = offset + index
                if uvid not in vertDict:
                    vertDict[uvid] = co
                return uvid
            offset += t.size
        return -1
    
    # get a unified face id tuple
    #    Stores the ordered face id tuple in faceDict
    #    and the used coordinates for vertex id in vertDict.
    #    cacheDict caches the unified vertex id (lookup in kdtree is expensive).
    #    For each mesh (where the face belongs to) a separate cacheDict is expected.
    def unifiedFaceId(kdtrees, face, vertices, faceDict, vertDict, cacheDict):
        fids = [ ]
        for v in face.vertices:
            uvid = cacheDict.get(v)
            if uvid == None:
                uvid = unifiedVertexId(kdtrees, vertices[v].co, vertDict)
                cacheDict[v] = uvid
            fids.append(uvid)
        ofids = tuple(fids)
        fids.sort()
        fuid = tuple(fids)
        if fuid not in faceDict:
            faceDict[fuid] = ofids
        return fuid
    
    # build vertex and face array from unified face sets
    def buildMesh(unifiedFaceSet, faceDict, vertDict):
        verts = [ ]
        nextV = 0
        myV = { }
        faces = [ ]
        for uf in unifiedFaceSet:
            of = faceDict[uf]
            myf = [ ]
            for uV in of:
                v = myV.get(uV)
                if v == None:
                    v = nextV
                    myV[uV] = nextV
                    verts.append(vertDict[uV])
                    nextV += 1
                myf.append(v)
            faces.append(myf)
        return verts, faces
    
    # create mesh object and link to scene
    def createMesh(name, verts, faces):
        me = bpy.data.meshes.new(name+"Mesh")
        ob = bpy.data.objects.new(name, me)
        ob.show_name = True
        bpy.context.scene.objects.link(ob)
        me.from_pydata(verts, [], faces)
        me.update(calc_edges=True)
        return ob