OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / meshtools / floor.py

126 lines · python ref:
import bpy, mathutils, math, bmesh
from math import cos, sin
from mathutils import Vector, Matrix, Quaternion, geometry
from sys import float_info
from ... utility import addon

DESC = """Floor \nMake sure to have and active face selected"""


class HOPS_OT_FLOOR(bpy.types.Operator):
    bl_idname = "hops.floor"
    bl_label = "Hops Floor"
    bl_description = DESC
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        if context.active_object:
            if context.active_object.type == 'MESH':
                if context.active_object.mode == 'EDIT':
                    return True
        return False


    def execute(self, context):
        objs = context.selected_editable_objects[:]
        for obj in objs:
            solver_v3(obj)
        return {'FINISHED'}


def solver_v3(obj):

    obj.update_from_editmode()
    mesh = obj.data
    bm = bmesh.from_edit_mesh(mesh)
    face = bm.faces.active

    # Validate
    if not face:
        bpy.ops.hops.display_notification(info="No Active Face")
        return

    # convert everything into world space
    center = obj.matrix_world @ face.calc_center_bounds()
    verts_world = [obj.matrix_world @ v.co for v in face.verts]
    normal = face.normal
    normal = obj.matrix_world.inverted().transposed() @ normal
    normal.normalize() # if matrix has scale, the normal won't be unit length

    # flip normal for object to be above floor
    face_matrix = normal.to_track_quat('-Z', 'Y').to_matrix().to_4x4()
    face_matrix.translation = center

    #2d version of vert coordinates in face space
    verts2d = [(face_matrix.inverted() @ v).to_2d() for v in verts_world]
    fit_angle = geometry.box_fit_2d(verts2d)
    face_matrix = face_matrix @ Matrix.Rotation(-fit_angle, 4,'Z')


    #  there is no scale to worry about
    loc = face_matrix.translation
    pivot = Matrix.Translation(loc)
    transform = face_matrix.copy()
    transform.translation = 0, 0, 0

    # undo transfromation of face plane to place it onto a floor
    transform_inv = transform.inverted()

    obj.matrix_world = pivot @ transform_inv @ pivot.inverted() @ obj.matrix_world

    # project object onto floor
    obj.matrix_world.translation -= Vector((0, 0, loc.z))


class HOPS_OT_FLOOR_OBJECT(bpy.types.Operator):
    bl_idname = "hops.floor_object"
    bl_label = "Hops Floor"
    bl_description = "Project objects to be above floor plane"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT' and context.selected_objects


    def execute (self, context):
        # get nearest vector to floor
        distance = float_info.max
        for obj in context.selected_objects:
            obj.tag = False
            for v in obj.bound_box:
                v = obj.matrix_world @ Vector(v)
                if v.z < distance:
                    distance = v.z

        for obj in context.selected_objects:
            if obj.tag: continue

            obj.tag = True

            # find the top parent of the object that is selected and not offset yet
            parent = obj.parent
            selected_parent = None

            while parent:
                if parent.select_get():
                    if parent.tag: break

                    parent.tag = True
                    selected_parent = parent

                parent = parent.parent

            # if such parent exists, update it instead to preserve child offsets
            obj = selected_parent if selected_parent else obj

            mat = obj.matrix_world.copy()
            mat.translation -= Vector((0, 0, distance))
            obj.matrix_world = mat

        if addon.preference().ui.Hops_extra_info:
            bpy.ops.hops.display_notification(info=F'To_Floor', subtext=f'Objects floored: {len(context.selected_objects)}')

        return {'FINISHED'}