Skip to content
Snippets Groups Projects
mesh_helpers.py 9.14 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 #####
    
    # <pep8-80 compliant>
    
    # Generic helper functions, to be used by any modules.
    
    import bmesh
    import array
    
    
    def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False):
        """
        Returns a transformed, triangulated copy of the mesh
        """
    
        assert(obj.type == 'MESH')
    
        if apply_modifiers and obj.modifiers:
            import bpy
            me = obj.to_mesh(bpy.context.scene, True, 'PREVIEW', calc_tessface=False)
            bm = bmesh.new()
            bm.from_mesh(me)
            bpy.data.meshes.remove(me)
            del bpy
        else:
            me = obj.data
            if obj.mode == 'EDIT':
                bm_orig = bmesh.from_edit_mesh(me)
                bm = bm_orig.copy()
            else:
                bm = bmesh.new()
                bm.from_mesh(me)
    
        # TODO. remove all customdata layers.
        # would save ram
    
        if transform:
            bm.transform(obj.matrix_world)
    
        if triangulate:
            bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=True)
    
        return bm
    
    
    def bmesh_from_object(obj):
        """
        Object/Edit Mode get mesh, use bmesh_to_object() to write back.
        """
        me = obj.data
        is_editmode = (obj.mode == 'EDIT')
        if is_editmode:
            bm = bmesh.from_edit_mesh(me)
        else:
            bm = bmesh.new()
            bm.from_mesh(me)
        return bm
    
    
    def bmesh_to_object(obj, bm):
        """
        Object/Edit Mode update the object.
        """
        me = obj.data
        is_editmode = (obj.mode == 'EDIT')
        if is_editmode:
            bmesh.update_edit_mesh(me, True)
        else:
            bm.to_mesh(me)
        # grr... cause an update
        if me.vertices:
            me.vertices[0].co[0] = me.vertices[0].co[0]
    
    
    def bmesh_calc_volume(bm):
        """
        Calculate the volume of a triangulated bmesh.
        """
        def tri_signed_volume(p1, p2, p3):
            return p1.dot(p2.cross(p3)) / 6.0
        return abs(sum((tri_signed_volume(*(v.co for v in f.verts))
                        for f in bm.faces)))
    
    
    
    def bmesh_calc_volume_signed(bm):
        """
        Calculate the volume of a triangulated bmesh.
        """
        def tri_signed_volume(p1, p2, p3):
            return p1.dot(p2.cross(p3)) / 6.0
        return sum((tri_signed_volume(*(v.co for v in f.verts))
                    for f in bm.faces))
    
    
    
    def bmesh_calc_area(bm):
        """
        Calculate the surface area.
        """
        return sum(f.calc_area() for f in bm.faces)
    
    
    def bmesh_check_self_intersect_object(obj):
        """
        Check if any faces self intersect
    
        returns an array of edge index values.
        """
        import bpy
    
        # Heres what we do!
        #
        # * Take original Mesh.
        # * Copy it and triangulate it (keeping list of original edge index values)
        # * Move the BMesh into a temp Mesh.
        # * Make a temp Object in the scene and assign the temp Mesh.
        # * For every original edge - ray-cast on the object to find which intersect.
        # * Report all edge intersections.
    
        # Triangulate
        bm = bmesh_copy_from_object(obj, transform=False, triangulate=False)
        face_map_index_org = {f: i for i, f in enumerate(bm.faces)}
        ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
        face_map = ret["face_map"]
        # map new index to original index
        face_map_index = {i: face_map_index_org[face_map.get(f, f)] for i, f in enumerate(bm.faces)}
        del face_map_index_org
        del ret
    
        # Create a real mesh (lame!)
        scene = bpy.context.scene
        me_tmp = bpy.data.meshes.new(name="~temp~")
        bm.to_mesh(me_tmp)
        bm.free()
        obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
        scene.objects.link(obj_tmp)
        scene.update()
        ray_cast = obj_tmp.ray_cast
    
        faces_error = set()
    
    
        EPS_NORMAL = 0.000001
        EPS_CENTER = 0.01  # should always be bigger
    
    
        for ed in me_tmp.edges:
            v1i, v2i = ed.vertices
            v1 = me_tmp.vertices[v1i]
            v2 = me_tmp.vertices[v2i]
    
            # setup the edge with an offset
            co_1 = v1.co.copy()
            co_2 = v2.co.copy()
            co_mid = (co_1 + co_2) * 0.5
            no_mid = (v1.normal + v2.normal).normalized() * EPS_NORMAL
            co_1 = co_1.lerp(co_mid, EPS_CENTER) + no_mid
            co_2 = co_2.lerp(co_mid, EPS_CENTER) + no_mid
    
            co, no, index = ray_cast(co_1, co_2)
            if index != -1:
                faces_error.add(face_map_index[index])
    
        scene.objects.unlink(obj_tmp)
        bpy.data.objects.remove(obj_tmp)
        bpy.data.meshes.remove(me_tmp)
    
    
        return array.array('i', faces_error)
    
    
    def bmesh_face_points_random(f, num_points=1, margin=0.05):
        import random
        from random import uniform
        uniform_args = 0.0 + margin, 1.0 - margin
    
        # for pradictable results
        random.seed(f.index)
    
        vecs = [v.co for v in f.verts]
    
        for i in range(num_points):
            u1 = uniform(*uniform_args)
            u2 = uniform(*uniform_args)
            u_tot = u1 + u2
    
            if u_tot > 1.0:
                u1 = 1.0 - u1
                u2 = 1.0 - u2
    
            side1 = vecs[1] - vecs[0]
            side2 = vecs[2] - vecs[0]
    
            yield vecs[0] + u1 * side1 + u2 * side2
    
    
    def bmesh_check_thick_object(obj, thickness):
    
        import bpy
    
        # Triangulate
        bm = bmesh_copy_from_object(obj, transform=True, triangulate=False)
        # map original faces to their index.
        face_index_map_org = {f: i for i, f in enumerate(bm.faces)}
        ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
        face_map = ret["face_map"]
        del ret
        # old edge -> new mapping
    
        # Convert new/old map to index dict.
    
        # Create a real mesh (lame!)
        scene = bpy.context.scene
        me_tmp = bpy.data.meshes.new(name="~temp~")
        bm.to_mesh(me_tmp)
        # bm.free()  # delay free
        obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
        scene.objects.link(obj_tmp)
        scene.update()
        ray_cast = obj_tmp.ray_cast
    
        EPS_BIAS = 0.0001
    
        faces_error = set()
    
        bm_faces_new = bm.faces[:]
    
        for f in bm_faces_new:
            no = f.normal
            no_sta = no * EPS_BIAS
            no_end = no * thickness
            for p in bmesh_face_points_random(f, num_points=6):
                # Cast the ray backwards
                p_a = p - no_sta
                p_b = p - no_end
    
                co, no, index = ray_cast(p_a, p_b)
    
                if index != -1:
                    # Add the face we hit
                    for f_iter in (f, bm_faces_new[index]):
                        # if the face wasn't triangulated, just use existing
                        f_org = face_map.get(f_iter, f_iter)
                        f_org_index = face_index_map_org[f_org]
                        faces_error.add(f_org_index)
    
        # finished with bm
        bm.free()
    
        scene.objects.unlink(obj_tmp)
        bpy.data.objects.remove(obj_tmp)
        bpy.data.meshes.remove(me_tmp)
    
    
        return array.array('i', faces_error)
    
    
    
    def object_merge(context, objects):
        """
        Caller must remove.
        """
    
        import bpy
    
        def cd_remove_all_but_active(seq):
            tot = len(seq)
            if tot > 1:
                act = seq.active_index
                for i in range(tot - 1, -1, -1):
                    if i != act:
                        seq.remove(seq[i])
    
        scene = context.scene
    
        # deselect all
        for obj in scene.objects:
            obj.select = False
    
        # add empty object
        mesh_base = bpy.data.meshes.new(name="~tmp~")
        obj_base = bpy.data.objects.new(name="~tmp~", object_data=mesh_base)
        base_base = scene.objects.link(obj_base)
        scene.objects.active = obj_base
        obj_base.select = True
    
        # loop over all meshes
        for obj in objects:
            if obj.type != 'MESH':
                continue
    
            # convert each to a mesh
            mesh_new = obj.to_mesh(scene=scene,
                                   apply_modifiers=True,
                                   settings='PREVIEW',
                                   calc_tessface=False)
    
            # remove non-active uvs/vcols
            cd_remove_all_but_active(mesh_new.vertex_colors)
            cd_remove_all_but_active(mesh_new.uv_textures)
    
            # join into base mesh
            obj_new = bpy.data.objects.new(name="~tmp-new~", object_data=mesh_new)
            base_new = scene.objects.link(obj_new)
            obj_new.matrix_world = obj.matrix_world
    
            fake_context = context.copy()
            fake_context["active_object"] = obj_base
            fake_context["selected_editable_bases"] = [base_base, base_new]
    
            bpy.ops.object.join(fake_context)
            del base_new, obj_new
    
            # remove object and its mesh, join does this
            #~ scene.objects.unlink(obj_new)
            #~ bpy.data.objects.remove(obj_new)
    
            bpy.data.meshes.remove(mesh_new)
    
    
        # return new object
        return base_base