Skip to content
Snippets Groups Projects
space_view3d_panel_measure.py 40.1 KiB
Newer Older
# ##### 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 = {
Luca Bonavita's avatar
Luca Bonavita committed
    "name": "Measure Panel",
    "author": "Buerbaum Martin (Pontiac)",
    "version": (0, 7, 12),
    "blender": (2, 5, 5),
    "api": 33931,
    "location": "View3D > Properties > Measure Panel",
    "description": "Measure distances between objects",
    "warning": "",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/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.

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.

Version history:
v0.7.12 -  Moved setting of properties to callback function
    (it is bad practise to set it in the draw code).
    Fixed distance calculation of parented objects.
    API change: add_modal_handler -> modal_handler_add
    Regression: Had to disable area display for selection with
    more than 2 meshes.
    Fixed Local/Global vert-loc calculations in EditMode.
v0.7.11 - Applied patch by Filiciss Muhgue that fixes the text in quad view.
v0.7.10 - Applied patch by Filiciss Muhgue that (mostly) fixes the quad view.
    Patch link: https://projects.blender.org/tracker/?func=
    detail&atid=127&aid=24932&group_id=9
    Thanks for that!
    Removed (now) unneeded "attr" setting for properties.
v0.7.9 - Updated scene properties for changes in property API.
    See http://lists.blender.org/pipermail/bf-committers/
        2010-September/028654.html
    Synced API changes in/from local copy.
v0.7.8 - Various Py API changes by Campbell ...
    bl_default_closed -> bl_options = {'DEFAULT_CLOSED'}
    x.verts -> x.vertices
    @classmethod    def poll(cls, context)
    No "location" in bl_info->name
    bl_info->api
v0.7.7 - One more change to the callback registration code.
    Now it should finally work as intended.
v0.7.6 - API changes (r885, r886) - register & unregister function
v0.7.5.3 - Small fix for bug in v0.7.5.1
    (location was off when object was moved)
v0.7.5.2 - Changed callback registration back to original code &
    fixed bug in there (use bl_idname instead of bl_label)
v0.7.5.1 - Global mode is now taking rotation into account properly.
v0.7.5 - Fixed lagging and drawing issues.
v0.7.4 - Fixed the modal_handler_add and callback_add code.
    Thanks to jesterKing for pointing that out :-)
v0.7.3.1 - Fixed bug that made all lines in Blender stippled :-)
v0.7.3 - Added display of delta x/y/z value in 3d view.
    * Inspired by warpi's patch here:
    http://blenderartists.org/forum/showpost.php?p=1671033&postcount=47
    * Also added display of dx,dy,dz lines
    * Changed the "dist" colors to something not already used
    by x/y/z axes.
v0.7.2 - Merged changes from trunk (scripts_addons r847):
    * obj.matrix -> obj.matrix_world
    * vert.selected -> vert.select
    * face.selected -> face.select
    * bl_info: warning, wiki_url, tracker_url
    * removed __bpydoc__
    * Use fontid=0 for blf functions. 0 is the default font.
v0.7.1 - Merged changes by Campbell:
    * Fix for API change: Collections like context.selected_objects
    no longer return None for empty lists.
    * Update for mathutils, also stripped some redundant
    conversions (Mostly "Vector()" stuff)
v0.7 - Initial support for drawing lines.
    (Thanks to Algorith for applying my perspective_matrix patch.)
    The distance value (in BUs) is also drawn in the 3D view now.
    Also fixed some wrong calculations of global/local distances.
    Now it's really "what you see is what is calculated".
    Use bl_info for Add-On information.
    Use "3D View" in category & name
    Renamed reenter_editmode to view3d.reenter_editmode.
    Renamed panel_measure.py into space_view3d_panel_measure.py
    Active object is only used for edit-mode now. Measurement
    with exactly one sel. (but not necessarily active) object
    now gets the obj via the sel-object array.
    API change Mathutils -> mathutils (r557)
    Deselecting 1 of 2 objects now works correctly (active object is ignored).
    Force a redraw of the area so disabling the "measure_panel_draw"
    checkbox will clear the line/text.
    Only calculate area (CPU heavy) if a "area" checkbox is enabled.
v0.6.4 - Fixed unneeded meshdata duplication (sometimes crashes Blender).
    The script now correctly calculated the surface area (faceAreaGlobal)
    of scaled meshes.
    http://projects.blender.org/tracker/
    ?func=detail&atid=453&aid=21913&group_id=153
v0.6.3 - Added register & unregister functions.
v0.6.2 - Fixed precision of second area property.
    Reduced display precision to 5 (instead of 6).
    Added (commented out code) for shortcut [F5] for
    updating EditMode selection & calculation.
    Changed the script so it can be managed from the "Add-Ons" tab
    in the user preferences.
    Corrected FSF address.
v0.6.1 - Updated reenter_editmode operator description.
    Fixed search for selected mesh objects.
    Added "BU^2" after values that are not yet translated via "unit".
v0.6
    *) Fix:  Removed EditMode/ObjectMode toggle stuff. This causes all the
       crashes and is generally not stable.
       Instead I've added a manual "refresh" button.
       I registered a new operator OBJECT_OT_reenter_editmode for this.
    *) Use "unit" settings (i.e. none/metric/imperial)
    *) Fix: Only display surface area (>=3 objects) if return value is >=0.
    *) Minor: Renamed objectFaceArea to objectSurfaceArea
    *) Updated Vector() and tuple() usage.
    *) Fixed some comments.
v0.5 - Global surface area (object mode) is now calculated as well.
    Support area calculation for face selection.
    Also made measurement panel closed by default. (Area calculation
    may use up a lot of CPU/RAM in extreme cases)
v0.4.1 - Various cleanups.
    Using the shorter "scene" instead of "context.scene"
    New functions measureGlobal() and measureLocal() for
    user-friendly access to the "space" setting.
v0.4 - Calculate & display the surface area of mesh
    objects (local space only right now).
    Expanded global/local switch.
    Made "local" option for 3Dcursor-only in edit mode actually work.
    Fixed local/global calculation for 3Dcursor<->vertex in edit mode.
v0.3.2 - Fixed calculation & display of local/global coordinates.
    The user can now select via dropdown which space is wanted/needed
    Basically this is a bugfix and new feature at the same time :-)
v0.3.1 - Fixed bug where "measure_panel_dist" wasn't defined
    before it was used.
    Also added the distance calculation "origin -> 3D cursor" for edit mode.
v0.3 - Support for mesh edit mode (1 or 2 selected vertices)
v0.2.1 - Small fix (selecting nothing didn't calculate the distance
    of the cursor from the origin anymore)
v0.2 - Distance value is now displayed via a FloatProperty widget (and
    therefore saved to file too right now [according to ideasman42].
    The value is save inside the scene right now.)
    Thanks goes to ideasman42 (Campbell Barton) for helping me out on this.
v0.1 - Initial revision. Seems to work fine for most purposes.

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 mathutils import Vector, Matrix
import bgl
import blf


# 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)


# Returns a single selected object.
# Returns None if more than one (or nothing) is selected.
# Note: Ignores the active object.
def getSingleObject(context):
    if len(context.selected_objects) == 1:
        return 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

    # Get a single selected object (or nothing).
    obj = getSingleObject(context)

    if (context.mode == 'EDIT_MESH'):
        obj = context.active_object

        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 = vert_loc * obj.matrix_world
                    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 = vert1_loc * obj.matrix_world
                    p2 = vert2_loc * obj.matrix_world
                    return (p1, p2, COLOR_GLOBAL)

            else:
                return None

    elif (context.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)

        elif (obj):
            # 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 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_MeshFace_area_get
# @sa: math_geom.c:area_quad_v3
# @sa: math_geom.c:area_tri_v3
def faceAreaGlobal(face, obj):
    area = 0.0

    mat = obj.matrix_world
    if len(face.vertices) == 4:
        v1, v2, v3, v4 = face.vertices
        v1 = obj.data.vertices[v1]
        v2 = obj.data.vertices[v2]
        v3 = obj.data.vertices[v3]
        v4 = obj.data.vertices[v4]

        # Apply transform matrix to vertex coordinates.
        v1 = v1.co * mat
        v2 = v2.co * mat
        v3 = v3.co * mat
        v4 = v4.co * mat

        vec1 = v2 - v1
        vec2 = v4 - v1

        n = vec1.cross(vec2)

        area = n.length / 2.0

        vec1 = v4 - v3
        vec2 = v2 - v3

        n = vec1.cross(vec2)

        area += n.length / 2.0

    elif len(face.vertices) == 3:
        v1, v2, v3 = face.vertices
        v1 = obj.data.vertices[v1]
        v2 = obj.data.vertices[v2]
        v3 = obj.data.vertices[v3]

        # Apply transform matrix to vertex coordinates.
        v1 = v1.co * mat
        v2 = v2.co * mat
        v3 = v3.co * mat

        vec1 = v3 - v2
        vec2 = v1 - v2

        n = vec1.cross(vec2)

        area = n.length / 2.0

    return area


# 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):
        areaTotal = 0

        mesh = obj.data

        # Count the area of all the faces.
        for face in mesh.faces:
            if not selectedOnly or face.select:
                if globalSpace:
                    areaTotal += faceAreaGlobal(face, obj)
                else:
                    areaTotal += face.area

        return areaTotal

    # We can not calculate an area for this object.
    return -1


# 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")


# Converts 3D coordinates in a 3DRegion
# into 2D screen coordinates for that region.
def region3d_get_2d_coordinates(context, loc_3d):
    # Get screen information
    mid_x = context.region.width / 2.0
    mid_y = context.region.height / 2.0
    width = context.region.width
    height = context.region.height

    # Get matrices
    vec = Vector((loc_3d[0], loc_3d[1], loc_3d[2], 1.0)) * total_mat

    # dehomogenise
    vec = Vector((
        vec[0] / vec[3],
        vec[1] / vec[3],
        vec[2] / vec[3]))

    x = int(mid_x + vec[0] * width / 2.0)
    y = int(mid_y + vec[1] * height / 2.0)

    return Vector((x, y, 0))


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()

    # Get measured 3D points and colors.
    line = getMeasurePoints(context)
    if (line and draw):
        p1, p2, color = line

        # Get and convert the Perspective Matrix of the current view/region.
        view3d = bpy.context
        region = view3d.region_data
        perspMatrix = region.perspective_matrix
        tempMat = [perspMatrix[i][j] 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)
        width = 1
        bgl.glLineWidth(width)
        # 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.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 = region3d_get_2d_coordinates(context, p2 + (p1 - p2) * 0.5)
        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.
        # 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:", round(dist, PRECISION)),
            ("X:", round(abs(p1[0] - p2[0]), PRECISION)),
            ("Y:", round(abs(p1[1] - p2[1]), PRECISION)),
            ("Z:", round(abs(p1[2] - p2[2]), PRECISION))]

        # 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.

        loc_x = coord_2d[0] + OFFSET_LINE
        loc_y = coord_2d[1]
        for t in texts:
            text = t[0]
            value = str(t[1]) + " BU"

            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
    # Handle mesh surface area calulations
    if (sce.measure_panel_calc_area):
        # Get a single selected object (or nothing).
        obj = getSingleObject(context)

        if (context.mode == 'EDIT_MESH'):
            obj = 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.
                mesh = 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 mesh.vertices if v.select == 1]

                if len(verts_selected) >= 3:
                    # Get selected faces
                    # @todo: Better (more efficient) way to do this?
                    faces_selected = [f for f in mesh.faces
                        if f.select == 1]

                    if len(faces_selected) > 0:
                        area = objectSurfaceArea(obj, True,
                            measureGlobal(sce))
                        if (area >= 0):
                            sce.measure_panel_area1 = area

        elif (context.mode == 'OBJECT'):
            # We are working in object mode.

            if len(context.selected_objects) > 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(context.selected_objects) == 2:
                # 2 objects selected.

                obj1, obj2 = context.selected_objects

                # Calculate surface area of the objects.
                area1 = objectSurfaceArea(obj1, False, measureGlobal(sce))
                area2 = objectSurfaceArea(obj2, False, measureGlobal(sce))
                sce.measure_panel_area1 = area1
                sce.measure_panel_area2 = area2

            elif (obj):
                # One object selected.

                # Calculate surface area of the object.
                area = objectSurfaceArea(obj, False, measureGlobal(sce))
                if (area >= 0):
                    sce.measure_panel_area1 = area


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':
                        context.window_manager.modal_handler_add(self)
                        self._handle = WINregion.callback_add(
                            draw_measurements_callback,
                            (self, context),
                            'POST_PIXEL')
                        print("Measure panel display callback added")

        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}


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

        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')
            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.
        if (context.area.type == 'VIEW_3D' and
            (context.mode == 'EDIT_MESH'
            or context.mode == 'OBJECT')):
            return 1

        return 0

    def draw_header(self, context):
        layout = self.layout
        sce = context.scene
        # Force a redraw.
        # This prevents the lines still be drawn after
        # disabling the "measure_panel_draw" checkbox.
        # @todo Better solution?
        context.area.tag_redraw()

        # Execute operator (this adds the callback)
        # if it wasn't done yet.
        bpy.ops.view3d.display_measurements()
        bpy.types.Scene.measure_panel_draw = bpy.props.BoolProperty(
            description="Draw distances in 3D View",
            default=1)

        # Define property for the calc-area setting.
        # @todo prevent double calculations for each refresh automatically?
        bpy.types.Scene.measure_panel_calc_area = bpy.props.BoolProperty(
            description="Calculate mesh surface area (heavy CPU" \
                " usage on bigger meshes)",
            default=0)

        layout.prop(sce, "measure_panel_draw")

    def draw(self, context):
        layout = self.layout
        sce = context.scene

        # Get a single selected object (or nothing).
        obj = getSingleObject(context)

        # Define a temporary attribute for the distance value
        bpy.types.Scene.measure_panel_dist = bpy.props.FloatProperty(
            name="Distance",
            precision=PRECISION,
            unit="LENGTH")
        bpy.types.Scene.measure_panel_area1 = bpy.props.FloatProperty(
        bpy.types.Scene.measure_panel_area2 = bpy.props.FloatProperty(
            precision=PRECISION,
            unit="AREA")

        TRANSFORM = [
            ("measure_global", "Global",
                "Calculate values in global space."),
            ("measure_local", "Local",
                "Calculate values inside the local object space.")]

        # Define dropdown for the global/local setting
        bpy.types.Scene.measure_panel_transform = bpy.props.EnumProperty(
            name="Space",
            description="Choose in which space you want to measure.",
            items=TRANSFORM,
            default='measure_global')

        if (context.mode == 'EDIT_MESH'):
            obj = 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.
                mesh = 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 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.

                    row = layout.row()
                    row.prop(sce, "measure_panel_dist")

                    row = layout.row()
                    row.label(text="", icon='CURSOR')
                    row.label(text="", icon='ARROW_LEFTRIGHT')
                    if measureLocal(sce):
                        row.label(text="Obj. Center")
                    else:
                        row.label(text="Origin [0,0,0]")

                    row = layout.row()
                    row.operator("view3d.reenter_editmode",
                        text="Update selection & distance")
#                       @todo
#                        description="The surface area value can" \
#                            " not be updated in mesh edit mode" \
#                            " automatically. Press this button" \
#                            " to do this manually, after you changed" \
#                            " the selection.")

                    row = layout.row()
                    row.prop(sce,
                        "measure_panel_transform",
                        expand=True)

                elif len(verts_selected) == 1:
                    # One vertex selected.
                    # We measure the distance from the
                    # selected vertex object to the 3D cursor.

                    row = layout.row()
                    row.prop(sce, "measure_panel_dist")

                    row = layout.row()
                    row.label(text="", icon='CURSOR')
                    row.label(text="", icon='ARROW_LEFTRIGHT')
                    row.label(text="", icon='VERTEXSEL')

                    row = layout.row()
                    row.operator("view3d.reenter_editmode",
                        text="Update selection & distance")

                    row = layout.row()
                    row.prop(sce,
                        "measure_panel_transform",
                        expand=True)

                elif len(verts_selected) == 2:
                    # Two vertices selected.
                    # We measure the distance between the
                    # two selected vertices.

                    row = layout.row()
                    row.prop(sce, "measure_panel_dist")

                    row = layout.row()
                    row.label(text="", icon='VERTEXSEL')
                    row.label(text="", icon='ARROW_LEFTRIGHT')
                    row.label(text="", icon='VERTEXSEL')

                    row = layout.row()
                    row.operator("view3d.reenter_editmode",
                        text="Update selection & distance")

                    row = layout.row()
                    row.prop(sce,
                        "measure_panel_transform",
                        expand=True)

                else:
                    row = layout.row()
                    row.prop(sce, "measure_panel_calc_area",
                        text="Surface area (selected faces):")

                    if (sce.measure_panel_calc_area):
                        # Get selected faces
                        # @todo: Better (more efficient) way to do this?
                        faces_selected = [f for f in mesh.faces
                            if f.select == 1]
                            if (sce.measure_panel_area1 >= 0):
                                row = layout.row()
                                row.label(
                                    text=str(len(faces_selected)),
                                    icon='FACESEL')
                                row.prop(sce, "measure_panel_area1")

                                row = layout.row()
                                row.operator("view3d.reenter_editmode",
                                    text="Update selection & area")

                                row = layout.row()
                                row.prop(sce,
                                    "measure_panel_transform",
                                    expand=True)

                        else:
                            row = layout.row()
                            row.label(text="Selection not supported.",
                                icon='INFO')

                            row = layout.row()
                            row.operator("view3d.reenter_editmode",
                                text="Update selection")

                    else:
                        row = layout.row()
                        row.operator("view3d.reenter_editmode",
                            text="Update selection")

        elif (context.mode == 'OBJECT'):
            # We are working in object mode.

            if len(context.selected_objects) > 2:
                # We have more that 2 objects selected...

                row = layout.row()
                row.prop(sce, "measure_panel_calc_area",
                        text="Surface area (selected faces):")

                if (sce.measure_panel_calc_area):
                    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.

                        # Calculate and display surface area of the objects.
                        # @todo: Convert to scene units! We do not have a
                        # FloatProperty field here for automatic conversion.

                        row = layout.row()
                        row.label(text="Multiple objects not yet supported",
                            icon='INFO')
                        row = layout.row()
                        row.label(text="(= More than two meshes)",
                            icon='INFO')
# @todo Make this work again.
#                        for o in mesh_objects:
#                            area = objectSurfaceArea(o, False,
#                                measureGlobal(sce))
#                            if (area >= 0):