Skip to content
Snippets Groups Projects
space_view3d_panel_measure.py 53.1 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 #####
    
    #
    # Uses volume calculation and manifold check code (GPL2+) from:
    # http://www.shapeways.com/forum/index.php?t=msg&goto=3639
    # Shapeways Volume Calculator by Benjamin Lauritzen (Loonsbury)
    #
    # #################################
    
    bl_info = {
    
    Luca Bonavita's avatar
    Luca Bonavita committed
        "name": "Measure Panel",
    
        "author": "Buerbaum Martin (Pontiac), TNae (Normal patch)," \
    
            " Benjamin Lauritzen (Loonsbury; Volume code)," \
            " Alessandro Sala (patch: Units in 3D View)",
    
        "version": (0, 8, 9),
    
        "blender": (2, 6, 0),
    
        "location": "View3D > Properties > Measure Panel",
    
        "description": "Measure distances between objects",
    
        "warning": "Script needs repairs",
    
        "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" \
    
            "Scripts/3D_interaction/Panel_Measure",
        "tracker_url": "https://projects.blender.org/tracker/index.php?" \
    
            "func=detail&aid=21445",
    
        "category": "3D View"}
    
    Measure panel
    
    This script displays in OBJECT MODE:
    * The distance of the 3D cursor to the origin of the
      3D space (if NOTHING is selected).
    * The distance of the 3D cursor to the center of an object
      (if exactly ONE object is selected).
    * The distance between 2 object centers
      (if exactly TWO objects are selected).
    * The surface area of any selected mesh object.
    
    * The average normal of the mesh surface of any selected mesh object.
    * The volume of any selected mesh object.
    
    
    Display in EDIT MODE (Local and Global space supported):
    * The distance of the 3D cursor to the origin
      (in Local space it is the object center instead).
    * The distance of the 3D cursor to a selected vertex.
    * The distance between 2 selected vertices.
    
    Usage:
    
    This functionality can be accessed via the
    "Properties" panel in 3D View ([N] key).
    
    It's very helpful to use one or two "Empty" objects with
    "Snap during transform" enabled for fast measurement.
    
    
    More links:
    http://gitorious.org/blender-scripts/blender-measure-panel-script
    http://blenderartists.org/forum/showthread.php?t=177800
    
    import bpy
    from bpy.props import *
    
    from bpy.app.handlers import persistent
    
    from mathutils import Vector, Matrix
    import bgl
    import blf
    
    from bpy_extras.view3d_utils import location_3d_to_region_2d
    
    from bpy_extras.mesh_utils import ngon_tessellate
    
    # Precicion for display of float values.
    PRECISION = 4
    
    # Name of the custom properties as stored in the scene.
    
    COLOR_LOCAL = (1.0, 0.5, 0.0, 0.8)
    COLOR_GLOBAL = (0.5, 0.0, 1.0, 0.8)
    
    # 3D View - text offset
    OFFSET_LINE = 10   # Offset the text a bit to the right.
    OFFSET_Y = 15      # Offset of the lines.
    OFFSET_VALUE = 30  # Offset of value(s) from the text.
    
    # 3D View - line width
    LINE_WIDTH_XYZ = 1
    LINE_WIDTH_DIST = 2
    
    
    # Returns a tuple describing the current measuring system
    # and formatting options.
    # Returned data is meant to be passed to formatDistance().
    # Original by Alessandro Sala (Feb, 12th 2012)
    def getUnitsInfo():
            scale = bpy.context.scene.unit_settings.scale_length
            unit_system = bpy.context.scene.unit_settings.system
            separate_units = bpy.context.scene.unit_settings.use_separate
            if unit_system == 'METRIC':
                    scale_steps = ((1000, 'km'), (1, 'm'), (1 / 100, 'cm'),
                        (1 / 1000, 'mm'), (1 / 1000000, '\u00b5m'))
            elif unit_system == 'IMPERIAL':
                    scale_steps = ((1760, 'mi'), (1, 'yd'), (1 / 3, '\''),
                        (1 / 36, '"'), (1 / 36000, 'thou'))
                    scale *= 1.0936133
            else:
                    scale_steps = ((1, ' BU'),)
                    separate_units = False
    
            return (scale, scale_steps, separate_units)
    
    
    # Converts a distance from BU into the measuring system
    # described by units_info.
    # Original by Alessandro Sala (Feb, 12th 2012)
    def convertDistance(val, units_info):
            scale, scale_steps, separate_units = units_info
            sval = val * scale
            rsval = round(sval, PRECISION)
            idx = 0
            while idx < len(scale_steps) - 1:
                    if rsval >= scale_steps[idx][0]:
                            break
                    idx += 1
            factor, suffix = scale_steps[idx]
            sval /= factor
            if not separate_units or idx == len(scale_steps) - 1:
                    dval = str(round(sval, PRECISION)) + suffix
            else:
                    ival = int(sval)
                    dval = str(ival) + suffix
                    fval = sval - ival
                    idx += 1
                    while idx < len(scale_steps):
                            fval *= scale_steps[idx - 1][0] / scale_steps[idx][0]
                            if fval >= 1:
                                    dval += ' ' \
                                        + str(round(fval, 1)) \
                                        + scale_steps[idx][1]
                                    break
                            idx += 1
    
            return dval
    
    
    
    # Returns a single selected object.
    # Returns None if more than one (or nothing) is selected.
    # Note: Ignores the active object.
    
    def getSingleObject():
        if len(bpy.context.selected_objects) == 1:
            return bpy.context.selected_objects[0]
    
    
        return None
    
    
    # Returns a list with 2 3D points (Vector) and a color (RGBA)
    # depending on the current view mode and the selection.
    def getMeasurePoints(context):
        sce = context.scene
    
        mode = context.mode
    
    
        # Get a single selected object (or nothing).
    
        obj = getSingleObject()
    
        if mode == 'EDIT_MESH':
    
            if obj and obj.type == 'MESH' and obj.data:
    
                # Get mesh data from Object.
                mesh = obj.data
    
                # Get the selected vertices.
                # @todo: Better (more efficient) way to do this?
    
                verts_selected = [v for v in mesh.vertices if v.select == 1]
    
    
                if len(verts_selected) == 0:
                    # Nothing selected.
                    # We measure the distance from...
                    # local  ... the object center to the 3D cursor.
                    # global ... the origin to the 3D cursor.
                    cur_loc = sce.cursor_location
    
                    obj_loc = obj.matrix_world.to_translation()
    
    
                    # Convert to local space, if needed.
                    if measureLocal(sce):
                        p1 = cur_loc
                        p2 = obj_loc
                        return (p1, p2, COLOR_GLOBAL)
    
                    else:
                        p1 = Vector((0.0, 0.0, 0.0))
                        p2 = cur_loc
                        return (p1, p2, COLOR_GLOBAL)
    
                elif len(verts_selected) == 1:
                    # One vertex selected.
                    # We measure the distance from the
                    # selected vertex object to the 3D cursor.
                    cur_loc = sce.cursor_location
                    vert_loc = verts_selected[0].co.copy()
    
                    # Convert to local or global space.
                    if measureLocal(sce):
    
                        p1 = vert_loc
    
                        p1 = obj.matrix_world * vert_loc
    
                        p2 = cur_loc
                        return (p1, p2, COLOR_GLOBAL)
    
                elif len(verts_selected) == 2:
                    # Two vertices selected.
                    # We measure the distance between the
                    # two selected vertices.
    
                    obj_loc = obj.matrix_world.to_translation()
    
                    vert1_loc = verts_selected[0].co.copy()
                    vert2_loc = verts_selected[1].co.copy()
    
                    # Convert to local or global space.
                    if measureLocal(sce):
    
                        p1 = vert1_loc
                        p2 = vert2_loc
    
                        p1 = obj.matrix_world * vert1_loc
                        p2 = obj.matrix_world * vert2_loc
    
        elif mode == 'OBJECT':
    
            # We are working in object mode.
    
    
            if len(context.selected_objects) > 2:
                return None
            elif len(context.selected_objects) == 2:
                # 2 objects selected.
                # We measure the distance between the 2 selected objects.
                obj1, obj2 = context.selected_objects
    
                obj1_loc = obj1.matrix_world.to_translation()
                obj2_loc = obj2.matrix_world.to_translation()
    
                return (obj1_loc, obj2_loc, COLOR_GLOBAL)
    
    
                # One object selected.
                # We measure the distance from the object to the 3D cursor.
                cur_loc = sce.cursor_location
    
                obj_loc = obj.matrix_world.to_translation()
    
                return (obj_loc, cur_loc, COLOR_GLOBAL)
    
            elif not context.selected_objects:
                # Nothing selected.
                # We measure the distance from the origin to the 3D cursor.
                p1 = Vector((0.0, 0.0, 0.0))
                p2 = sce.cursor_location
                return (p1, p2, COLOR_GLOBAL)
    
            else:
                return None
    
    
    
    # Return the length of an edge (in global space if "obj" is set).
    # Respects the scaling (via the "obj.matrix_world" parameter).
    def edgeLengthGlobal(edge, obj, globalSpace):
        v1, v2 = edge.vertices
    
        # Get vertex data
        v1 = obj.data.vertices[v1]
        v2 = obj.data.vertices[v2]
    
        if globalSpace:
            mat = obj.matrix_world
            # Apply transform matrix to vertex coordinates.
            v1 = mat * v1.co
            v2 = mat * v2.co
        else:
            v1 = v1.co
            v2 = v2.co
    
        return (v1 - v2).length
    
    
    # Calculate the edge length of a mesh object.
    # *) Set selectedOnly=1 if you only want to count selected edges.
    # *) Set globalSpace=1 if you want to calculate
    #    the global edge length (object mode).
    # Note: Be sure you have updated the mesh data before
    #       running this with selectedOnly=1!
    # @todo Support other object types (surfaces, etc...)?
    def objectEdgeLength(obj, selectedOnly, globalSpace):
    
        if obj and obj.type == 'MESH' and obj.data:
    
            edgeTotal = 0
    
            mesh = obj.data
    
            # Count the length of all edges.
            for ed in mesh.edges:
                if not selectedOnly or ed.select:
                    edgeTotal += edgeLengthGlobal(ed, obj, globalSpace)
    
            return edgeTotal
    
        # We can not calculate a length for this object.
        return -1
    
    
    
    # Return the area of a face (in global space).
    # @note Copies the functionality of the following functions,
    
    # but also respects the scaling (via the "obj.matrix_world" parameter):
    
    # @sa: rna_mesh.c:rna_MeshTessFace_area_get
    
    # @sa: math_geom.c:area_quad_v3
    # @sa: math_geom.c:area_tri_v3
    
    # @sa: math_geom.c:area_poly_v3
    # @todo Fix calculation of "n" for n-gons?
    def polyAreaGlobal(poly, obj):
    
        mesh = obj.data
        mat = obj.matrix_world.copy()
    
        norm = poly.normal
    
    
        area = 0.0
    
        if len(poly.vertices) > 3:
            # Tesselate the polygon into multiple tris
    
            tris = ngon_tessellate(mesh, poly.vertices)
    
            for tri in tris:
                # Get vertex data
                v1, v2, v3 = tri
    
                # Get indices from original poly
                v1 = poly.vertices[v1]
                v2 = poly.vertices[v2]
                v3 = poly.vertices[v3]
    
                # Get vertex information from indices
                v1 = mesh.vertices[v1]
                v2 = mesh.vertices[v2]
                v3 = mesh.vertices[v3]
    
                # Apply transform matrix to vertex coordinates.
                v1 = mat * v1.co
                v2 = mat * v2.co
                v3 = mat * v3.co
    
                # Calculate area for the new tri
                vec1 = v3 - v2
                vec2 = v1 - v2
    
                n = vec1.cross(vec2)
    
                area += n.length / 2.0
    
        elif len(poly.vertices) == 3:
    
            v1, v2, v3 = poly.vertices
    
            v1 = mesh.vertices[v1]
            v2 = mesh.vertices[v2]
            v3 = mesh.vertices[v3]
    
    
            # Apply transform matrix to vertex coordinates.
    
            v1 = mat * v1.co
            v2 = mat * v2.co
            v3 = mat * v3.co
    
    
            vec1 = v3 - v2
            vec2 = v1 - v2
    
            n = vec1.cross(vec2)
    
            area = n.length / 2.0
    
    
        # Apply rotation and scale to the normal as well.
        rot_mat = obj.matrix_world.to_quaternion()
        scale = obj.matrix_world.to_scale()
        norm = rot_mat * norm
        norm = Vector((
            norm.x * scale.x,
            norm.y * scale.y,
            norm.z * scale.z)).normalized()
    
    
        return area, norm
    
    
    
    # Calculate the surface area of a mesh object.
    # *) Set selectedOnly=1 if you only want to count selected faces.
    # *) Set globalSpace=1 if you want to calculate
    #    the global surface area (object mode).
    # Note: Be sure you have updated the mesh data before
    #       running this with selectedOnly=1!
    # @todo Support other object types (surfaces, etc...)?
    def objectSurfaceArea(obj, selectedOnly, globalSpace):
    
        if obj and obj.type == 'MESH' and obj.data:
    
            normTotal = Vector((0.0, 0.0, 0.0))
    
    
            mesh = obj.data
    
            # Count the area of all the faces.
    
            for poly in mesh.polygons:
                if not selectedOnly or poly.select:
    
                        a, n = polyAreaGlobal(poly, obj)
    
                        areaTotal += a
                        normTotal += n
    
                        areaTotal += poly.area
                        normTotal += poly.normal
    
            return areaTotal, normTotal
    
    
        # We can not calculate an area for this object.
    
        return -1, Vector((0.0, 0.0, 0.0))
    
    # Calculate the volume of a mesh object.
    # Copyright Loonsbury (loonsbury@yahoo.com)
    def objectVolume(obj, globalSpace):
    
        if obj and obj.type == 'MESH' and obj.data:
    
    
            # Check if mesh is non-manifold
            if not checkManifold(obj):
                return -1
    
    
            # Check if mesh has n-gons
            if checkNgon(obj):
                return -2
    
    
            mesh = obj.data
    
            volTot = 0
    
    
            for poly in mesh.polygons:
                fzn = poly.normal.z
    
                if len(poly.vertices) == 4:
                    v1, v2, v3, v4 = poly.vertices
    
                    v1, v2, v3 = poly.vertices
    
                v1 = mesh.vertices[v1]
                v2 = mesh.vertices[v2]
                v3 = mesh.vertices[v3]
    
    
                # Scaled vert coordinates with object XYZ offsets for
                # selection extremes/sizing.
                if globalSpace:
                    x1 = v1.co[0] * obj.scale[0] + obj.location[0]
                    y1 = v1.co[1] * obj.scale[1] + obj.location[1]
                    z1 = v1.co[2] * obj.scale[2] + obj.location[2]
    
                    x2 = v2.co[0] * obj.scale[0] + obj.location[0]
                    y2 = v2.co[1] * obj.scale[1] + obj.location[1]
                    z2 = v2.co[2] * obj.scale[2] + obj.location[2]
    
                    x3 = v3.co[0] * obj.scale[0] + obj.location[0]
                    y3 = v3.co[1] * obj.scale[1] + obj.location[1]
                    z3 = v3.co[2] * obj.scale[2] + obj.location[2]
    
                else:
                    x1, y1, z1 = v1.co
                    x2, y2, z2 = v2.co
                    x3, y3, z3 = v3.co
    
                pa = 0.5 * abs(
                    (x1 * (y3 - y2))
                    + (x2 * (y1 - y3))
                    + (x3 * (y2 - y1)))
                volume = ((z1 + z2 + z3) / 3.0) * pa
    
                # Allowing for quads
    
                if len(poly.vertices) == 4:
    
                    # Get vertex data
    
    
                    if globalSpace:
                        x4 = v4.co[0] * obj.scale[0] + obj.location[0]
                        y4 = v4.co[1] * obj.scale[1] + obj.location[1]
                        z4 = v4.co[2] * obj.scale[2] + obj.location[2]
    
                    else:
                        x4, y4, z4 = v4.co
    
                    pa = 0.5 * abs(
                        (x1 * (y4 - y3))
                        + (x3 * (y1 - y4))
                        + (x4 * (y3 - y1)))
    
                    volume += ((z1 + z3 + z4) / 3.0) * pa
    
                if fzn < 0:
                    fzn = -1
    
                elif fzn > 0:
                    fzn = 1
    
                else:
                    fzn = 0
    
                volTot += fzn * volume
    
            return volTot
    
    #    else:
    #        print obj.name, ': Object must be a mesh!'        # TODO
    
    
        return -3
    
    
    
    # Manifold Checks
    # Copyright Loonsbury (loonsbury@yahoo.com)
    def checkManifold(obj):
    
        if obj and obj.type == 'MESH' and obj.data:
    
            mesh = obj.data
    
            mc = dict([(ed.key, 0) for ed in mesh.edges])     # TODO
    
    
            for p in mesh.polygons:
                for ek in p.edge_keys:
    
                    mc[ek] += 1
                    if mc[ek] > 2:
                        return 0
    
            mt = [e[1] for e in mc.items()]
            mt.sort()
    
            if mt[0] < 2:
                return 0
    
            if mt[len(mt) - 1] > 2:
                return 0
    
            return 1
    
        else:
            return -1
    
    
    Martin Buerbaum's avatar
    Martin Buerbaum committed
    
    
    # Check if a mesh has n-gons (polygon with more than 4 edges).
    def checkNgon(obj):
        if obj and obj.type == 'MESH' and obj.data:
            mesh = obj.data
    
            for p in mesh.polygons:
                if len(p.vertices) > 4:
                    return 1
    
            return 0
    
        else:
            return -1
    
    Martin Buerbaum's avatar
    Martin Buerbaum committed
    
    
    # User friendly access to the "space" setting.
    def measureGlobal(sce):
        return (sce.measure_panel_transform == "measure_global")
    
    
    # User friendly access to the "space" setting.
    def measureLocal(sce):
        return (sce.measure_panel_transform == "measure_local")
    
    
    
    # Calculate values if geometry, selection or cursor changed.
    @persistent
    def scene_update(context):
        sce = context
        mode = bpy.context.mode
    
        if (mode == 'EDIT_MESH' and not sce.measure_panel_update):
            return
    
        if (bpy.data.objects.is_updated
            or bpy.context.scene.is_updated
            or sce.measure_panel_update):
            # TODO: Better way to check selection changes and cursor changes?
    
            sel_objs = bpy.context.selected_objects
    
            # EDGE LENGTH
            if sce.measure_panel_calc_edge_length:
                if (mode == 'EDIT_MESH'
                    and sce.measure_panel_update):
                    sce.measure_panel_update = 0
                    obj = context.active_object
    
                    #if obj.is_updated:
                    length_total = objectEdgeLength(obj, True,
                        measureGlobal(sce))
                    sce.measure_panel_edge_length = length_total
    
                elif mode == 'OBJECT':
                    length_total = -1
    
                    for o in sel_objs:
                        if o.type == 'MESH':
                            length = objectEdgeLength(o, False, measureGlobal(sce))
    
                            if length >= 0:
                                if length_total < 0:
                                    length_total = 0
    
                                length_total += length
    
                    sce.measure_panel_edge_length = length_total
    
            # AREA
            # Handle mesh surface area calulations
            if sce.measure_panel_calc_area:
                if (mode == 'EDIT_MESH'
                    and sce.measure_panel_update):
                    sce.measure_panel_update = 0
                    obj = bpy.context.active_object
    
                    if obj and obj.type == 'MESH' and obj.data:
                        # "Note: a Mesh will return the selection state of the mesh
                        # when EditMode was last exited. A Python script operating
                        # in EditMode must exit EditMode before getting the current
                        # selection state of the mesh."
                        # http://www.blender.org/documentation/249PythonDoc/
                        # /Mesh.MVert-class.html#sel
                        # We can only provide this by existing &
                        # re-entering EditMode.
                        # @todo: Better way to do this?
    
                        # Get mesh data from Object.
                        me = obj.data
    
                        # Get transformation matrix from object.
                        ob_mat = obj.matrix_world
                        # Also make an inversed copy! of the matrix.
                        ob_mat_inv = ob_mat.copy()
                        Matrix.invert(ob_mat_inv)
    
                        # Get the selected vertices.
                        # @todo: Better (more efficient) way to do this?
                        verts_selected = [v for v in me.vertices if v.select == 1]
    
                        if len(verts_selected) >= 3:
                            # Get selected faces
                            # @todo: Better (more efficient) way to do this?
                            polys_selected = [p for p in me.polygons
                                if p.select == 1]
    
                            if len(polys_selected) > 0:
                                area, normal = objectSurfaceArea(obj, True,
                                    measureGlobal(sce))
                                if area >= 0.0:
                                    sce.measure_panel_area1 = area
                                    sce.measure_panel_normal1 = normal
    
                elif mode == 'OBJECT':
                    # We are working in object mode.
    
                    # Get a single selected object (or nothing).
                    obj = getSingleObject()
    
                    if len(sel_objs) > 2:
                        return
    # @todo Make this work again.
    #                    # We have more that 2 objects selected...
    #
    #                    mesh_objects = [o for o in context.selected_objects
    #                       if o.type == 'MESH']
    
    #                    if len(mesh_objects) > 0:
    #                        # ... and at least one of them is a mesh.
    #
    #                        for o in mesh_objects:
    #                            area = objectSurfaceArea(o, False,
    #                                measureGlobal(sce))
    #                           if area >= 0:
    #                               #row.label(text=o.name, icon='OBJECT_DATA')
    #                               #row.label(text=str(round(area, PRECISION))
    #                               #    + " BU^2")
    
                    elif len(sel_objs) == 2:
                        # 2 objects selected.
    
                        obj1, obj2 = sel_objs
    
                        # Calculate surface area of the objects.
                        area1, normal1 = objectSurfaceArea(obj1, False,
                            measureGlobal(sce))
                        area2, normal2 = objectSurfaceArea(obj2, False,
                            measureGlobal(sce))
                        sce.measure_panel_area1 = area1
                        sce.measure_panel_area2 = area2
                        sce.measure_panel_normal1 = normal1
                        sce.measure_panel_normal2 = normal2
    
                    elif obj:
                        # One object selected.
    
                        # Calculate surface area of the object.
                        area, normal = objectSurfaceArea(obj, False,
                            measureGlobal(sce))
    
                        sce.measure_panel_area1 = area
                        sce.measure_panel_normal1 = normal
    
            # VOLUME
            # Handle mesh volume calulations.
            if sce.measure_panel_calc_volume:
                obj = getSingleObject()
    
                if mode == 'OBJECT':
                    # We are working in object mode.
    
                    #if len(sel_objs) > 2:       # TODO
                    #el
                    if len(sel_objs) == 2:
                        # 2 objects selected.
    
                        obj1, obj2 = sel_objs
    
                        # Calculate surface area of the objects.
                        volume1 = objectVolume(obj1, measureGlobal(sce))
                        volume2 = objectVolume(obj2, measureGlobal(sce))
    
                        sce.measure_panel_volume1 = volume1
                        sce.measure_panel_volume2 = volume2
    
                    elif obj:
                        # One object selected.
    
                        # Calculate surface area of the object.
                        volume1 = objectVolume(obj, measureGlobal(sce))
    
                        sce.measure_panel_volume1 = volume1
    
    def draw_measurements_callback(self, context):
        sce = context.scene
    
        draw = 0
        if hasattr(sce, "measure_panel_draw"):
            draw = sce.measure_panel_draw
    
        # 2D drawing code example
        #bgl.glBegin(bgl.GL_LINE_STRIP)
        #bgl.glVertex2i(0, 0)
        #bgl.glVertex2i(80, 100)
        #bgl.glEnd()
    
        if draw:
            # Get measured 3D points and colors.
            line = getMeasurePoints(context)
    
            if line:
                p1, p2, color = line
    
                # Get & convert the Perspective Matrix of the current view/region.
                view3d = bpy.context
                region = view3d.region_data
                perspMatrix = region.perspective_matrix
                tempMat = [perspMatrix[j][i] for i in range(4) for j in range(4)]
                perspBuff = bgl.Buffer(bgl.GL_FLOAT, 16, tempMat)
    
                # ---
                # Store previous OpenGL settings.
                # Store MatrixMode
                MatrixMode_prev = bgl.Buffer(bgl.GL_INT, [1])
                bgl.glGetIntegerv(bgl.GL_MATRIX_MODE, MatrixMode_prev)
                MatrixMode_prev = MatrixMode_prev[0]
    
                # Store projection matrix
                ProjMatrix_prev = bgl.Buffer(bgl.GL_DOUBLE, [16])
                bgl.glGetFloatv(bgl.GL_PROJECTION_MATRIX, ProjMatrix_prev)
    
                # Store Line width
                lineWidth_prev = bgl.Buffer(bgl.GL_FLOAT, [1])
                bgl.glGetFloatv(bgl.GL_LINE_WIDTH, lineWidth_prev)
                lineWidth_prev = lineWidth_prev[0]
    
                # Store GL_BLEND
                blend_prev = bgl.Buffer(bgl.GL_BYTE, [1])
                bgl.glGetFloatv(bgl.GL_BLEND, blend_prev)
                blend_prev = blend_prev[0]
    
                line_stipple_prev = bgl.Buffer(bgl.GL_BYTE, [1])
                bgl.glGetFloatv(bgl.GL_LINE_STIPPLE, line_stipple_prev)
                line_stipple_prev = line_stipple_prev[0]
    
                # Store glColor4f
                color_prev = bgl.Buffer(bgl.GL_FLOAT, [4])
                bgl.glGetFloatv(bgl.GL_COLOR, color_prev)
    
                # ---
                # Prepare for 3D drawing
                bgl.glLoadIdentity()
                bgl.glMatrixMode(bgl.GL_PROJECTION)
                bgl.glLoadMatrixf(perspBuff)
    
                bgl.glEnable(bgl.GL_BLEND)
                bgl.glEnable(bgl.GL_LINE_STIPPLE)
    
                # ---
                # Draw 3D stuff.
                bgl.glLineWidth(LINE_WIDTH_XYZ)
                # X
                bgl.glColor4f(1, 0, 0, 0.8)
                bgl.glBegin(bgl.GL_LINE_STRIP)
                bgl.glVertex3f(p1[0], p1[1], p1[2])
                bgl.glVertex3f(p2[0], p1[1], p1[2])
                bgl.glEnd()
                # Y
                bgl.glColor4f(0, 1, 0, 0.8)
                bgl.glBegin(bgl.GL_LINE_STRIP)
                bgl.glVertex3f(p1[0], p1[1], p1[2])
                bgl.glVertex3f(p1[0], p2[1], p1[2])
                bgl.glEnd()
                # Z
                bgl.glColor4f(0, 0, 1, 0.8)
                bgl.glBegin(bgl.GL_LINE_STRIP)
                bgl.glVertex3f(p1[0], p1[1], p1[2])
                bgl.glVertex3f(p1[0], p1[1], p2[2])
                bgl.glEnd()
    
                # Dist
                bgl.glLineWidth(LINE_WIDTH_DIST)
                bgl.glColor4f(color[0], color[1], color[2], color[3])
                bgl.glBegin(bgl.GL_LINE_STRIP)
                bgl.glVertex3f(p1[0], p1[1], p1[2])
                bgl.glVertex3f(p2[0], p2[1], p2[2])
                bgl.glEnd()
    
                # ---
                # Restore previous OpenGL settings
                bgl.glLoadIdentity()
                bgl.glMatrixMode(MatrixMode_prev)
                bgl.glLoadMatrixf(ProjMatrix_prev)
                bgl.glLineWidth(lineWidth_prev)
                if not blend_prev:
                    bgl.glDisable(bgl.GL_BLEND)
                if not line_stipple_prev:
                    bgl.glDisable(bgl.GL_LINE_STIPPLE)
                bgl.glColor4f(
                    color_prev[0],
                    color_prev[1],
                    color_prev[2],
                    color_prev[3])
    
                # ---
                # Draw (2D) text
                # We do this after drawing the lines so
                # we can draw it OVER the line.
                coord_2d = location_3d_to_region_2d(
                    context.region,
                    context.space_data.region_3d,
                    p1.lerp(p2, 0.5))
                dist = (p1 - p2).length
    
                # Write distance value into the scene property,
                # so we can display it in the panel & refresh the panel.
                if hasattr(sce, "measure_panel_dist"):
                    sce.measure_panel_dist = dist
                    context.area.tag_redraw()
    
                texts = [
                    ("Dist:", dist),
                    ("X:", abs(p1[0] - p2[0])),
                    ("Y:", abs(p1[1] - p2[1])),
                    ("Z:", abs(p1[2] - p2[2]))]
    
                # Draw all texts
                # @todo Get user pref for text color in 3D View
                bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
                blf.size(0, 12, 72)  # Prevent font size to randomly change.
    
                uinfo = getUnitsInfo()
    
                loc_x = coord_2d[0] + OFFSET_LINE
                loc_y = coord_2d[1]
    
                for t in texts:
                    text = t[0]
    
                    value = convertDistance(t[1], uinfo)
    
                    blf.position(0, loc_x, loc_y, 0)
                    blf.draw(0, text)
                    blf.position(0, loc_x + OFFSET_VALUE, loc_y, 0)
                    blf.draw(0, value)
    
                    loc_y -= OFFSET_Y
    
    
    class VIEW3D_OT_display_measurements(bpy.types.Operator):
    
        """Display the measurements made in the 'Measure' panel"""
    
        bl_idname = "view3d.display_measurements"
    
        bl_label = "Display the measurements made in the" \
    
            " 'Measure' panel in the 3D View"
    
        bl_options = {'REGISTER'}
    
        def modal(self, context, event):
            context.area.tag_redraw()
    
            return {'FINISHED'}
    
        def execute(self, context):
            if context.area.type == 'VIEW_3D':
    
    Campbell Barton's avatar
    Campbell Barton committed
                mgr_ops = context.window_manager.operators.values()
    
                if not self.bl_idname in [op.bl_idname for op in mgr_ops]:
    
                    # Add the region OpenGL drawing callback
                    for WINregion in context.area.regions:
                        if WINregion.type == 'WINDOW':
    
                            self._handle = WINregion.callback_add(
                                draw_measurements_callback,
                                (self, context),
                                'POST_PIXEL')
    
                            print("Measure panel display callback added")
    
                            context.window_manager.modal_handler_add(self)
    
    
            else:
                self.report({'WARNING'}, "View3D not found, cannot run operator")
                return {'CANCELLED'}
    
    
    
    class VIEW3D_OT_activate_measure_panel(bpy.types.Operator):
        bl_label = "Activate"
        bl_idname = "view3d.activate_measure_panel"
    
        bl_description = "Activate the callback needed to draw the lines"
    
        bl_options = {'REGISTER'}
    
        def invoke(self, context, event):
    
            # Execute operator (this adds the callback)
            # if it wasn't done yet.
            bpy.ops.view3d.display_measurements()
            return {'FINISHED'}
    
    
    
    class VIEW3D_OT_reenter_editmode(bpy.types.Operator):
        bl_label = "Re-enter EditMode"
        bl_idname = "view3d.reenter_editmode"
    
        bl_description = "Update mesh data of an active mesh object " \
    
            "(this is done by exiting and re-entering mesh edit mode)"
    
        bl_options = {'REGISTER'}
    
        def invoke(self, context, event):
    
            # Get the active object.
            obj = context.active_object
    
            sce = context.scene
    
            if obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH':
    
                # Exit and re-enter mesh EditMode.
                bpy.ops.object.mode_set(mode='OBJECT')
                bpy.ops.object.mode_set(mode='EDIT')
    
                sce.measure_panel_update = 1
    
                return {'FINISHED'}
    
            return {'CANCELLED'}
    
    
    class VIEW3D_PT_measure(bpy.types.Panel):
        bl_space_type = 'VIEW_3D'
        bl_region_type = 'UI'
        bl_label = "Measure"
    
        bl_options = {'DEFAULT_CLOSED'}
    
        @classmethod
        def poll(cls, context):
    
            # Only display this panel in the object and edit mode 3D view.
    
            mode = context.mode
    
            if (context.area.type == 'VIEW_3D' and
    
                (mode == 'EDIT_MESH' or mode == 'OBJECT')):
    
                return 1
    
            return 0
    
        def draw_header(self, context):
            layout = self.layout
            sce = context.scene
    
            mgr_ops = context.window_manager.operators.values()
            if (not "VIEW3D_OT_display_measurements"