Esempio n. 1
0
    def _exec_obj_mode(self, context):
        all_type_err = True  # no obj is of type mesh
        vert_count_err = False  # less than 2 vertices selected

        if self.join_select:
            coords = np.empty((0, 3), dtype=float)
            ob = context.object if context.object else \
                context.selected_objects[0]
            mat_wrld_inv = np.array(ob.matrix_world.inverted())

            for o in context.selected_objects:
                if o.type == 'MESH':
                    all_type_err = False
                else:
                    continue

                ocoords = sbio.get_vecs(o.data.vertices)

                if o is not ob:
                    mat = mat_wrld_inv @ np.array(o.matrix_world)
                    ocoords = sbt.transf_pts(mat, ocoords)

                coords = np.concatenate((coords, ocoords))

            if len(coords) > 1:
                self._core(context, ob, coords, context.selected_objects)
            else:
                vert_count_err = True
        else:
            for o in context.selected_objects:
                if o.type == 'MESH':
                    all_type_err = False
                else:
                    continue

                # If we align to axes and the ob is rotated, we can't
                # use Blender's bounding box. Instead, we have to find
                # the global bounds from all global vertex positions.
                # This is because for a rotated object, the global
                # bounds of its local bounding box aren't always equal
                # to the global bounds of all its vertices.
                # If we don't align to axes, we aren't interested in the
                # global ob bounds anyway.
                rot = np.array(o.matrix_world.to_euler())
                coords = sbio.get_vecs(o.data.vertices) \
                    if self.align_to_axes and rot.dot(rot) > 0.001 \
                    else np.array(o.bound_box)

                if len(coords) > 1:
                    self._core(context, o, coords, [o])
                else:
                    vert_count_err = True

        if vert_count_err:
            self.report({'ERROR_INVALID_INPUT'},
                        "A selection must at least contain two vertices")
        if all_type_err:
            self.report({'ERROR_INVALID_INPUT'},
                        "An object must be of type mesh")
Esempio n. 2
0
    def execute(self, context):
        obs_in_editmode = context.objects_in_mode_unique_data
        bpy.ops.object.mode_set(mode='OBJECT')
        try:
            for o in obs_in_editmode:
                viewdir = Vector((0, 0, -1))

                for area in context.screen.areas:
                    if area.type != 'VIEW_3D':
                        continue

                    r3d = area.spaces[0].region_3d
                    if r3d is None:
                        continue

                    viewdir.rotate(r3d.view_rotation)
                    break

                # For every face normal, calculate the dot product
                # with the view direction
                nrmls = get_vecs(o.data.polygons, attr='normal')
                dotprdcs = np.dot(nrmls, np.array(viewdir))

                # Select each face with dot product entry > 0
                selflags = np.greater(dotprdcs, 0)
                o.data.polygons.foreach_set('select', selflags)
        finally:
            bpy.ops.object.mode_set(mode='EDIT')

        return {'FINISHED'}
Esempio n. 3
0
    def execute(self, context):
        for o in context.selected_editable_objects:
            if o.type != 'MESH':
                continue

            coords = get_vecs(o.data.vertices)

            # Reconstruct basis vectors via Singular Value Decomposition
            _, _, v = np.linalg.svd(coords)

            # Ensure basis vectors are as close to world axes as
            # possible: if a basis vector points in the opposite
            # direction, invert it
            i3 = np.identity(3)
            for i in range(3):
                if np.dot(v[i], i3[i]) < 0:
                    v[i] = -v[i]

            # Apply new basis to mesh
            set_vals(o.data.vertices, coords @ v.T)

            # Modify object rotation so object stays in the same place,
            # even with its mesh changed
            o.matrix_basis @= Matrix(append_row_and_col(v.T))
        return {'FINISHED'}
Esempio n. 4
0
    def execute(self, context):
        if self.method == 'RED':
            meth = get_red
        elif self.method == 'GRE':
            meth = get_green
        elif self.method == 'BLU':
            meth = get_blue
        else:
            meth = avg_rgb

        for o in context.selected_editable_objects:
            if o.type != 'MESH':
                continue

            cols = o.data.vertex_colors.active
            if not cols:
                continue

            # Get colors of all mesh loops
            # These loops don't always match the vertex count and
            # are not stored at the correct vertex indices.
            cs = get_vecs(cols.data, attr='color', vecsize=4)

            # For every loop, get its vertex index
            lops = o.data.loops
            vindcs = get_scalars(lops, attr='vertex_index', dtype=np.int)

            # Find the indices of 'vindcs' at which a unique entry is
            # found for the first time.
            _, i_vindcs = np.unique(vindcs, return_index=True)

            # This index list 'i_vindcs' filters out all redundant
            # entries in 'cs' and sorts them so each color lands at
            # the index of its corresponding vertex.
            # Then calculate the (unique) weights of the colors via
            # the chosen method.
            weights = meth(cs[i_vindcs])
            u_weights = np.unique(weights)

            vg = o.vertex_groups.new(name=cols.name)
            for w in u_weights:
                # Get the indices of one weight value and add it to
                # the vertex group.
                indcs = np.where(weights == w)[0]
                vg.add(indcs.tolist(), w, 'REPLACE')

        return {'FINISHED'}
Esempio n. 5
0
    def _find_parts(self, context):
        for o in context.objects_in_mode:
            data = o.data
            parts = TreeDict(acc=concat)
            coords = get_vecs(data.vertices)
            # choose comparison method
            method = np.linalg.norm if self._method == 0 else np.prod

            for indcs in get_parts(from_edit_mesh(data).verts):
                bounds, _ = get_bounds_and_center(coords[indcs])
                # calculate comparison value from bounding box,
                # round to create less bins in dict for better
                # performance
                key = round(method(bounds), self._resolution)
                parts[key] = indcs

            self._data.append((data.vertices, parts))
Esempio n. 6
0
    def _find_parts(self, obs):
        for o in obs:
            # get parts, each a vertex index list
            bob = bmesh.new()
            bob.from_mesh(o.data)
            parts = get_parts(bob.verts)
            del bob

            # choose comparison method
            method = np.linalg.norm if self._method == 0 else np.prod

            # create dict of parts and their comparison value
            partdict = TreeDict(acc=concat)
            coords = get_vecs(o.data.vertices)
            for indcs in parts:
                bounds, _ = get_bounds_and_center(coords[indcs])
                # calculate comparison value from bounding box,
                # round to create less bins in dict for better
                # performance
                key = round(method(bounds), self._resolution)
                partdict[key] = indcs

            self._data.append((o.data.vertices, partdict))
Esempio n. 7
0
    def execute(self, context):
        # TARGET
        target = context.object
        tdata = target.data
        tverts = tdata.vertices
        teuler = target.matrix_world.to_euler()

        if tdata.is_editmode:
            # ensure newest changes from edit mode are visible to data
            target.update_from_editmode()

            # get selected vertices in target
            sel_flags_target = sbio.get_scalars(tdata.vertices)
            tcoords = sbio.get_vecs(tverts)[sel_flags_target]
        else:
            trot = np.array(teuler)

            # If we align to axes and the target is rotated, we can't
            # use Blender's bounding box. Instead, we have to find the
            # global bounds from all global vertex positions.
            # This is because for a rotated object, the global bounds of
            # its local bounding box aren't always equal to the global
            # bounds of all its vertices.
            # If we don't align to axes, we aren't interested in the
            # global target bounds anyway.
            tcoords = sbio.get_vecs(tverts) \
                if self.axes_align \
                and trot.dot(trot) > 0.001 \
                else np.array(target.bound_box)

        if len(tcoords) < 2:
            self.report({'ERROR_INVALID_INPUT'}, "Select at least 2 vertices")
            return {'CANCELLED'}

        tworldmat = np.array(target.matrix_world)

        if self.axes_align:
            # If we align sources to world axes, we are interested in
            # the target bounds in world coordinates.
            tcoords = sbt.transf_pts(tworldmat, tcoords)
            # If we align sources to axes, we ignore target's rotation.
            trotmat = np.identity(3)

        tbounds, tcenter = sbio.get_bounds_and_center(tcoords)

        if not self.axes_align:
            # Even though we want the target bounds in object space if
            # align to axes is false, we still are interested in world
            # scale and center.
            tbounds *= np.array(target.matrix_world.to_scale())
            tcenter = sbt.transf_point(tworldmat, tcenter)
            # target rotation to later apply to all sources
            trotmat = np.array(teuler.to_matrix())

        # SOURCE
        error_happened = False
        for source in context.selected_objects:
            if source is target:
                continue

            if source.type != 'MESH':
                continue

            sdata = source.data
            sverts = sdata.vertices

            if sdata.is_editmode:
                # get selected vertices in source
                source.update_from_editmode()
                sverts_all = sbio.get_vecs(sdata.vertices)
                sselflags = sbio.get_scalars(sdata.vertices)
                sverts_sel = sverts_all[sselflags]

                if len(sverts_sel) < 2:
                    error_happened = True
                    continue
            else:
                sverts_sel = np.array(source.bound_box)

            sbounds, scenter = sbio.get_bounds_and_center(sverts_sel)
            sbounds_recpr = np.reciprocal(
                sbounds,
                # prevent division by 0
                out=np.ones_like(sbounds),
                where=sbounds != 0,
            )

            # assemble transformation matrix later applied to source
            transf_mat = \
                sbmm.to_transl_mat(tcenter) @ \
                sbmm.append_row_and_col(
                    trotmat @
                    sbmm.to_scale_mat(tbounds) @
                    sbmm.euler_to_rot_mat(np.array(self.rot_offset)) @
                    sbmm.to_scale_mat(sbounds_recpr)
                ) @ \
                sbmm.to_transl_mat(-scenter)

            if sdata.is_editmode:
                # somehow the mesh doesn't update if we stay in edit
                # mode
                bpy.ops.object.mode_set(mode='OBJECT')
                # transform transformation matrix from world to object
                # space
                transf_mat = np.array(source.matrix_world.inverted()) \
                    @ transf_mat
                # update every selected vertex with transformed
                # coordinates
                sverts_all[sselflags] = \
                    sbt.transf_pts(transf_mat, sverts_sel)
                # overwrite complete vertex list (also non-selected)
                sbio.set_vals(sverts, sverts_all)
                bpy.ops.object.mode_set(mode='EDIT')
            else:
                source.matrix_world = Matrix(transf_mat)

        if error_happened:
            self.report(
                {'ERROR_INVALID_INPUT'},
                "Select at least 2 vertices per selected source object")
        return {'FINISHED'}
Esempio n. 8
0
 def get_sel_verts(o):
     # ensure newest changes from edit mode are
     # visible to data
     o.update_from_editmode()
     sel_flags = sbio.get_scalars(o.data.vertices)
     return sbio.get_vecs(o.data.vertices)[sel_flags], sel_flags
Esempio n. 9
0
    def execute(self, context):
        source = context.object
        try:
            sdata = source.data
            sgeom = sdata.polygons
        except AttributeError:
            self.report(
                {'ERROR_INVALID_INPUT'},
                "The active object needs to have a mesh data block.",
            )
            return {'CANCELLED'}

        # get comparison values for source
        scvals = get_vecs(sgeom, 'center')
        if self.in_wrld_crds:
            # transform source to world coordinates
            mat = np.array(source.matrix_world)
            scvals = transf_pts(mat, scvals)

        # build KD-Tree from comparison values
        kd = KDTree(len(sgeom))
        for i, v in enumerate(scvals):
            kd.insert(v, i)
        kd.balance()

        # get values to transfer from source
        stvals = get_scalars(sgeom, 'material_index', np.int8)

        all_meshless = True  # for error-reporting
        for target in context.selected_objects:
            if target is source:
                continue

            try:
                tdata = target.data
                tgeom = tdata.polygons
                all_meshless = False
            except AttributeError:
                continue

            # get comparison values for target
            tcvals = get_vecs(tgeom, 'center')

            if self.in_wrld_crds:
                # transform target to world coordinates
                mat = np.array(target.matrix_world)
                tcvals = transf_pts(mat, tcvals)
                ttvals = np.empty(len(tgeom), dtype=np.int32)

            # for every comparison point in target, find closest in
            # source and copy over its transfer value
            for ti, tv in enumerate(tcvals):
                _, si, _ = kd.find(tv)
                ttvals[ti] = stvals[si]

            # set values to transfer to target
            set_vals(tgeom, ttvals, 'material_index')

            tmats = tdata.materials
            if self.assign_mat:
                # transfer assigned materials
                for i, m in enumerate(sdata.materials):
                    if i < len(tmats):
                        tmats[i] = m
                    else:
                        tmats.append(m)

        if all_meshless:
            self.report(
                {'ERROR_INVALID_INPUT'},
                "No selected target object has a mesh data block.",
            )
            return {'CANCELLED'}
        return {'FINISHED'}
Esempio n. 10
0
    def execute(self, context):
        mode = context.mode
        selobs = context.selected_objects
        arms = [o for o in selobs if o.type == 'ARMATURE']

        if not arms:
            self.report({'ERROR_INVALID_INPUT'},
                        "Select exactly one armature.")
            return {'CANCELLED'}

        arm = arms[0]
        bone1, bone2 = None, None

        for b in arm.data.bones:
            if b.select:
                if bone1 is None:
                    bone1 = b
                else:
                    bone2 = b
                    break
        if bone2 is None:
            self.report({'ERROR_INVALID_INPUT'}, "Select exactly two bones.")
            return {'CANCELLED'}

        # Transform bones into world system
        arm2wrld = np.array(arm.matrix_world)
        b1 = transf_point(arm2wrld, bone1.head_local)
        b2 = transf_point(arm2wrld, bone2.head_local)

        dims = np.array(self.axes)

        # Vector from bone1 to bone2, if wished projected onto the
        # axis/plane isolated through the 'axes' parameter by zeroing
        # coordinates in ignored dimensions
        b1b2 = (b2 - b1) * dims

        # Squared distance between the bones
        sqrbdist = np.linalg.norm(b1b2)**2

        if sqrbdist == 0:
            # If both bones are in the same position after projection,
            # we can't interpolate between them.
            # This way, bone1 is assigned a weight of one.
            sqrbdist = 1e-15

        for o in selobs:
            if o.type != 'MESH':
                continue

            # Get selected vertices
            o.update_from_editmode()
            verts = o.data.vertices
            selflags = get_scalars(verts)
            pts = get_vecs(verts)[selflags]

            # Also transform vertices into the world system
            pts = transf_vecs(o.matrix_world, pts)
            # Calc vector from bone1 to every point and zero coordinates
            # of dimensions that are ignored.
            b1pts = (pts - b1) * dims
            # Calc dot product of this vector and the vector from bone1
            # to bone2
            # Get final weight by dividing through the squared bone
            # distance
            weights = np.dot(b1b2, b1pts.T) / sqrbdist
            # breakpoint()

            # Get indices of selected vertices
            indcs = np.arange(len(selflags))[selflags]
            vgs = o.vertex_groups
            try:
                vg1 = vgs[bone1.name]
            except KeyError:
                self.report(
                    {'ERROR_INVALID_INPUT'},
                    (f"Vertex group '{bone1.name}' does not exist "
                     f"on object '{o.name}'"),
                )
                continue
            if self.bidirect:
                try:
                    vg2 = vgs[bone2.name]
                except KeyError:
                    self.report(
                        {'ERROR_INVALID_INPUT'},
                        (f"Vertex group '{bone2.name}' does not "
                         f"exist on object '{o.name}'"),
                    )
                    continue

            # The mesh doesn't update in edit mode.
            bpy.ops.object.mode_set(mode='OBJECT')
            try:
                for i, w in zip(indcs, weights):
                    # No clue why add expects a one-element list, but
                    # that's how it is.
                    # item() converts the NumPy int to a native int
                    idx = [i.item()]
                    vg1.add(idx, 1 - w, 'REPLACE')
                    if self.bidirect:
                        vg2.add(idx, w, 'REPLACE')
            finally:
                # This is so stupid Blender! Why not make those
                # strings match!?
                if mode == 'PAINT_WEIGHT':
                    bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
                else:
                    bpy.ops.object.mode_set(mode='EDIT')
        return {'FINISHED'}
Esempio n. 11
0
def sample_mesh(mesh, samplecnt=1024, mask=None):
    """
    Draw N random samples on the surface of a triangle mesh.

    Parameters
    ----------
    mesh : bpy.types.Mesh
        Blender mesh to sample from.
    samplecnt : int = 1024
        Number of samples to use.
    mask : Iterable or None = None
        Iterable specifying the faces from which to sample either by
        passing their index in the mesh's face list as an Integer
        iterable or as a Bool iterable where that specific index is set
        to True. If None is passed, every face is sampled.

    Returns
    -------
    out : numpy.ndarray
        2D array with shape (N, 3), containing the coordinates of the
        N drawn sample points.
    """
    # Load mesh into bmesh,
    bob = bm.new()
    bob.from_mesh(mesh, face_normals=False)
    if mask is not None:
        bfaces = np.array(bob.faces)
        invmask = np.ones(len(bfaces), dtype=np.bool)
        invmask[mask] = False
        bm.ops.delete(bob, geom=bfaces[invmask], context='FACES')
        del bfaces, invmask

    # triangulate it,
    bm.ops.triangulate(bob, faces=bob.faces)
    # and save it back to a temporary mesh, so that we can use
    # Blenders' fast-access foreach functions to get vectorized data.
    mesh = bpy.data.meshes.new("tmp")
    bob.to_mesh(mesh)
    del bob

    # Accumulate all triangle areas in the mesh to sample each triangle
    # with a probability proportional to its surface area.
    areas = get_scalars(mesh.polygons, 'area', np.float64)
    areas = np.cumsum(areas)
    # Choose N random floats between 0 and the sum of all areas.
    rdareas = np.random.uniform(0., areas[-1], samplecnt)
    # For each random float, find the index of the triangle with the
    # highest, but less equal cumulative area (the left neighbor of the
    # randomly drawn area).
    rdindcs = np.searchsorted(areas, rdareas)
    # These vectorized calculations eat up a lot of memory. Make some
    # unneeded data applicable for garbage collection.
    del areas, rdareas

    # Get the vertex coordinates.
    pts = get_vecs(mesh.vertices)
    # Get the vertex indices for each triangle.
    tris = get_vecs(mesh.polygons, 'vertices', dtype=np.int32)
    bpy.data.meshes.remove(mesh)
    # Inner indexing operation: For each randomly chosen triangle index,
    # insert the actual vertex indices of the corresponding triangle
    # into the array.
    # Outer indexing operation: For each vertex index in the triangle
    # array, insert its actual vertex coordinates.
    # This 3D array holds a lot of redundant data now, this probably
    # scales pretty badly with the sample count and the number of
    # triangles.
    tris = pts[tris[rdindcs, :]]
    del rdindcs, pts

    # For each sample, draw two random floats that determine where on
    # the triangle the sample point is placed.
    # This is done via the following formula, with the triangle's
    # vertex coordinate vectors A, B, C:
    # P = (1 - sqrt(r1))*A + sqrt(r1)*(1 - r2)*B + sqrt(r1)*r2*C
    r1 = np.sqrt(np.random.rand(samplecnt))
    r2 = np.random.rand(samplecnt)
    # Calculate coefficients for each vertex A, B, C
    coef = np.stack(
        (1 - r1, r1 * (1 - r2), r1 * r2),
        axis=1,
    ).reshape(-1, 3, 1)
    del r1, r2
    # For each triangle sum up A, B, and C, leaving one point per
    # sample. Remember, one triangle can be several times in 'tris',
    # but a different sample is drawn from its surface every time.
    return np.sum(coef * tris, axis=1)
Esempio n. 12
0
    def _find_concave_patches(self, context):
        """
        A patch is a set of connected faces. For each patch containing
        at least two faces facing each other, return a list of vertex
        indices making up such a patch, together with its center point
        and diameter. A patch's border is along edges between faces
        facing away from each other (think ridges).
        """
        self._meshes.clear()
        for o in context.objects_in_mode_unique_data:
            o.update_from_editmode()
            data = o.data
            polys = data.polygons
            bob = bm.from_edit_mesh(data)
            bfaces = bob.faces
            bfaces.ensure_lookup_table()
            # bmesh has to recalculate face centers, so get them
            # directly from the mesh data instead
            centrs = get_vecs(polys, attr='center')
            # Bool array of vertex indices already visited.
            # Unselected faces will be True already.
            flags = get_scalars(polys)
            # Will contain a list of tuples. First entry is the list of
            # face indices of the patch. Second is the maximum angle
            # between two neighboring faces in the patch.
            # This list is only needed to not delete vertices while we
            # iterate the mesh.
            patches = []

            for i, new in enumerate(flags):
                if not new:
                    continue

                flags[i] = False
                # Faces to visit
                stack = [bfaces[i]]
                # Face indices of the patch
                findcs = []
                # Maximum dot product between two neighboring faces in
                # the patch.
                maxdot = 0

                while stack:
                    f = stack.pop()
                    n = f.normal
                    c = centrs[f.index]
                    findcs.append(f.index)

                    # Push all faces connected to f on stack...
                    for l in f.loops:
                        f2 = l.link_loop_radial_next.face
                        i2 = f2.index
                        # The dot product between f's normal and the
                        # vector between both face's centers is a
                        # simple way to measure if they are parallel
                        # (=0), concave (>0), or convex (<0).
                        angl = n.dot(normalize(centrs[i2] - c))
                        # but only if not already checked and f and f2
                        # are not convex (don't face away from each
                        # other)
                        if flags[i2] and angl > -1e-3:
                            maxdot = max(maxdot, angl)
                            flags[i2] = False
                            stack.append(f2)

                if len(findcs) > 2:
                    # pihalf: transform dot product result to rad angle
                    patches.append((findcs, maxdot * pihalf))

            del flags
            # second representation of patches, this time as a tuple of
            # face indices, max angle, and diameter
            patches2 = []

            for findcs, maxangl in patches:
                bounds, _ = get_bounds_and_center(centrs[findcs])
                patches2.append((findcs, maxangl, np.linalg.norm(bounds)))
            self._meshes.append((data, patches2))