1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # Uses volume calculation and manifold check code (GPL2+) from:
20 # http://www.shapeways.com/forum/index.php?t=msg&goto=3639
21 # Shapeways Volume Calculator by Benjamin Lauritzen (Loonsbury)
23 # #################################
26 "name": "Measure Panel",
27 "author": "Buerbaum Martin (Pontiac), TNae (Normal patch), "
28 "Benjamin Lauritzen (Loonsbury; Volume code), "
29 "Alessandro Sala (patch: Units in 3D View) ",
31 "blender": (2, 60, 0),
32 "location": "View3D > Properties > Measure Panel",
33 "description": "Measure distances between objects",
34 "warning": "Script needs repairs",
35 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
36 "Scripts/3D_interaction/Panel_Measure",
37 "category": "3D View",
43 This script displays in OBJECT MODE:
44 * The distance of the 3D cursor to the origin of the
45 3D space (if NOTHING is selected).
46 * The distance of the 3D cursor to the center of an object
47 (if exactly ONE object is selected).
48 * The distance between 2 object centers
49 (if exactly TWO objects are selected).
50 * The surface area of any selected mesh object.
51 * The average normal of the mesh surface of any selected mesh object.
52 * The volume of any selected mesh object.
54 Display in EDIT MODE (Local and Global space supported):
55 * The distance of the 3D cursor to the origin
56 (in Local space it is the object center instead).
57 * The distance of the 3D cursor to a selected vertex.
58 * The distance between 2 selected vertices.
62 This functionality can be accessed via the
63 "Properties" panel in 3D View ([N] key).
65 It's very helpful to use one or two "Empty" objects with
66 "Snap during transform" enabled for fast measurement.
69 http://gitorious.org/blender-scripts/blender-measure-panel-script
70 http://blenderartists.org/forum/showthread.php?t=177800
74 from bpy
.props
import *
75 from bpy
.app
.handlers
import persistent
76 from mathutils
import Vector
, Matrix
79 from bpy_extras
.view3d_utils
import location_3d_to_region_2d
80 from bpy_extras
.mesh_utils
import ngon_tessellate
83 # Precicion for display of float values.
86 # Name of the custom properties as stored in the scene.
87 COLOR_LOCAL
= (1.0, 0.5, 0.0, 0.8)
88 COLOR_GLOBAL
= (0.5, 0.0, 1.0, 0.8)
90 # 3D View - text offset
91 OFFSET_LINE
= 10 # Offset the text a bit to the right.
92 OFFSET_Y
= 15 # Offset of the lines.
93 OFFSET_VALUE
= 30 # Offset of value(s) from the text.
95 # 3D View - line width
100 # Returns a tuple describing the current measuring system
101 # and formatting options.
102 # Returned data is meant to be passed to formatDistance().
103 # Original by Alessandro Sala (Feb, 12th 2012)
104 # Update by Alessandro Sala (Dec, 18th 2012)
106 scale
= bpy
.context
.scene
.unit_settings
.scale_length
107 unit_system
= bpy
.context
.scene
.unit_settings
.system
108 separate_units
= bpy
.context
.scene
.unit_settings
.use_separate
109 if unit_system
== 'METRIC':
110 scale_steps
= ((1000, 'km'), (1, 'm'), (1 / 100, 'cm'),
111 (1 / 1000, 'mm'), (1 / 1000000, '\u00b5m'))
112 elif unit_system
== 'IMPERIAL':
113 scale_steps
= ((5280, 'mi'), (1, '\''),
114 (1 / 12, '"'), (1 / 12000, 'thou'))
115 scale
/= 0.3048 # BU to feet
117 scale_steps
= ((1, ' BU'),)
118 separate_units
= False
120 return (scale
, scale_steps
, separate_units
)
123 # Converts a distance from BU into the measuring system
124 # described by units_info.
125 # Original by Alessandro Sala (Feb, 12th 2012)
126 # Update by Alessandro Sala (Dec, 18th 2012)
127 def convertDistance(val
, units_info
):
128 scale
, scale_steps
, separate_units
= units_info
131 while idx
< len(scale_steps
) - 1:
132 if sval
>= scale_steps
[idx
][0]:
135 factor
, suffix
= scale_steps
[idx
]
137 if not separate_units
or idx
== len(scale_steps
) - 1:
138 dval
= str(round(sval
, PRECISION
)) + suffix
141 dval
= str(round(ival
, PRECISION
)) + suffix
144 while idx
< len(scale_steps
):
145 fval
*= scale_steps
[idx
- 1][0] / scale_steps
[idx
][0]
149 + scale_steps
[idx
][1]
156 # Returns a single selected object.
157 # Returns None if more than one (or nothing) is selected.
158 # Note: Ignores the active object.
159 def getSingleObject():
160 if len(bpy
.context
.selected_objects
) == 1:
161 return bpy
.context
.selected_objects
[0]
166 # Returns a list with 2 3D points (Vector) and a color (RGBA)
167 # depending on the current view mode and the selection.
168 def getMeasurePoints(context
):
172 # Get a single selected object (or nothing).
173 obj
= getSingleObject()
175 if mode
== 'EDIT_MESH':
176 obj
= context
.active_object
178 if obj
and obj
.type == 'MESH' and obj
.data
:
179 # Get mesh data from Object.
182 # Get the selected vertices.
183 # @todo: Better (more efficient) way to do this?
184 verts_selected
= [v
for v
in mesh
.vertices
if v
.select
== 1]
186 if len(verts_selected
) == 0:
188 # We measure the distance from...
189 # local ... the object center to the 3D cursor.
190 # global ... the origin to the 3D cursor.
191 cur_loc
= sce
.cursor_location
192 obj_loc
= obj
.matrix_world
.to_translation()
194 # Convert to local space, if needed.
195 if measureLocal(sce
):
198 return (p1
, p2
, COLOR_GLOBAL
)
201 p1
= Vector((0.0, 0.0, 0.0))
203 return (p1
, p2
, COLOR_GLOBAL
)
205 elif len(verts_selected
) == 1:
206 # One vertex selected.
207 # We measure the distance from the
208 # selected vertex object to the 3D cursor.
209 cur_loc
= sce
.cursor_location
210 vert_loc
= verts_selected
[0].co
.copy()
212 # Convert to local or global space.
213 if measureLocal(sce
):
216 return (p1
, p2
, COLOR_LOCAL
)
219 p1
= obj
.matrix_world
* vert_loc
221 return (p1
, p2
, COLOR_GLOBAL
)
223 elif len(verts_selected
) == 2:
224 # Two vertices selected.
225 # We measure the distance between the
226 # two selected vertices.
227 obj_loc
= obj
.matrix_world
.to_translation()
228 vert1_loc
= verts_selected
[0].co
.copy()
229 vert2_loc
= verts_selected
[1].co
.copy()
231 # Convert to local or global space.
232 if measureLocal(sce
):
235 return (p1
, p2
, COLOR_LOCAL
)
238 p1
= obj
.matrix_world
* vert1_loc
239 p2
= obj
.matrix_world
* vert2_loc
240 return (p1
, p2
, COLOR_GLOBAL
)
245 elif mode
== 'OBJECT':
246 # We are working in object mode.
248 if len(context
.selected_objects
) > 2:
250 elif len(context
.selected_objects
) == 2:
251 # 2 objects selected.
252 # We measure the distance between the 2 selected objects.
253 obj1
, obj2
= context
.selected_objects
254 obj1_loc
= obj1
.matrix_world
.to_translation()
255 obj2_loc
= obj2
.matrix_world
.to_translation()
256 return (obj1_loc
, obj2_loc
, COLOR_GLOBAL
)
259 # One object selected.
260 # We measure the distance from the object to the 3D cursor.
261 cur_loc
= sce
.cursor_location
262 obj_loc
= obj
.matrix_world
.to_translation()
263 return (obj_loc
, cur_loc
, COLOR_GLOBAL
)
265 elif not context
.selected_objects
:
267 # We measure the distance from the origin to the 3D cursor.
268 p1
= Vector((0.0, 0.0, 0.0))
269 p2
= sce
.cursor_location
270 return (p1
, p2
, COLOR_GLOBAL
)
276 # Return the length of an edge (in global space if "obj" is set).
277 # Respects the scaling (via the "obj.matrix_world" parameter).
278 def edgeLengthGlobal(edge
, obj
, globalSpace
):
279 v1
, v2
= edge
.vertices
282 v1
= obj
.data
.vertices
[v1
]
283 v2
= obj
.data
.vertices
[v2
]
286 mat
= obj
.matrix_world
287 # Apply transform matrix to vertex coordinates.
294 return (v1
- v2
).length
297 # Calculate the edge length of a mesh object.
298 # *) Set selectedOnly=1 if you only want to count selected edges.
299 # *) Set globalSpace=1 if you want to calculate
300 # the global edge length (object mode).
301 # Note: Be sure you have updated the mesh data before
302 # running this with selectedOnly=1!
303 # @todo Support other object types (surfaces, etc...)?
304 def objectEdgeLength(obj
, selectedOnly
, globalSpace
):
305 if obj
and obj
.type == 'MESH' and obj
.data
:
310 # Count the length of all edges.
311 for ed
in mesh
.edges
:
312 if not selectedOnly
or ed
.select
:
313 edgeTotal
+= edgeLengthGlobal(ed
, obj
, globalSpace
)
317 # We can not calculate a length for this object.
321 # Return the area of a face (in global space).
322 # @note Copies the functionality of the following functions,
323 # but also respects the scaling (via the "obj.matrix_world" parameter):
324 # @sa: rna_mesh.c:rna_MeshTessFace_area_get
325 # @sa: math_geom.c:area_quad_v3
326 # @sa: math_geom.c:area_tri_v3
327 # @sa: math_geom.c:area_poly_v3
328 # @todo Fix calculation of "n" for n-gons?
329 def polyAreaGlobal(poly
, obj
):
331 mat
= obj
.matrix_world
.copy()
336 if len(poly
.vertices
) > 3:
337 # Tesselate the polygon into multiple tris
338 tris
= ngon_tessellate(mesh
, poly
.vertices
)
344 # Get indices from original poly
345 v1
= poly
.vertices
[v1
]
346 v2
= poly
.vertices
[v2
]
347 v3
= poly
.vertices
[v3
]
349 # Get vertex information from indices
350 v1
= mesh
.vertices
[v1
]
351 v2
= mesh
.vertices
[v2
]
352 v3
= mesh
.vertices
[v3
]
354 # Apply transform matrix to vertex coordinates.
359 # Calculate area for the new tri
365 area
+= n
.length
/ 2.0
367 elif len(poly
.vertices
) == 3:
371 v1
, v2
, v3
= poly
.vertices
374 v1
= mesh
.vertices
[v1
]
375 v2
= mesh
.vertices
[v2
]
376 v3
= mesh
.vertices
[v3
]
378 # Apply transform matrix to vertex coordinates.
388 area
= n
.length
/ 2.0
390 # Apply rotation and scale to the normal as well.
391 rot_mat
= obj
.matrix_world
.to_quaternion()
392 scale
= obj
.matrix_world
.to_scale()
393 norm
= rot_mat
* norm
397 norm
.z
* scale
.z
)).normalized()
402 # Calculate the surface area of a mesh object.
403 # *) Set selectedOnly=1 if you only want to count selected faces.
404 # *) Set globalSpace=1 if you want to calculate
405 # the global surface area (object mode).
406 # Note: Be sure you have updated the mesh data before
407 # running this with selectedOnly=1!
408 # @todo Support other object types (surfaces, etc...)?
409 def objectSurfaceArea(obj
, selectedOnly
, globalSpace
):
410 if obj
and obj
.type == 'MESH' and obj
.data
:
412 normTotal
= Vector((0.0, 0.0, 0.0))
416 # Count the area of all the faces.
417 for poly
in mesh
.polygons
:
418 if not selectedOnly
or poly
.select
:
420 a
, n
= polyAreaGlobal(poly
, obj
)
424 areaTotal
+= poly
.area
425 normTotal
+= poly
.normal
427 return areaTotal
, normTotal
429 # We can not calculate an area for this object.
430 return -1, Vector((0.0, 0.0, 0.0))
433 # Calculate the volume of a mesh object.
434 # Copyright Loonsbury (loonsbury@yahoo.com)
435 def objectVolume(obj
, globalSpace
):
436 if obj
and obj
.type == 'MESH' and obj
.data
:
438 # Check if mesh is non-manifold
439 if not checkManifold(obj
):
442 # Check if mesh has n-gons
450 for poly
in mesh
.polygons
:
453 if len(poly
.vertices
) == 4:
454 v1
, v2
, v3
, v4
= poly
.vertices
456 v1
, v2
, v3
= poly
.vertices
458 v1
= mesh
.vertices
[v1
]
459 v2
= mesh
.vertices
[v2
]
460 v3
= mesh
.vertices
[v3
]
462 # Scaled vert coordinates with object XYZ offsets for
463 # selection extremes/sizing.
465 x1
= v1
.co
[0] * obj
.scale
[0] + obj
.location
[0]
466 y1
= v1
.co
[1] * obj
.scale
[1] + obj
.location
[1]
467 z1
= v1
.co
[2] * obj
.scale
[2] + obj
.location
[2]
469 x2
= v2
.co
[0] * obj
.scale
[0] + obj
.location
[0]
470 y2
= v2
.co
[1] * obj
.scale
[1] + obj
.location
[1]
471 z2
= v2
.co
[2] * obj
.scale
[2] + obj
.location
[2]
473 x3
= v3
.co
[0] * obj
.scale
[0] + obj
.location
[0]
474 y3
= v3
.co
[1] * obj
.scale
[1] + obj
.location
[1]
475 z3
= v3
.co
[2] * obj
.scale
[2] + obj
.location
[2]
486 volume
= ((z1
+ z2
+ z3
) / 3.0) * pa
489 if len(poly
.vertices
) == 4:
491 v4
= mesh
.vertices
[v4
]
494 x4
= v4
.co
[0] * obj
.scale
[0] + obj
.location
[0]
495 y4
= v4
.co
[1] * obj
.scale
[1] + obj
.location
[1]
496 z4
= v4
.co
[2] * obj
.scale
[2] + obj
.location
[2]
506 volume
+= ((z1
+ z3
+ z4
) / 3.0) * pa
517 volTot
+= fzn
* volume
522 # print obj.name, ': Object must be a mesh!' # TODO
528 # Copyright Loonsbury (loonsbury@yahoo.com)
529 def checkManifold(obj
):
530 if obj
and obj
.type == 'MESH' and obj
.data
:
533 mc
= dict([(ed
.key
, 0) for ed
in mesh
.edges
]) # TODO
535 for p
in mesh
.polygons
:
536 for ek
in p
.edge_keys
:
541 mt
= [e
[1] for e
in mc
.items()]
547 if mt
[len(mt
) - 1] > 2:
556 # Check if a mesh has n-gons (polygon with more than 4 edges).
558 if obj
and obj
.type == 'MESH' and obj
.data
:
561 for p
in mesh
.polygons
:
562 if len(p
.vertices
) > 4:
571 # User friendly access to the "space" setting.
572 def measureGlobal(sce
):
573 return (sce
.measure_panel_transform
== "measure_global")
576 # User friendly access to the "space" setting.
577 def measureLocal(sce
):
578 return (sce
.measure_panel_transform
== "measure_local")
581 # Calculate values if geometry, selection or cursor changed.
583 def scene_update(context
):
585 mode
= bpy
.context
.mode
587 if (mode
== 'EDIT_MESH' and not sce
.measure_panel_update
):
590 if (bpy
.data
.objects
.is_updated
591 or bpy
.context
.scene
.is_updated
592 or sce
.measure_panel_update
):
593 # TODO: Better way to check selection changes and cursor changes?
595 sel_objs
= bpy
.context
.selected_objects
598 if sce
.measure_panel_calc_edge_length
:
599 if (mode
== 'EDIT_MESH'
600 and sce
.measure_panel_update
):
601 sce
.measure_panel_update
= 0
602 obj
= context
.active_object
605 length_total
= objectEdgeLength(obj
, True,
607 sce
.measure_panel_edge_length
= length_total
609 elif mode
== 'OBJECT':
614 length
= objectEdgeLength(o
, False, measureGlobal(sce
))
620 length_total
+= length
622 sce
.measure_panel_edge_length
= length_total
625 # Handle mesh surface area calulations
626 if sce
.measure_panel_calc_area
:
627 if (mode
== 'EDIT_MESH'
628 and sce
.measure_panel_update
):
629 sce
.measure_panel_update
= 0
630 obj
= bpy
.context
.active_object
632 if obj
and obj
.type == 'MESH' and obj
.data
:
633 # "Note: a Mesh will return the selection state of the mesh
634 # when EditMode was last exited. A Python script operating
635 # in EditMode must exit EditMode before getting the current
636 # selection state of the mesh."
637 # http://www.blender.org/documentation/249PythonDoc/
638 # /Mesh.MVert-class.html#sel
639 # We can only provide this by existing &
640 # re-entering EditMode.
641 # @todo: Better way to do this?
643 # Get mesh data from Object.
646 # Get transformation matrix from object.
647 ob_mat
= obj
.matrix_world
648 # Also make an inversed copy! of the matrix.
649 ob_mat_inv
= ob_mat
.copy()
650 Matrix
.invert(ob_mat_inv
)
652 # Get the selected vertices.
653 # @todo: Better (more efficient) way to do this?
654 verts_selected
= [v
for v
in me
.vertices
if v
.select
== 1]
656 if len(verts_selected
) >= 3:
658 # @todo: Better (more efficient) way to do this?
659 polys_selected
= [p
for p
in me
.polygons
662 if len(polys_selected
) > 0:
663 area
, normal
= objectSurfaceArea(obj
, True,
666 sce
.measure_panel_area1
= area
667 sce
.measure_panel_normal1
= normal
669 elif mode
== 'OBJECT':
670 # We are working in object mode.
672 # Get a single selected object (or nothing).
673 obj
= getSingleObject()
675 if len(sel_objs
) > 2:
677 # @todo Make this work again.
678 # # We have more that 2 objects selected...
680 # mesh_objects = [o for o in context.selected_objects
681 # if o.type == 'MESH']
683 # if len(mesh_objects) > 0:
684 # # ... and at least one of them is a mesh.
686 # for o in mesh_objects:
687 # area = objectSurfaceArea(o, False,
688 # measureGlobal(sce))
690 # #row.label(text=o.name, icon='OBJECT_DATA')
691 # #row.label(text=str(round(area, PRECISION))
694 elif len(sel_objs
) == 2:
695 # 2 objects selected.
697 obj1
, obj2
= sel_objs
699 # Calculate surface area of the objects.
700 area1
, normal1
= objectSurfaceArea(obj1
, False,
702 area2
, normal2
= objectSurfaceArea(obj2
, False,
704 sce
.measure_panel_area1
= area1
705 sce
.measure_panel_area2
= area2
706 sce
.measure_panel_normal1
= normal1
707 sce
.measure_panel_normal2
= normal2
710 # One object selected.
712 # Calculate surface area of the object.
713 area
, normal
= objectSurfaceArea(obj
, False,
716 sce
.measure_panel_area1
= area
717 sce
.measure_panel_normal1
= normal
720 # Handle mesh volume calulations.
721 if sce
.measure_panel_calc_volume
:
722 obj
= getSingleObject()
725 # We are working in object mode.
727 #if len(sel_objs) > 2: # TODO
729 if len(sel_objs
) == 2:
730 # 2 objects selected.
732 obj1
, obj2
= sel_objs
734 # Calculate surface area of the objects.
735 volume1
= objectVolume(obj1
, measureGlobal(sce
))
736 volume2
= objectVolume(obj2
, measureGlobal(sce
))
738 sce
.measure_panel_volume1
= volume1
739 sce
.measure_panel_volume2
= volume2
742 # One object selected.
744 # Calculate surface area of the object.
745 volume1
= objectVolume(obj
, measureGlobal(sce
))
747 sce
.measure_panel_volume1
= volume1
750 def draw_measurements_callback(self
, context
):
754 if hasattr(sce
, "measure_panel_draw"):
755 draw
= sce
.measure_panel_draw
757 # 2D drawing code example
758 #bgl.glBegin(bgl.GL_LINE_STRIP)
759 #bgl.glVertex2i(0, 0)
760 #bgl.glVertex2i(80, 100)
764 # Get measured 3D points and colors.
765 line
= getMeasurePoints(context
)
770 # Get & convert the Perspective Matrix of the current view/region.
772 region
= view3d
.region_data
773 perspMatrix
= region
.perspective_matrix
774 tempMat
= [perspMatrix
[j
][i
] for i
in range(4) for j
in range(4)]
775 perspBuff
= bgl
.Buffer(bgl
.GL_FLOAT
, 16, tempMat
)
778 # Store previous OpenGL settings.
780 MatrixMode_prev
= bgl
.Buffer(bgl
.GL_INT
, [1])
781 bgl
.glGetIntegerv(bgl
.GL_MATRIX_MODE
, MatrixMode_prev
)
782 MatrixMode_prev
= MatrixMode_prev
[0]
784 # Store projection matrix
785 ProjMatrix_prev
= bgl
.Buffer(bgl
.GL_DOUBLE
, [16])
786 bgl
.glGetFloatv(bgl
.GL_PROJECTION_MATRIX
, ProjMatrix_prev
)
789 lineWidth_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [1])
790 bgl
.glGetFloatv(bgl
.GL_LINE_WIDTH
, lineWidth_prev
)
791 lineWidth_prev
= lineWidth_prev
[0]
794 blend_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
795 bgl
.glGetFloatv(bgl
.GL_BLEND
, blend_prev
)
796 blend_prev
= blend_prev
[0]
798 line_stipple_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
799 bgl
.glGetFloatv(bgl
.GL_LINE_STIPPLE
, line_stipple_prev
)
800 line_stipple_prev
= line_stipple_prev
[0]
803 color_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [4])
804 bgl
.glGetFloatv(bgl
.GL_COLOR
, color_prev
)
807 # Prepare for 3D drawing
809 bgl
.glMatrixMode(bgl
.GL_PROJECTION
)
810 bgl
.glLoadMatrixf(perspBuff
)
812 bgl
.glEnable(bgl
.GL_BLEND
)
813 bgl
.glEnable(bgl
.GL_LINE_STIPPLE
)
817 bgl
.glLineWidth(LINE_WIDTH_XYZ
)
819 bgl
.glColor4f(1, 0, 0, 0.8)
820 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
821 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
822 bgl
.glVertex3f(p2
[0], p1
[1], p1
[2])
825 bgl
.glColor4f(0, 1, 0, 0.8)
826 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
827 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
828 bgl
.glVertex3f(p1
[0], p2
[1], p1
[2])
831 bgl
.glColor4f(0, 0, 1, 0.8)
832 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
833 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
834 bgl
.glVertex3f(p1
[0], p1
[1], p2
[2])
838 bgl
.glLineWidth(LINE_WIDTH_DIST
)
839 bgl
.glColor4f(color
[0], color
[1], color
[2], color
[3])
840 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
841 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
842 bgl
.glVertex3f(p2
[0], p2
[1], p2
[2])
846 # Restore previous OpenGL settings
848 bgl
.glMatrixMode(MatrixMode_prev
)
849 bgl
.glLoadMatrixf(ProjMatrix_prev
)
850 bgl
.glLineWidth(lineWidth_prev
)
852 bgl
.glDisable(bgl
.GL_BLEND
)
853 if not line_stipple_prev
:
854 bgl
.glDisable(bgl
.GL_LINE_STIPPLE
)
863 # We do this after drawing the lines so
864 # we can draw it OVER the line.
865 coord_2d
= location_3d_to_region_2d(
867 context
.space_data
.region_3d
,
869 dist
= (p1
- p2
).length
871 # Write distance value into the scene property,
872 # so we can display it in the panel & refresh the panel.
873 if hasattr(sce
, "measure_panel_dist"):
874 sce
.measure_panel_dist
= dist
875 context
.area
.tag_redraw()
879 ("X:", abs(p1
[0] - p2
[0])),
880 ("Y:", abs(p1
[1] - p2
[1])),
881 ("Z:", abs(p1
[2] - p2
[2]))]
884 # @todo Get user pref for text color in 3D View
885 bgl
.glColor4f(1.0, 1.0, 1.0, 1.0)
886 blf
.size(0, 12, 72) # Prevent font size to randomly change.
888 uinfo
= getUnitsInfo()
890 loc_x
= coord_2d
[0] + OFFSET_LINE
896 value
= convertDistance(t
[1], uinfo
)
898 blf
.position(0, loc_x
, loc_y
, 0)
900 blf
.position(0, loc_x
+ OFFSET_VALUE
, loc_y
, 0)
906 class VIEW3D_OT_display_measurements(bpy
.types
.Operator
):
907 """Display the measurements made in the 'Measure' panel"""
908 bl_idname
= "view3d.display_measurements"
909 bl_label
= "Display the measurements made in the" \
910 " 'Measure' panel in the 3D View"
911 bl_options
= {'REGISTER'}
913 def modal(self
, context
, event
):
914 context
.area
.tag_redraw()
917 def execute(self
, context
):
918 if context
.area
.type == 'VIEW_3D':
919 mgr_ops
= context
.window_manager
.operators
.values()
920 if not self
.bl_idname
in [op
.bl_idname
for op
in mgr_ops
]:
921 # Add the region OpenGL drawing callback
923 # XXX, this is never removed!, it should be! (at least when disabling the addon)
924 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(
925 draw_measurements_callback
,
927 'WINDOW', 'POST_PIXEL')
929 print("Measure panel display callback added")
931 # XXX, never removed!
932 context
.window_manager
.modal_handler_add(self
)
933 return {'RUNNING_MODAL'}
938 self
.report({'WARNING'}, "View3D not found, cannot run operator")
942 class VIEW3D_OT_activate_measure_panel(bpy
.types
.Operator
):
943 bl_label
= "Activate"
944 bl_idname
= "view3d.activate_measure_panel"
945 bl_description
= "Activate the callback needed to draw the lines"
946 bl_options
= {'REGISTER'}
948 def invoke(self
, context
, event
):
950 # Execute operator (this adds the callback)
951 # if it wasn't done yet.
952 bpy
.ops
.view3d
.display_measurements()
956 class VIEW3D_OT_reenter_editmode(bpy
.types
.Operator
):
957 bl_label
= "Re-enter EditMode"
958 bl_idname
= "view3d.reenter_editmode"
959 bl_description
= "Update mesh data of an active mesh object " \
960 "(this is done by exiting and re-entering mesh edit mode)"
961 bl_options
= {'REGISTER'}
963 def invoke(self
, context
, event
):
965 # Get the active object.
966 obj
= context
.active_object
969 if obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH':
970 # Exit and re-enter mesh EditMode.
971 bpy
.ops
.object.mode_set(mode
='OBJECT')
972 bpy
.ops
.object.mode_set(mode
='EDIT')
973 sce
.measure_panel_update
= 1
979 class VIEW3D_PT_measure(bpy
.types
.Panel
):
980 bl_space_type
= 'VIEW_3D'
981 bl_region_type
= 'UI'
983 bl_options
= {'DEFAULT_CLOSED'}
986 def poll(cls
, context
):
987 # Only display this panel in the object and edit mode 3D view.
989 if (context
.area
.type == 'VIEW_3D' and
990 (mode
== 'EDIT_MESH' or mode
== 'OBJECT')):
995 def draw_header(self
, context
):
999 mgr_ops
= context
.window_manager
.operators
.values()
1000 if (not "VIEW3D_OT_display_measurements"
1001 in [op
.bl_idname
for op
in mgr_ops
]):
1002 layout
.operator("view3d.activate_measure_panel",
1005 def draw(self
, context
):
1006 layout
= self
.layout
1010 # Get a single selected object (or nothing).
1011 obj
= getSingleObject()
1013 drawTansformButtons
= 1
1015 if mode
== 'EDIT_MESH':
1016 obj
= context
.active_object
1019 row
.operator("view3d.reenter_editmode",
1020 text
="Update selection")
1022 # description="The calculated values can" \
1023 # " not be updated in mesh edit mode" \
1024 # " automatically. Press this button" \
1025 # " to do this manually, after you changed" \
1028 if obj
and obj
.type == 'MESH' and obj
.data
:
1029 # "Note: a Mesh will return the selection state of the mesh
1030 # when EditMode was last exited. A Python script operating
1031 # in EditMode must exit EditMode before getting the current
1032 # selection state of the mesh."
1033 # http://www.blender.org/documentation/249PythonDoc/
1034 # /Mesh.MVert-class.html#sel
1035 # We can only provide this by existing & re-entering EditMode.
1036 # @todo: Better way to do this?
1038 # Get mesh data from Object.
1041 # Get transformation matrix from object.
1042 ob_mat
= obj
.matrix_world
1043 # Also make an inversed copy! of the matrix.
1044 ob_mat_inv
= ob_mat
.copy()
1045 Matrix
.invert(ob_mat_inv
)
1047 # Get the selected vertices.
1048 # @todo: Better (more efficient) way to do this?
1049 verts_selected
= [v
for v
in mesh
.vertices
if v
.select
== 1]
1051 if len(verts_selected
) == 0:
1053 # We measure the distance from...
1054 # local ... the object center to the 3D cursor.
1055 # global ... the origin to the 3D cursor.
1056 layout
.label(text
="Distance")
1060 row
.prop(sce
, "measure_panel_dist")
1063 row
.label(text
="", icon
='CURSOR')
1064 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1065 if measureLocal(sce
):
1066 row
.label(text
="Obj. Center")
1068 row
.label(text
="Origin [0,0,0]")
1070 layout
.prop(sce
, "measure_panel_draw")
1072 elif len(verts_selected
) == 1:
1073 # One vertex selected.
1074 # We measure the distance from the
1075 # selected vertex object to the 3D cursor.
1076 layout
.label(text
="Distance")
1080 row
.prop(sce
, "measure_panel_dist")
1083 row
.label(text
="", icon
='CURSOR')
1084 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1085 row
.label(text
="", icon
='VERTEXSEL')
1087 layout
.prop(sce
, "measure_panel_draw")
1089 elif len(verts_selected
) == 2:
1090 # Two vertices selected.
1091 # We measure the distance between the
1092 # two selected vertices.
1093 layout
.label(text
="Distance")
1097 row
.prop(sce
, "measure_panel_dist")
1100 row
.label(text
="", icon
='VERTEXSEL')
1101 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1102 row
.label(text
="", icon
='VERTEXSEL')
1104 layout
.prop(sce
, "measure_panel_draw")
1106 edges_selected
= [ed
for ed
in mesh
.edges
if ed
.select
== 1]
1107 if len(edges_selected
) >= 1:
1109 row
.prop(sce
, "measure_panel_calc_edge_length",
1110 text
="Edge Length (selected edges)")
1112 if sce
.measure_panel_calc_edge_length
:
1113 if sce
.measure_panel_edge_length
>= 0:
1117 text
=str(len(edges_selected
)),
1121 row
.label(text
="Length")
1122 row
.prop(sce
, "measure_panel_edge_length")
1124 if len(verts_selected
) > 2:
1126 row
.prop(sce
, "measure_panel_calc_area",
1127 text
="Surface area (selected faces)")
1129 if sce
.measure_panel_calc_area
:
1130 # Get selected faces
1131 # @todo: Better (more efficient) way to do this?
1132 polys_selected
= [p
for p
in mesh
.polygons
1135 if len(polys_selected
) > 0:
1136 if sce
.measure_panel_area1
>= 0:
1140 text
=str(len(polys_selected
)),
1144 row
.label(text
="Area")
1145 row
.prop(sce
, "measure_panel_area1")
1148 row
.label(text
="Normal")
1150 row
.prop(sce
, "measure_panel_normal1")
1154 row
.label(text
="Selection not supported",
1157 if drawTansformButtons
:
1160 "measure_panel_transform",
1163 elif mode
== 'OBJECT':
1164 # We are working in object mode.
1166 mesh_objects
= [o
for o
in context
.selected_objects
1167 if o
.type == 'MESH']
1169 if len(context
.selected_objects
) > 2:
1170 # We have more that 2 objects selected...
1174 row
.prop(sce
, "measure_panel_calc_edge_length",
1177 if sce
.measure_panel_calc_edge_length
:
1178 if len(mesh_objects
) > 0:
1182 row
.label(text
="Total edge length")
1183 row
.prop(sce
, "measure_panel_edge_length")
1187 row
.prop(sce
, "measure_panel_calc_area",
1188 text
="Surface area")
1190 if sce
.measure_panel_calc_area
:
1191 if len(mesh_objects
) > 0:
1192 # ... and at least one of them is a mesh.
1194 # Calculate and display surface area of the objects.
1195 # @todo: Convert to scene units! We do not have a
1196 # FloatProperty field here for automatic conversion.
1199 row
.label(text
="Multiple objects not yet supported",
1202 row
.label(text
="(= More than two meshes)",
1204 # @todo Make this work again.
1205 # for o in mesh_objects:
1206 # area = objectSurfaceArea(o, False,
1207 # measureGlobal(sce))
1209 # row = layout.row()
1210 # row.label(text=o.name, icon='OBJECT_DATA')
1211 # row.label(text=str(round(area, PRECISION))
1214 elif len(context
.selected_objects
) == 2:
1215 # 2 objects selected.
1216 # We measure the distance between the 2 selected objects.
1217 layout
.label(text
="Distance")
1219 obj1
, obj2
= context
.selected_objects
1223 row
.prop(sce
, "measure_panel_dist")
1226 row
.label(text
="", icon
='OBJECT_DATA')
1227 row
.prop(obj1
, "name", text
="")
1229 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1231 row
.label(text
="", icon
='OBJECT_DATA')
1232 row
.prop(obj2
, "name", text
="")
1234 layout
.prop(sce
, "measure_panel_draw")
1238 row
.prop(sce
, "measure_panel_calc_edge_length",
1241 if sce
.measure_panel_calc_edge_length
:
1242 if sce
.measure_panel_edge_length
>= 0:
1243 if len(mesh_objects
) > 0:
1247 row
.label(text
="Total edge length")
1248 row
.prop(sce
, "measure_panel_edge_length")
1253 row
.prop(sce
, "measure_panel_calc_area",
1254 text
="Surface area")
1256 if sce
.measure_panel_calc_area
:
1257 # Display surface area of the objects.
1258 if (sce
.measure_panel_area1
>= 0
1259 or sce
.measure_panel_area2
>= 0):
1260 if sce
.measure_panel_area1
>= 0:
1263 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1266 row
.label(text
="Area")
1267 row
.prop(sce
, "measure_panel_area1")
1270 row
.label(text
="Normal")
1272 row
.prop(sce
, "measure_panel_normal1")
1274 if sce
.measure_panel_area2
>= 0:
1277 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1280 row
.label(text
="Area")
1281 row
.prop(sce
, "measure_panel_area2")
1284 row
.label(text
="Normal")
1286 row
.prop(sce
, "measure_panel_normal2")
1290 row
.prop(sce
, "measure_panel_calc_volume",
1293 if sce
.measure_panel_calc_volume
:
1294 # Display volume of the objects.
1295 if sce
.measure_panel_volume1
>= -2:
1298 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1300 if sce
.measure_panel_volume1
>= 0:
1302 row
.label(text
="Volume")
1303 row
.prop(sce
, "measure_panel_volume1")
1304 elif sce
.measure_panel_volume1
>= -1:
1306 row
.label(text
="Mesh is non-manifold!",
1310 row
.label(text
="Mesh has n-gons (faces with " \
1311 "more than 4 edges)!",
1314 if sce
.measure_panel_volume2
>= -2:
1317 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1319 if sce
.measure_panel_volume2
>= 0:
1321 row
.label(text
="Volume")
1322 row
.prop(sce
, "measure_panel_volume2")
1323 elif sce
.measure_panel_volume2
>= -1:
1325 row
.label(text
="Mesh is non-manifold!",
1329 row
.label(text
="Mesh has n-gons (faces with " \
1330 "more than 4 edges)!",
1334 # One object selected.
1335 # We measure the distance from the object to the 3D cursor.
1336 layout
.label(text
="Distance")
1340 row
.prop(sce
, "measure_panel_dist")
1343 row
.label(text
="", icon
='CURSOR')
1345 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1347 row
.label(text
="", icon
='OBJECT_DATA')
1348 row
.prop(obj
, "name", text
="")
1350 layout
.prop(sce
, "measure_panel_draw")
1354 row
.prop(sce
, "measure_panel_calc_edge_length",
1357 if sce
.measure_panel_calc_edge_length
:
1358 if sce
.measure_panel_edge_length
>= 0:
1359 if len(mesh_objects
) > 0:
1363 row
.label(text
="Total edge length")
1364 row
.prop(sce
, "measure_panel_edge_length")
1368 row
.prop(sce
, "measure_panel_calc_area",
1369 text
="Surface area")
1371 if sce
.measure_panel_calc_area
:
1372 # Display surface area of the object.
1374 if sce
.measure_panel_area1
>= 0.0:
1377 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1380 row
.label(text
="Area")
1381 row
.prop(sce
, "measure_panel_area1")
1384 row
.label(text
="Normal")
1386 row
.prop(sce
, "measure_panel_normal1")
1390 row
.prop(sce
, "measure_panel_calc_volume",
1393 if sce
.measure_panel_calc_volume
:
1394 # Display volume of the objects.
1395 if sce
.measure_panel_volume1
>= -2:
1398 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1400 if sce
.measure_panel_volume1
>= 0:
1402 row
.label(text
="Volume")
1403 row
.prop(sce
, "measure_panel_volume1")
1404 elif sce
.measure_panel_volume1
>= -1:
1406 row
.label(text
="Mesh is non-manifold!",
1410 row
.label(text
="Mesh has n-gons (faces with " \
1411 "more than 4 edges)!",
1414 elif not context
.selected_objects
:
1416 # We measure the distance from the origin to the 3D cursor.
1417 layout
.label(text
="Distance")
1421 row
.prop(sce
, "measure_panel_dist")
1424 row
.label(text
="", icon
='CURSOR')
1425 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1426 row
.label(text
="Origin [0,0,0]")
1428 layout
.prop(sce
, "measure_panel_draw")
1432 row
.label(text
="Selection not supported",
1435 if drawTansformButtons
:
1438 "measure_panel_transform",
1443 bpy
.utils
.register_module(__name__
)
1445 bpy
.app
.handlers
.scene_update_post
.append(scene_update
)
1447 # Define a temporary attribute for the distance value
1448 bpy
.types
.Scene
.measure_panel_dist
= bpy
.props
.FloatProperty(
1450 precision
=PRECISION
,
1452 bpy
.types
.Scene
.measure_panel_edge_length
= bpy
.props
.FloatProperty(
1454 precision
=PRECISION
,
1456 bpy
.types
.Scene
.measure_panel_area1
= bpy
.props
.FloatProperty(
1458 precision
=PRECISION
,
1460 bpy
.types
.Scene
.measure_panel_area2
= bpy
.props
.FloatProperty(
1462 precision
=PRECISION
,
1464 bpy
.types
.Scene
.measure_panel_normal1
= bpy
.props
.FloatVectorProperty(
1466 precision
=PRECISION
,
1468 bpy
.types
.Scene
.measure_panel_normal2
= bpy
.props
.FloatVectorProperty(
1470 precision
=PRECISION
,
1472 bpy
.types
.Scene
.measure_panel_volume1
= bpy
.props
.FloatProperty(
1474 precision
=PRECISION
,
1476 bpy
.types
.Scene
.measure_panel_volume2
= bpy
.props
.FloatProperty(
1478 precision
=PRECISION
,
1482 ("measure_global", "Global",
1483 "Calculate values in global space"),
1484 ("measure_local", "Local",
1485 "Calculate values inside the local object space")]
1487 # Define dropdown for the global/local setting
1488 bpy
.types
.Scene
.measure_panel_transform
= bpy
.props
.EnumProperty(
1490 description
="Choose in which space you want to measure",
1492 default
='measure_global')
1494 # Define property for the draw setting.
1495 bpy
.types
.Scene
.measure_panel_draw
= bpy
.props
.BoolProperty(
1496 name
="Draw distance",
1497 description
="Draw distances in 3D View",
1500 bpy
.types
.Scene
.measure_panel_calc_edge_length
= bpy
.props
.BoolProperty(
1501 description
="Calculate total length of (selected) edges",
1504 # Define property for the calc-area setting.
1505 # @todo prevent double calculations for each refresh automatically?
1506 bpy
.types
.Scene
.measure_panel_calc_area
= bpy
.props
.BoolProperty(
1507 description
="Calculate mesh surface area (heavy CPU "
1508 "usage on bigger meshes)",
1511 # Define property for the calc-volume setting.
1512 bpy
.types
.Scene
.measure_panel_calc_volume
= bpy
.props
.BoolProperty(
1513 description
="Calculate mesh volume (heavy CPU "
1514 "usage on bigger meshes)",
1517 # Define dropdown for the global/local setting
1518 bpy
.types
.Scene
.measure_panel_update
= bpy
.props
.BoolProperty(
1519 description
="Update CPU heavy calculations",
1526 bpy
.utils
.unregister_module(__name__
)
1528 bpy
.app
.handlers
.scene_update_post
.remove(scene_update
)
1530 # Remove properties.
1531 del bpy
.types
.Scene
.measure_panel_dist
1532 del bpy
.types
.Scene
.measure_panel_edge_length
1533 del bpy
.types
.Scene
.measure_panel_area1
1534 del bpy
.types
.Scene
.measure_panel_area2
1535 del bpy
.types
.Scene
.measure_panel_normal1
1536 del bpy
.types
.Scene
.measure_panel_normal2
1537 del bpy
.types
.Scene
.measure_panel_volume1
1538 del bpy
.types
.Scene
.measure_panel_volume2
1539 del bpy
.types
.Scene
.measure_panel_transform
1540 del bpy
.types
.Scene
.measure_panel_draw
1541 del bpy
.types
.Scene
.measure_panel_calc_edge_length
1542 del bpy
.types
.Scene
.measure_panel_calc_area
1543 del bpy
.types
.Scene
.measure_panel_calc_volume
1544 del bpy
.types
.Scene
.measure_panel_update
1548 if __name__
== "__main__":