# ##### 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 = { "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': 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 p2 = cur_loc return (p1, p2, COLOR_LOCAL) else: 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 return (p1, p2, COLOR_LOCAL) else: p1 = obj.matrix_world * vert1_loc p2 = obj.matrix_world * vert2_loc return (p1, p2, COLOR_GLOBAL) else: return None 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) 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 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: # Triangle # Get vertex indices v1, v2, v3 = poly.vertices # Get vertex data 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: areaTotal = 0 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: if globalSpace: a, n = polyAreaGlobal(poly, obj) areaTotal += a normTotal += n else: 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 else: 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 v4 = mesh.vertices[v4] 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 # 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 # 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': 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) return {'RUNNING_MODAL'} return {'CANCELLED'} 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" in [op.bl_idname for op in mgr_ops]): layout.operator("view3d.activate_measure_panel", text="Activate") def draw(self, context): layout = self.layout sce = context.scene mode = context.mode # Get a single selected object (or nothing). obj = getSingleObject() drawTansformButtons = 1 if mode == 'EDIT_MESH': obj = context.active_object row = layout.row() row.operator("view3d.reenter_editmode", text="Update selection") # @todo # description="The calculated values can" \ # " not be updated in mesh edit mode" \ # " automatically. Press this button" \ # " to do this manually, after you changed" \ # " the selection") 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. layout.label(text="Distance") box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.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]") layout.prop(sce, "measure_panel_draw") elif len(verts_selected) == 1: # One vertex selected. # We measure the distance from the # selected vertex object to the 3D cursor. layout.label(text="Distance") box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.row() row.label(text="", icon='CURSOR') row.label(text="", icon='ARROW_LEFTRIGHT') row.label(text="", icon='VERTEXSEL') layout.prop(sce, "measure_panel_draw") elif len(verts_selected) == 2: # Two vertices selected. # We measure the distance between the # two selected vertices. layout.label(text="Distance") box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.row() row.label(text="", icon='VERTEXSEL') row.label(text="", icon='ARROW_LEFTRIGHT') row.label(text="", icon='VERTEXSEL') layout.prop(sce, "measure_panel_draw") edges_selected = [ed for ed in mesh.edges if ed.select == 1] if len(edges_selected) >= 1: row = layout.row() row.prop(sce, "measure_panel_calc_edge_length", text="Edge Length (selected edges)") if sce.measure_panel_calc_edge_length: if sce.measure_panel_edge_length >= 0: box = layout.box() row = box.row() row.label( text=str(len(edges_selected)), icon='EDGESEL') row = box.row() row.label(text="Length") row.prop(sce, "measure_panel_edge_length") if len(verts_selected) > 2: 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? polys_selected = [p for p in mesh.polygons if p.select == 1] if len(polys_selected) > 0: if sce.measure_panel_area1 >= 0: box = layout.box() row = box.row() row.label( text=str(len(polys_selected)), icon='FACESEL') row = box.row() row.label(text="Area") row.prop(sce, "measure_panel_area1") row = box.row() row.label(text="Normal") row = box.row() row.prop(sce, "measure_panel_normal1") else: row = layout.row() row.label(text="Selection not supported", icon='INFO') if drawTansformButtons: row = layout.row() row.prop(sce, "measure_panel_transform", expand=True) elif mode == 'OBJECT': # We are working in object mode. mesh_objects = [o for o in context.selected_objects if o.type == 'MESH'] if len(context.selected_objects) > 2: # We have more that 2 objects selected... # EDGES row = layout.row() row.prop(sce, "measure_panel_calc_edge_length", text="Edge Length") if sce.measure_panel_calc_edge_length: if len(mesh_objects) > 0: box = layout.box() row = box.row() row.label(text="Total edge length") row.prop(sce, "measure_panel_edge_length") # AREA row = layout.row() row.prop(sce, "measure_panel_calc_area", text="Surface area") if sce.measure_panel_calc_area: 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: # row = layout.row() # 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. # We measure the distance between the 2 selected objects. layout.label(text="Distance") obj1, obj2 = context.selected_objects box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.row() row.label(text="", icon='OBJECT_DATA') row.prop(obj1, "name", text="") row.label(text="", icon='ARROW_LEFTRIGHT') row.label(text="", icon='OBJECT_DATA') row.prop(obj2, "name", text="") layout.prop(sce, "measure_panel_draw") # EDGES row = layout.row() row.prop(sce, "measure_panel_calc_edge_length", text="Edge Length") if sce.measure_panel_calc_edge_length: if sce.measure_panel_edge_length >= 0: if len(mesh_objects) > 0: box = layout.box() row = box.row() row.label(text="Total edge length") row.prop(sce, "measure_panel_edge_length") # AREA row = layout.row() row.prop(sce, "measure_panel_calc_area", text="Surface area") if sce.measure_panel_calc_area: # Display surface area of the objects. if (sce.measure_panel_area1 >= 0 or sce.measure_panel_area2 >= 0): if sce.measure_panel_area1 >= 0: box = layout.box() row = box.row() row.label(text=obj1.name, icon='OBJECT_DATA') row = box.row() row.label(text="Area") row.prop(sce, "measure_panel_area1") row = box.row() row.label(text="Normal") row = box.row() row.prop(sce, "measure_panel_normal1") if sce.measure_panel_area2 >= 0: box = layout.box() row = box.row() row.label(text=obj2.name, icon='OBJECT_DATA') row = box.row() row.label(text="Area") row.prop(sce, "measure_panel_area2") row = box.row() row.label(text="Normal") row = box.row() row.prop(sce, "measure_panel_normal2") # VOL row = layout.row() row.prop(sce, "measure_panel_calc_volume", text="Volume") if sce.measure_panel_calc_volume: # Display volume of the objects. if sce.measure_panel_volume1 >= -2: box = layout.box() row = box.row() row.label(text=obj1.name, icon='OBJECT_DATA') if sce.measure_panel_volume1 >= 0: row = box.row() row.label(text="Volume") row.prop(sce, "measure_panel_volume1") elif sce.measure_panel_volume1 >= -1: row = box.row() row.label(text="Mesh is non-manifold!", icon='INFO') else: # -2 row = box.row() row.label(text="Mesh has n-gons (faces with " \ "more than 4 edges)!", icon='INFO') if sce.measure_panel_volume2 >= -2: box = layout.box() row = box.row() row.label(text=obj2.name, icon='OBJECT_DATA') if sce.measure_panel_volume2 >= 0: row = box.row() row.label(text="Volume") row.prop(sce, "measure_panel_volume2") elif sce.measure_panel_volume2 >= -1: row = box.row() row.label(text="Mesh is non-manifold!", icon='INFO') else: # -2 row = box.row() row.label(text="Mesh has n-gons (faces with " \ "more than 4 edges)!", icon='INFO') elif obj: # One object selected. # We measure the distance from the object to the 3D cursor. layout.label(text="Distance") box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.row() row.label(text="", icon='CURSOR') row.label(text="", icon='ARROW_LEFTRIGHT') row.label(text="", icon='OBJECT_DATA') row.prop(obj, "name", text="") layout.prop(sce, "measure_panel_draw") # EDGES row = layout.row() row.prop(sce, "measure_panel_calc_edge_length", text="Edge Length") if sce.measure_panel_calc_edge_length: if sce.measure_panel_edge_length >= 0: if len(mesh_objects) > 0: box = layout.box() row = box.row() row.label(text="Total edge length") row.prop(sce, "measure_panel_edge_length") # AREA row = layout.row() row.prop(sce, "measure_panel_calc_area", text="Surface area") if sce.measure_panel_calc_area: # Display surface area of the object. if sce.measure_panel_area1 >= 0.0: box = layout.box() row = box.row() row.label(text=obj.name, icon='OBJECT_DATA') row = box.row() row.label(text="Area") row.prop(sce, "measure_panel_area1") row = box.row() row.label(text="Normal") row = box.row() row.prop(sce, "measure_panel_normal1") # VOL row = layout.row() row.prop(sce, "measure_panel_calc_volume", text="Volume") if sce.measure_panel_calc_volume: # Display volume of the objects. if sce.measure_panel_volume1 >= -2: box = layout.box() row = box.row() row.label(text=obj.name, icon='OBJECT_DATA') if sce.measure_panel_volume1 >= 0: row = box.row() row.label(text="Volume") row.prop(sce, "measure_panel_volume1") elif sce.measure_panel_volume1 >= -1: row = box.row() row.label(text="Mesh is non-manifold!", icon='INFO') else: # -2 row = box.row() row.label(text="Mesh has n-gons (faces with " \ "more than 4 edges)!", icon='INFO') elif not context.selected_objects: # Nothing selected. # We measure the distance from the origin to the 3D cursor. layout.label(text="Distance") box = layout.box() row = box.row() row.prop(sce, "measure_panel_dist") row = box.row() row.label(text="", icon='CURSOR') row.label(text="", icon='ARROW_LEFTRIGHT') row.label(text="Origin [0,0,0]") layout.prop(sce, "measure_panel_draw") else: row = layout.row() row.label(text="Selection not supported", icon='INFO') if drawTansformButtons: row = layout.row() row.prop(sce, "measure_panel_transform", expand=True) def register(): bpy.utils.register_module(__name__) bpy.app.handlers.scene_update_post.append(scene_update) # 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_edge_length = bpy.props.FloatProperty( name="", precision=PRECISION, unit="LENGTH") bpy.types.Scene.measure_panel_area1 = bpy.props.FloatProperty( name="", precision=PRECISION, unit="AREA") bpy.types.Scene.measure_panel_area2 = bpy.props.FloatProperty( name="", precision=PRECISION, unit="AREA") bpy.types.Scene.measure_panel_normal1 = bpy.props.FloatVectorProperty( name="", precision=PRECISION, subtype="XYZ") bpy.types.Scene.measure_panel_normal2 = bpy.props.FloatVectorProperty( name="", precision=PRECISION, subtype="XYZ") bpy.types.Scene.measure_panel_volume1 = bpy.props.FloatProperty( name="", precision=PRECISION, unit="VOLUME") bpy.types.Scene.measure_panel_volume2 = bpy.props.FloatProperty( name="", precision=PRECISION, unit="VOLUME") 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') # Define property for the draw setting. bpy.types.Scene.measure_panel_draw = bpy.props.BoolProperty( name="Draw distance", description="Draw distances in 3D View", default=1) bpy.types.Scene.measure_panel_calc_edge_length = bpy.props.BoolProperty( description="Calculate total length of (selected) edges", default=0) # 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) # Define property for the calc-volume setting. bpy.types.Scene.measure_panel_calc_volume = bpy.props.BoolProperty( description="Calculate mesh volume (heavy CPU " "usage on bigger meshes)", default=0) # Define dropdown for the global/local setting bpy.types.Scene.measure_panel_update = bpy.props.BoolProperty( description="Update CPU heavy calculations", default=0) pass def unregister(): bpy.utils.unregister_module(__name__) bpy.app.handlers.scene_update_post.remove(scene_update) # Remove properties. del bpy.types.Scene.measure_panel_dist del bpy.types.Scene.measure_panel_edge_length del bpy.types.Scene.measure_panel_area1 del bpy.types.Scene.measure_panel_area2 del bpy.types.Scene.measure_panel_normal1 del bpy.types.Scene.measure_panel_normal2 del bpy.types.Scene.measure_panel_volume1 del bpy.types.Scene.measure_panel_volume2 del bpy.types.Scene.measure_panel_transform del bpy.types.Scene.measure_panel_draw del bpy.types.Scene.measure_panel_calc_edge_length del bpy.types.Scene.measure_panel_calc_area del bpy.types.Scene.measure_panel_calc_volume del bpy.types.Scene.measure_panel_update pass if __name__ == "__main__": register()