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), "
30 "Daniel Ashby (callback removal code) ",
32 "blender": (2, 60, 0),
33 "location": "View3D > Properties > Measure Panel",
34 "description": "Measure distances between objects",
35 "warning": "Script needs repairs",
36 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
37 "Scripts/3D_interaction/Panel_Measure",
38 "category": "3D View",
44 This script displays in OBJECT MODE:
45 * The distance of the 3D cursor to the origin of the
46 3D space (if NOTHING is selected).
47 * The distance of the 3D cursor to the center of an object
48 (if exactly ONE object is selected).
49 * The distance between 2 object centers
50 (if exactly TWO objects are selected).
51 * The surface area of any selected mesh object.
52 * The average normal of the mesh surface of any selected mesh object.
53 * The volume of any selected mesh object.
55 Display in EDIT MODE (Local and Global space supported):
56 * The distance of the 3D cursor to the origin
57 (in Local space it is the object center instead).
58 * The distance of the 3D cursor to a selected vertex.
59 * The distance between 2 selected vertices.
63 This functionality can be accessed via the
64 "Properties" panel in 3D View ([N] key).
66 It's very helpful to use one or two "Empty" objects with
67 "Snap during transform" enabled for fast measurement.
70 http://gitorious.org/blender-scripts/blender-measure-panel-script
71 http://blenderartists.org/forum/showthread.php?t=177800
75 from bpy
.props
import *
76 from bpy
.app
.handlers
import persistent
77 from mathutils
import Vector
, Matrix
80 from bpy_extras
.view3d_utils
import location_3d_to_region_2d
81 from bpy_extras
.mesh_utils
import ngon_tessellate
84 # Precicion for display of float values.
87 # Name of the custom properties as stored in the scene.
88 COLOR_LOCAL
= (1.0, 0.5, 0.0, 0.8)
89 COLOR_GLOBAL
= (0.5, 0.0, 1.0, 0.8)
91 # 3D View - text offset
92 OFFSET_LINE
= 10 # Offset the text a bit to the right.
93 OFFSET_Y
= 15 # Offset of the lines.
94 OFFSET_VALUE
= 30 # Offset of value(s) from the text.
96 # 3D View - line width
101 # Returns a tuple describing the current measuring system
102 # and formatting options.
103 # Returned data is meant to be passed to formatDistance().
104 # Original by Alessandro Sala (Feb, 12th 2012)
105 # Update by Alessandro Sala (Dec, 18th 2012)
107 scale
= bpy
.context
.scene
.unit_settings
.scale_length
108 unit_system
= bpy
.context
.scene
.unit_settings
.system
109 separate_units
= bpy
.context
.scene
.unit_settings
.use_separate
110 if unit_system
== 'METRIC':
111 scale_steps
= ((1000, 'km'), (1, 'm'), (1 / 100, 'cm'),
112 (1 / 1000, 'mm'), (1 / 1000000, '\u00b5m'))
113 elif unit_system
== 'IMPERIAL':
114 scale_steps
= ((5280, 'mi'), (1, '\''),
115 (1 / 12, '"'), (1 / 12000, 'thou'))
116 scale
/= 0.3048 # BU to feet
118 scale_steps
= ((1, ' BU'),)
119 separate_units
= False
121 return (scale
, scale_steps
, separate_units
)
124 # Converts a distance from BU into the measuring system
125 # described by units_info.
126 # Original by Alessandro Sala (Feb, 12th 2012)
127 # Update by Alessandro Sala (Dec, 18th 2012)
128 def convertDistance(val
, units_info
):
129 scale
, scale_steps
, separate_units
= units_info
132 while idx
< len(scale_steps
) - 1:
133 if sval
>= scale_steps
[idx
][0]:
136 factor
, suffix
= scale_steps
[idx
]
138 if not separate_units
or idx
== len(scale_steps
) - 1:
139 dval
= str(round(sval
, PRECISION
)) + suffix
142 dval
= str(round(ival
, PRECISION
)) + suffix
145 while idx
< len(scale_steps
):
146 fval
*= scale_steps
[idx
- 1][0] / scale_steps
[idx
][0]
150 + scale_steps
[idx
][1]
157 # Returns a single selected object.
158 # Returns None if more than one (or nothing) is selected.
159 # Note: Ignores the active object.
160 def getSingleObject():
161 if len(bpy
.context
.selected_objects
) == 1:
162 return bpy
.context
.selected_objects
[0]
167 # Returns a list with 2 3D points (Vector) and a color (RGBA)
168 # depending on the current view mode and the selection.
169 def getMeasurePoints(context
):
173 # Get a single selected object (or nothing).
174 obj
= getSingleObject()
176 if mode
== 'EDIT_MESH':
177 obj
= context
.active_object
179 if obj
and obj
.type == 'MESH' and obj
.data
:
180 # Get mesh data from Object.
183 # Get the selected vertices.
184 # @todo: Better (more efficient) way to do this?
185 verts_selected
= [v
for v
in mesh
.vertices
if v
.select
== 1]
187 if len(verts_selected
) == 0:
189 # We measure the distance from...
190 # local ... the object center to the 3D cursor.
191 # global ... the origin to the 3D cursor.
192 cur_loc
= sce
.cursor_location
193 obj_loc
= obj
.matrix_world
.to_translation()
195 # Convert to local space, if needed.
196 if measureLocal(sce
):
199 return (p1
, p2
, COLOR_GLOBAL
)
202 p1
= Vector((0.0, 0.0, 0.0))
204 return (p1
, p2
, COLOR_GLOBAL
)
206 elif len(verts_selected
) == 1:
207 # One vertex selected.
208 # We measure the distance from the
209 # selected vertex object to the 3D cursor.
210 cur_loc
= sce
.cursor_location
211 vert_loc
= verts_selected
[0].co
.copy()
213 # Convert to local or global space.
214 if measureLocal(sce
):
217 return (p1
, p2
, COLOR_LOCAL
)
220 p1
= obj
.matrix_world
* vert_loc
222 return (p1
, p2
, COLOR_GLOBAL
)
224 elif len(verts_selected
) == 2:
225 # Two vertices selected.
226 # We measure the distance between the
227 # two selected vertices.
228 obj_loc
= obj
.matrix_world
.to_translation()
229 vert1_loc
= verts_selected
[0].co
.copy()
230 vert2_loc
= verts_selected
[1].co
.copy()
232 # Convert to local or global space.
233 if measureLocal(sce
):
236 return (p1
, p2
, COLOR_LOCAL
)
239 p1
= obj
.matrix_world
* vert1_loc
240 p2
= obj
.matrix_world
* vert2_loc
241 return (p1
, p2
, COLOR_GLOBAL
)
246 elif mode
== 'OBJECT':
247 # We are working in object mode.
249 if len(context
.selected_objects
) > 2:
251 elif len(context
.selected_objects
) == 2:
252 # 2 objects selected.
253 # We measure the distance between the 2 selected objects.
254 obj1
, obj2
= context
.selected_objects
255 obj1_loc
= obj1
.matrix_world
.to_translation()
256 obj2_loc
= obj2
.matrix_world
.to_translation()
257 return (obj1_loc
, obj2_loc
, COLOR_GLOBAL
)
260 # One object selected.
261 # We measure the distance from the object to the 3D cursor.
262 cur_loc
= sce
.cursor_location
263 obj_loc
= obj
.matrix_world
.to_translation()
264 return (obj_loc
, cur_loc
, COLOR_GLOBAL
)
266 elif not context
.selected_objects
:
268 # We measure the distance from the origin to the 3D cursor.
269 p1
= Vector((0.0, 0.0, 0.0))
270 p2
= sce
.cursor_location
271 return (p1
, p2
, COLOR_GLOBAL
)
277 # Return the length of an edge (in global space if "obj" is set).
278 # Respects the scaling (via the "obj.matrix_world" parameter).
279 def edgeLengthGlobal(edge
, obj
, globalSpace
):
280 v1
, v2
= edge
.vertices
283 v1
= obj
.data
.vertices
[v1
]
284 v2
= obj
.data
.vertices
[v2
]
287 mat
= obj
.matrix_world
288 # Apply transform matrix to vertex coordinates.
295 return (v1
- v2
).length
298 # Calculate the edge length of a mesh object.
299 # *) Set selectedOnly=1 if you only want to count selected edges.
300 # *) Set globalSpace=1 if you want to calculate
301 # the global edge length (object mode).
302 # Note: Be sure you have updated the mesh data before
303 # running this with selectedOnly=1!
304 # @todo Support other object types (surfaces, etc...)?
305 def objectEdgeLength(obj
, selectedOnly
, globalSpace
):
306 if obj
and obj
.type == 'MESH' and obj
.data
:
311 # Count the length of all edges.
312 for ed
in mesh
.edges
:
313 if not selectedOnly
or ed
.select
:
314 edgeTotal
+= edgeLengthGlobal(ed
, obj
, globalSpace
)
318 # We can not calculate a length for this object.
322 # Return the area of a face (in global space).
323 # @note Copies the functionality of the following functions,
324 # but also respects the scaling (via the "obj.matrix_world" parameter):
325 # @sa: rna_mesh.c:rna_MeshTessFace_area_get
326 # @sa: math_geom.c:area_quad_v3
327 # @sa: math_geom.c:area_tri_v3
328 # @sa: math_geom.c:area_poly_v3
329 # @todo Fix calculation of "n" for n-gons?
330 def polyAreaGlobal(poly
, obj
):
332 mat
= obj
.matrix_world
.copy()
337 if len(poly
.vertices
) > 3:
338 # Tesselate the polygon into multiple tris
339 tris
= ngon_tessellate(mesh
, poly
.vertices
)
345 # Get indices from original poly
346 v1
= poly
.vertices
[v1
]
347 v2
= poly
.vertices
[v2
]
348 v3
= poly
.vertices
[v3
]
350 # Get vertex information from indices
351 v1
= mesh
.vertices
[v1
]
352 v2
= mesh
.vertices
[v2
]
353 v3
= mesh
.vertices
[v3
]
355 # Apply transform matrix to vertex coordinates.
360 # Calculate area for the new tri
366 area
+= n
.length
/ 2.0
368 elif len(poly
.vertices
) == 3:
372 v1
, v2
, v3
= poly
.vertices
375 v1
= mesh
.vertices
[v1
]
376 v2
= mesh
.vertices
[v2
]
377 v3
= mesh
.vertices
[v3
]
379 # Apply transform matrix to vertex coordinates.
389 area
= n
.length
/ 2.0
391 # Apply rotation and scale to the normal as well.
392 rot_mat
= obj
.matrix_world
.to_quaternion()
393 scale
= obj
.matrix_world
.to_scale()
394 norm
= rot_mat
* norm
398 norm
.z
* scale
.z
)).normalized()
403 # Calculate the surface area of a mesh object.
404 # *) Set selectedOnly=1 if you only want to count selected faces.
405 # *) Set globalSpace=1 if you want to calculate
406 # the global surface area (object mode).
407 # Note: Be sure you have updated the mesh data before
408 # running this with selectedOnly=1!
409 # @todo Support other object types (surfaces, etc...)?
410 def objectSurfaceArea(obj
, selectedOnly
, globalSpace
):
411 if obj
and obj
.type == 'MESH' and obj
.data
:
413 normTotal
= Vector((0.0, 0.0, 0.0))
417 # Count the area of all the faces.
418 for poly
in mesh
.polygons
:
419 if not selectedOnly
or poly
.select
:
421 a
, n
= polyAreaGlobal(poly
, obj
)
425 areaTotal
+= poly
.area
426 normTotal
+= poly
.normal
428 return areaTotal
, normTotal
430 # We can not calculate an area for this object.
431 return -1, Vector((0.0, 0.0, 0.0))
434 # Calculate the volume of a mesh object.
435 # Copyright Loonsbury (loonsbury@yahoo.com)
436 def objectVolume(obj
, globalSpace
):
437 if obj
and obj
.type == 'MESH' and obj
.data
:
439 # Check if mesh is non-manifold
440 if not checkManifold(obj
):
443 # Check if mesh has n-gons
451 for poly
in mesh
.polygons
:
454 if len(poly
.vertices
) == 4:
455 v1
, v2
, v3
, v4
= poly
.vertices
457 v1
, v2
, v3
= poly
.vertices
459 v1
= mesh
.vertices
[v1
]
460 v2
= mesh
.vertices
[v2
]
461 v3
= mesh
.vertices
[v3
]
463 # Scaled vert coordinates with object XYZ offsets for
464 # selection extremes/sizing.
466 x1
= v1
.co
[0] * obj
.scale
[0] + obj
.location
[0]
467 y1
= v1
.co
[1] * obj
.scale
[1] + obj
.location
[1]
468 z1
= v1
.co
[2] * obj
.scale
[2] + obj
.location
[2]
470 x2
= v2
.co
[0] * obj
.scale
[0] + obj
.location
[0]
471 y2
= v2
.co
[1] * obj
.scale
[1] + obj
.location
[1]
472 z2
= v2
.co
[2] * obj
.scale
[2] + obj
.location
[2]
474 x3
= v3
.co
[0] * obj
.scale
[0] + obj
.location
[0]
475 y3
= v3
.co
[1] * obj
.scale
[1] + obj
.location
[1]
476 z3
= v3
.co
[2] * obj
.scale
[2] + obj
.location
[2]
487 volume
= ((z1
+ z2
+ z3
) / 3.0) * pa
490 if len(poly
.vertices
) == 4:
492 v4
= mesh
.vertices
[v4
]
495 x4
= v4
.co
[0] * obj
.scale
[0] + obj
.location
[0]
496 y4
= v4
.co
[1] * obj
.scale
[1] + obj
.location
[1]
497 z4
= v4
.co
[2] * obj
.scale
[2] + obj
.location
[2]
507 volume
+= ((z1
+ z3
+ z4
) / 3.0) * pa
518 volTot
+= fzn
* volume
523 # print obj.name, ': Object must be a mesh!' # TODO
529 # Copyright Loonsbury (loonsbury@yahoo.com)
530 def checkManifold(obj
):
531 if obj
and obj
.type == 'MESH' and obj
.data
:
534 mc
= dict([(ed
.key
, 0) for ed
in mesh
.edges
]) # TODO
536 for p
in mesh
.polygons
:
537 for ek
in p
.edge_keys
:
542 mt
= [e
[1] for e
in mc
.items()]
548 if mt
[len(mt
) - 1] > 2:
557 # Check if a mesh has n-gons (polygon with more than 4 edges).
559 if obj
and obj
.type == 'MESH' and obj
.data
:
562 for p
in mesh
.polygons
:
563 if len(p
.vertices
) > 4:
572 # User friendly access to the "space" setting.
573 def measureGlobal(sce
):
574 return (sce
.measure_panel_transform
== "measure_global")
577 # User friendly access to the "space" setting.
578 def measureLocal(sce
):
579 return (sce
.measure_panel_transform
== "measure_local")
582 # Calculate values if geometry, selection or cursor changed.
584 def scene_update(context
):
586 mode
= bpy
.context
.mode
588 if (mode
== 'EDIT_MESH' and not sce
.measure_panel_update
):
591 if (bpy
.data
.objects
.is_updated
592 or bpy
.context
.scene
.is_updated
593 or sce
.measure_panel_update
):
594 # TODO: Better way to check selection changes and cursor changes?
596 sel_objs
= bpy
.context
.selected_objects
599 if sce
.measure_panel_calc_edge_length
:
600 if (mode
== 'EDIT_MESH'
601 and sce
.measure_panel_update
):
602 sce
.measure_panel_update
= 0
603 obj
= bpy
.context
.object
606 length_total
= objectEdgeLength(obj
, True,
608 sce
.measure_panel_edge_length
= length_total
610 elif mode
== 'OBJECT':
615 length
= objectEdgeLength(o
, False, measureGlobal(sce
))
621 length_total
+= length
623 sce
.measure_panel_edge_length
= length_total
626 # Handle mesh surface area calulations
627 if sce
.measure_panel_calc_area
:
628 if (mode
== 'EDIT_MESH'
629 and sce
.measure_panel_update
):
630 sce
.measure_panel_update
= 0
631 obj
= bpy
.context
.active_object
633 if obj
and obj
.type == 'MESH' and obj
.data
:
634 # "Note: a Mesh will return the selection state of the mesh
635 # when EditMode was last exited. A Python script operating
636 # in EditMode must exit EditMode before getting the current
637 # selection state of the mesh."
638 # http://www.blender.org/documentation/249PythonDoc/
639 # /Mesh.MVert-class.html#sel
640 # We can only provide this by existing &
641 # re-entering EditMode.
642 # @todo: Better way to do this?
644 # Get mesh data from Object.
647 # Get transformation matrix from object.
648 ob_mat
= obj
.matrix_world
649 # Also make an inversed copy! of the matrix.
650 ob_mat_inv
= ob_mat
.copy()
651 Matrix
.invert(ob_mat_inv
)
653 # Get the selected vertices.
654 # @todo: Better (more efficient) way to do this?
655 verts_selected
= [v
for v
in me
.vertices
if v
.select
== 1]
657 if len(verts_selected
) >= 3:
659 # @todo: Better (more efficient) way to do this?
660 polys_selected
= [p
for p
in me
.polygons
663 if len(polys_selected
) > 0:
664 area
, normal
= objectSurfaceArea(obj
, True,
667 sce
.measure_panel_area1
= area
668 sce
.measure_panel_normal1
= normal
670 elif mode
== 'OBJECT':
671 # We are working in object mode.
673 # Get a single selected object (or nothing).
674 obj
= getSingleObject()
676 if len(sel_objs
) > 2:
678 # @todo Make this work again.
679 # # We have more that 2 objects selected...
681 # mesh_objects = [o for o in context.selected_objects
682 # if o.type == 'MESH']
684 # if len(mesh_objects) > 0:
685 # # ... and at least one of them is a mesh.
687 # for o in mesh_objects:
688 # area = objectSurfaceArea(o, False,
689 # measureGlobal(sce))
691 # #row.label(text=o.name, icon='OBJECT_DATA')
692 # #row.label(text=str(round(area, PRECISION))
695 elif len(sel_objs
) == 2:
696 # 2 objects selected.
698 obj1
, obj2
= sel_objs
700 # Calculate surface area of the objects.
701 area1
, normal1
= objectSurfaceArea(obj1
, False,
703 area2
, normal2
= objectSurfaceArea(obj2
, False,
705 sce
.measure_panel_area1
= area1
706 sce
.measure_panel_area2
= area2
707 sce
.measure_panel_normal1
= normal1
708 sce
.measure_panel_normal2
= normal2
711 # One object selected.
713 # Calculate surface area of the object.
714 area
, normal
= objectSurfaceArea(obj
, False,
717 sce
.measure_panel_area1
= area
718 sce
.measure_panel_normal1
= normal
721 # Handle mesh volume calulations.
722 if sce
.measure_panel_calc_volume
:
723 obj
= getSingleObject()
726 # We are working in object mode.
728 #if len(sel_objs) > 2: # TODO
730 if len(sel_objs
) == 2:
731 # 2 objects selected.
733 obj1
, obj2
= sel_objs
735 # Calculate surface area of the objects.
736 volume1
= objectVolume(obj1
, measureGlobal(sce
))
737 volume2
= objectVolume(obj2
, measureGlobal(sce
))
739 sce
.measure_panel_volume1
= volume1
740 sce
.measure_panel_volume2
= volume2
743 # One object selected.
745 # Calculate surface area of the object.
746 volume1
= objectVolume(obj
, measureGlobal(sce
))
748 sce
.measure_panel_volume1
= volume1
751 def draw_measurements_callback(self
, context
):
755 if hasattr(sce
, "measure_panel_draw"):
756 draw
= sce
.measure_panel_draw
758 # 2D drawing code example
759 #bgl.glBegin(bgl.GL_LINE_STRIP)
760 #bgl.glVertex2i(0, 0)
761 #bgl.glVertex2i(80, 100)
764 # Get measured 3D points and colors.
765 line
= getMeasurePoints(context
)
770 dist
= (p1
- p2
).length
772 # Write distance value into the scene property,
773 # so we can display it in the panel & refresh the panel.
774 if hasattr(sce
, "measure_panel_dist"):
775 sce
.measure_panel_dist
= dist
776 context
.area
.tag_redraw()
779 # Get & convert the Perspective Matrix of the current view/region.
781 region
= view3d
.region_data
782 perspMatrix
= region
.perspective_matrix
783 tempMat
= [perspMatrix
[j
][i
] for i
in range(4) for j
in range(4)]
784 perspBuff
= bgl
.Buffer(bgl
.GL_FLOAT
, 16, tempMat
)
787 # Store previous OpenGL settings.
789 MatrixMode_prev
= bgl
.Buffer(bgl
.GL_INT
, [1])
790 bgl
.glGetIntegerv(bgl
.GL_MATRIX_MODE
, MatrixMode_prev
)
791 MatrixMode_prev
= MatrixMode_prev
[0]
793 # Store projection matrix
794 ProjMatrix_prev
= bgl
.Buffer(bgl
.GL_DOUBLE
, [16])
795 bgl
.glGetFloatv(bgl
.GL_PROJECTION_MATRIX
, ProjMatrix_prev
)
798 lineWidth_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [1])
799 bgl
.glGetFloatv(bgl
.GL_LINE_WIDTH
, lineWidth_prev
)
800 lineWidth_prev
= lineWidth_prev
[0]
803 blend_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
804 bgl
.glGetFloatv(bgl
.GL_BLEND
, blend_prev
)
805 blend_prev
= blend_prev
[0]
807 line_stipple_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
808 bgl
.glGetFloatv(bgl
.GL_LINE_STIPPLE
, line_stipple_prev
)
809 line_stipple_prev
= line_stipple_prev
[0]
812 color_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [4])
813 bgl
.glGetFloatv(bgl
.GL_COLOR
, color_prev
)
816 # Prepare for 3D drawing
818 bgl
.glMatrixMode(bgl
.GL_PROJECTION
)
819 bgl
.glLoadMatrixf(perspBuff
)
821 bgl
.glEnable(bgl
.GL_BLEND
)
822 bgl
.glEnable(bgl
.GL_LINE_STIPPLE
)
826 bgl
.glLineWidth(LINE_WIDTH_XYZ
)
828 bgl
.glColor4f(1, 0, 0, 0.8)
829 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
830 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
831 bgl
.glVertex3f(p2
[0], p1
[1], p1
[2])
834 bgl
.glColor4f(0, 1, 0, 0.8)
835 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
836 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
837 bgl
.glVertex3f(p1
[0], p2
[1], p1
[2])
840 bgl
.glColor4f(0, 0, 1, 0.8)
841 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
842 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
843 bgl
.glVertex3f(p1
[0], p1
[1], p2
[2])
847 bgl
.glLineWidth(LINE_WIDTH_DIST
)
848 bgl
.glColor4f(color
[0], color
[1], color
[2], color
[3])
849 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
850 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
851 bgl
.glVertex3f(p2
[0], p2
[1], p2
[2])
855 # Restore previous OpenGL settings
857 bgl
.glMatrixMode(MatrixMode_prev
)
858 bgl
.glLoadMatrixf(ProjMatrix_prev
)
859 bgl
.glLineWidth(lineWidth_prev
)
861 bgl
.glDisable(bgl
.GL_BLEND
)
862 if not line_stipple_prev
:
863 bgl
.glDisable(bgl
.GL_LINE_STIPPLE
)
872 # We do this after drawing the lines so
873 # we can draw it OVER the line.
874 coord_2d
= location_3d_to_region_2d(
876 context
.space_data
.region_3d
,
881 ("X:", abs(p1
[0] - p2
[0])),
882 ("Y:", abs(p1
[1] - p2
[1])),
883 ("Z:", abs(p1
[2] - p2
[2]))]
886 # @todo Get user pref for text color in 3D View
887 bgl
.glColor4f(1.0, 1.0, 1.0, 1.0)
888 blf
.size(0, 12, 72) # Prevent font size to randomly change.
890 uinfo
= getUnitsInfo()
892 loc_x
= coord_2d
[0] + OFFSET_LINE
898 value
= convertDistance(t
[1], uinfo
)
900 blf
.position(0, loc_x
, loc_y
, 0)
902 blf
.position(0, loc_x
+ OFFSET_VALUE
, loc_y
, 0)
908 # Callback code Daniel Ashby 2014-10-30
909 class VIEW3D_OT_display_measurements(bpy
.types
.Operator
):
910 """Display the measurements made in the 'Measure' panel"""
911 bl_idname
= "view3d.display_measurements"
912 bl_label
= "Display measurements"
913 bl_description
= "Display the measurements made in the" \
914 " 'Measure' panel in the 3D View"
915 bl_options
= {'REGISTER'} # TODO: can this be removed?
919 def handle_add(self
, context
):
920 VIEW3D_OT_display_measurements
._handle \
921 = bpy
.types
.SpaceView3D
.draw_handler_add(
922 draw_measurements_callback
,
924 'WINDOW', 'POST_PIXEL')
927 def handle_remove(context
):
928 if VIEW3D_OT_display_measurements
._handle
is not None:
929 bpy
.types
.SpaceView3D
.draw_handler_remove(
930 VIEW3D_OT_display_measurements
._handle
,
932 VIEW3D_OT_display_measurements
._handle
= None
934 def modal(self
, context
, event
):
936 context
.area
.tag_redraw
938 if not context
.window_manager
.display_measurements_runstate
:
940 VIEW3D_OT_display_measurements
.handle_remove(context
)
943 return {'PASS_THROUGH'}
945 def cancel(self
, context
):
946 if context
.window_manager
.display_measurements_runstate
:
947 display_measurements
.handle_remove(context
)
948 context
.window_manager
.display_measurements_runstate
= False
951 def invoke(self
, context
, event
):
952 if context
.area
.type == 'VIEW_3D':
953 if context
.window_manager
.display_measurements_runstate
is False:
954 # operator is called for the first time, start everything
955 context
.window_manager
.display_measurements_runstate
= True
956 VIEW3D_OT_display_measurements
.handle_add(self
, context
)
957 context
.window_manager
.modal_handler_add(self
)
958 return {'RUNNING_MODAL'}
961 # operator is called again, stop displaying
962 context
.window_manager
.display_measurements_runstate
= False
966 self
.report({'WARNING'}, "3D View not found, can't run operator"
967 " for 'Display measurements'")
971 class VIEW3D_OT_activate_measure_panel(bpy
.types
.Operator
):
972 bl_label
= "Activate"
973 bl_idname
= "view3d.activate_measure_panel"
974 bl_description
= "Activate the callback needed to draw the lines"
975 bl_options
= {'REGISTER'}
977 def invoke(self
, context
, event
):
979 # Execute operator (this adds the callback)
980 # if it wasn't done yet.
981 bpy
.ops
.view3d
.display_measurements()
985 class VIEW3D_OT_reenter_editmode(bpy
.types
.Operator
):
986 bl_label
= "Re-enter EditMode"
987 bl_idname
= "view3d.reenter_editmode"
988 bl_description
= "Update mesh data of an active mesh object " \
989 "(this is done by exiting and re-entering mesh edit mode)"
990 bl_options
= {'REGISTER'}
992 def invoke(self
, context
, event
):
994 # Get the active object.
995 obj
= context
.active_object
998 if obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH':
999 # Exit and re-enter mesh EditMode.
1000 bpy
.ops
.object.mode_set(mode
='OBJECT')
1001 bpy
.ops
.object.mode_set(mode
='EDIT')
1002 sce
.measure_panel_update
= 1
1005 return {'CANCELLED'}
1008 class VIEW3D_PT_measure(bpy
.types
.Panel
):
1009 bl_space_type
= 'VIEW_3D'
1010 bl_region_type
= 'UI'
1011 bl_label
= "Measure"
1012 bl_options
= {'DEFAULT_CLOSED'}
1015 def poll(cls
, context
):
1016 # Only display this panel in the object and edit mode 3D view.
1018 if (context
.area
.type == 'VIEW_3D' and
1019 (mode
== 'EDIT_MESH' or mode
== 'OBJECT')):
1024 def draw_header(self
, context
):
1025 layout
= self
.layout
1028 if not context
.window_manager
.display_measurements_runstate
:
1029 layout
.operator("view3d.display_measurements", text
="Activate",
1032 def draw(self
, context
):
1033 layout
= self
.layout
1037 # Get a single selected object (or nothing).
1038 obj
= getSingleObject()
1040 drawTansformButtons
= 1
1042 if mode
== 'EDIT_MESH':
1043 obj
= context
.active_object
1046 row
.operator("view3d.reenter_editmode",
1047 text
="Update selection")
1049 # description="The calculated values can" \
1050 # " not be updated in mesh edit mode" \
1051 # " automatically. Press this button" \
1052 # " to do this manually, after you changed" \
1055 if obj
and obj
.type == 'MESH' and obj
.data
:
1056 # "Note: a Mesh will return the selection state of the mesh
1057 # when EditMode was last exited. A Python script operating
1058 # in EditMode must exit EditMode before getting the current
1059 # selection state of the mesh."
1060 # http://www.blender.org/documentation/249PythonDoc/
1061 # /Mesh.MVert-class.html#sel
1062 # We can only provide this by existing & re-entering EditMode.
1063 # @todo: Better way to do this?
1065 # Get mesh data from Object.
1068 # Get transformation matrix from object.
1069 ob_mat
= obj
.matrix_world
1070 # Also make an inversed copy! of the matrix.
1071 ob_mat_inv
= ob_mat
.copy()
1072 Matrix
.invert(ob_mat_inv
)
1074 # Get the selected vertices.
1075 # @todo: Better (more efficient) way to do this?
1076 verts_selected
= [v
for v
in mesh
.vertices
if v
.select
== 1]
1078 if len(verts_selected
) == 0:
1080 # We measure the distance from...
1081 # local ... the object center to the 3D cursor.
1082 # global ... the origin to the 3D cursor.
1083 layout
.label(text
="Distance")
1087 row
.prop(sce
, "measure_panel_dist")
1090 row
.label(text
="", icon
='CURSOR')
1091 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1092 if measureLocal(sce
):
1093 row
.label(text
="Obj. Center")
1095 row
.label(text
="Origin [0,0,0]")
1097 layout
.prop(sce
, "measure_panel_draw")
1099 elif len(verts_selected
) == 1:
1100 # One vertex selected.
1101 # We measure the distance from the
1102 # selected vertex object to the 3D cursor.
1103 layout
.label(text
="Distance")
1107 row
.prop(sce
, "measure_panel_dist")
1110 row
.label(text
="", icon
='CURSOR')
1111 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1112 row
.label(text
="", icon
='VERTEXSEL')
1114 layout
.prop(sce
, "measure_panel_draw")
1116 elif len(verts_selected
) == 2:
1117 # Two vertices selected.
1118 # We measure the distance between the
1119 # two selected vertices.
1120 layout
.label(text
="Distance")
1124 row
.prop(sce
, "measure_panel_dist")
1127 row
.label(text
="", icon
='VERTEXSEL')
1128 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1129 row
.label(text
="", icon
='VERTEXSEL')
1131 layout
.prop(sce
, "measure_panel_draw")
1133 edges_selected
= [ed
for ed
in mesh
.edges
if ed
.select
== 1]
1134 if len(edges_selected
) >= 1:
1136 row
.prop(sce
, "measure_panel_calc_edge_length",
1137 text
="Edge Length (selected edges)")
1139 if sce
.measure_panel_calc_edge_length
:
1140 if sce
.measure_panel_edge_length
>= 0:
1144 text
=str(len(edges_selected
)),
1148 row
.label(text
="Length")
1149 row
.prop(sce
, "measure_panel_edge_length")
1151 if len(verts_selected
) > 2:
1153 row
.prop(sce
, "measure_panel_calc_area",
1154 text
="Surface area (selected faces)")
1156 if sce
.measure_panel_calc_area
:
1157 # Get selected faces
1158 # @todo: Better (more efficient) way to do this?
1159 polys_selected
= [p
for p
in mesh
.polygons
1162 if len(polys_selected
) > 0:
1163 if sce
.measure_panel_area1
>= 0:
1167 text
=str(len(polys_selected
)),
1171 row
.label(text
="Area")
1172 row
.prop(sce
, "measure_panel_area1")
1175 row
.label(text
="Normal")
1177 row
.prop(sce
, "measure_panel_normal1")
1181 row
.label(text
="Selection not supported",
1184 if drawTansformButtons
:
1187 "measure_panel_transform",
1190 elif mode
== 'OBJECT':
1191 # We are working in object mode.
1193 mesh_objects
= [o
for o
in context
.selected_objects
1194 if o
.type == 'MESH']
1196 if len(context
.selected_objects
) > 2:
1197 # We have more that 2 objects selected...
1201 row
.prop(sce
, "measure_panel_calc_edge_length",
1204 if sce
.measure_panel_calc_edge_length
:
1205 if len(mesh_objects
) > 0:
1209 row
.label(text
="Total edge length")
1210 row
.prop(sce
, "measure_panel_edge_length")
1214 row
.prop(sce
, "measure_panel_calc_area",
1215 text
="Surface area")
1217 if sce
.measure_panel_calc_area
:
1218 if len(mesh_objects
) > 0:
1219 # ... and at least one of them is a mesh.
1221 # Calculate and display surface area of the objects.
1222 # @todo: Convert to scene units! We do not have a
1223 # FloatProperty field here for automatic conversion.
1226 row
.label(text
="Multiple objects not yet supported",
1229 row
.label(text
="(= More than two meshes)",
1231 # @todo Make this work again.
1232 # for o in mesh_objects:
1233 # area = objectSurfaceArea(o, False,
1234 # measureGlobal(sce))
1236 # row = layout.row()
1237 # row.label(text=o.name, icon='OBJECT_DATA')
1238 # row.label(text=str(round(area, PRECISION))
1241 elif len(context
.selected_objects
) == 2:
1242 # 2 objects selected.
1243 # We measure the distance between the 2 selected objects.
1244 layout
.label(text
="Distance")
1246 obj1
, obj2
= context
.selected_objects
1250 row
.prop(sce
, "measure_panel_dist")
1253 row
.label(text
="", icon
='OBJECT_DATA')
1254 row
.prop(obj1
, "name", text
="")
1256 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1258 row
.label(text
="", icon
='OBJECT_DATA')
1259 row
.prop(obj2
, "name", text
="")
1261 layout
.prop(sce
, "measure_panel_draw")
1265 row
.prop(sce
, "measure_panel_calc_edge_length",
1268 if sce
.measure_panel_calc_edge_length
:
1269 if sce
.measure_panel_edge_length
>= 0:
1270 if len(mesh_objects
) > 0:
1274 row
.label(text
="Total edge length")
1275 row
.prop(sce
, "measure_panel_edge_length")
1280 row
.prop(sce
, "measure_panel_calc_area",
1281 text
="Surface area")
1283 if sce
.measure_panel_calc_area
:
1284 # Display surface area of the objects.
1285 if (sce
.measure_panel_area1
>= 0
1286 or sce
.measure_panel_area2
>= 0):
1287 if sce
.measure_panel_area1
>= 0:
1290 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1293 row
.label(text
="Area")
1294 row
.prop(sce
, "measure_panel_area1")
1297 row
.label(text
="Normal")
1299 row
.prop(sce
, "measure_panel_normal1")
1301 if sce
.measure_panel_area2
>= 0:
1304 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1307 row
.label(text
="Area")
1308 row
.prop(sce
, "measure_panel_area2")
1311 row
.label(text
="Normal")
1313 row
.prop(sce
, "measure_panel_normal2")
1317 row
.prop(sce
, "measure_panel_calc_volume",
1320 if sce
.measure_panel_calc_volume
:
1321 # Display volume of the objects.
1322 if sce
.measure_panel_volume1
>= -2:
1325 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1327 if sce
.measure_panel_volume1
>= 0:
1329 row
.label(text
="Volume")
1330 row
.prop(sce
, "measure_panel_volume1")
1331 elif sce
.measure_panel_volume1
>= -1:
1333 row
.label(text
="Mesh is non-manifold!",
1337 row
.label(text
="Mesh has n-gons (faces with " \
1338 "more than 4 edges)!",
1341 if sce
.measure_panel_volume2
>= -2:
1344 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1346 if sce
.measure_panel_volume2
>= 0:
1348 row
.label(text
="Volume")
1349 row
.prop(sce
, "measure_panel_volume2")
1350 elif sce
.measure_panel_volume2
>= -1:
1352 row
.label(text
="Mesh is non-manifold!",
1356 row
.label(text
="Mesh has n-gons (faces with " \
1357 "more than 4 edges)!",
1361 # One object selected.
1362 # We measure the distance from the object to the 3D cursor.
1363 layout
.label(text
="Distance")
1367 row
.prop(sce
, "measure_panel_dist")
1370 row
.label(text
="", icon
='CURSOR')
1372 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1374 row
.label(text
="", icon
='OBJECT_DATA')
1375 row
.prop(obj
, "name", text
="")
1377 layout
.prop(sce
, "measure_panel_draw")
1381 row
.prop(sce
, "measure_panel_calc_edge_length",
1384 if sce
.measure_panel_calc_edge_length
:
1385 if sce
.measure_panel_edge_length
>= 0:
1386 if len(mesh_objects
) > 0:
1390 row
.label(text
="Total edge length")
1391 row
.prop(sce
, "measure_panel_edge_length")
1395 row
.prop(sce
, "measure_panel_calc_area",
1396 text
="Surface area")
1398 if sce
.measure_panel_calc_area
:
1399 # Display surface area of the object.
1401 if sce
.measure_panel_area1
>= 0.0:
1404 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1407 row
.label(text
="Area")
1408 row
.prop(sce
, "measure_panel_area1")
1411 row
.label(text
="Normal")
1413 row
.prop(sce
, "measure_panel_normal1")
1417 row
.prop(sce
, "measure_panel_calc_volume",
1420 if sce
.measure_panel_calc_volume
:
1421 # Display volume of the objects.
1422 if sce
.measure_panel_volume1
>= -2:
1425 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1427 if sce
.measure_panel_volume1
>= 0:
1429 row
.label(text
="Volume")
1430 row
.prop(sce
, "measure_panel_volume1")
1431 elif sce
.measure_panel_volume1
>= -1:
1433 row
.label(text
="Mesh is non-manifold!",
1437 row
.label(text
="Mesh has n-gons (faces with " \
1438 "more than 4 edges)!",
1441 elif not context
.selected_objects
:
1443 # We measure the distance from the origin to the 3D cursor.
1444 layout
.label(text
="Distance")
1448 row
.prop(sce
, "measure_panel_dist")
1451 row
.label(text
="", icon
='CURSOR')
1452 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1453 row
.label(text
="Origin [0,0,0]")
1455 layout
.prop(sce
, "measure_panel_draw")
1459 row
.label(text
="Selection not supported",
1462 if drawTansformButtons
:
1465 "measure_panel_transform",
1469 VIEW3D_OT_display_measurements
,
1470 VIEW3D_OT_reenter_editmode
,
1475 bpy
.app
.handlers
.scene_update_post
.append(scene_update
)
1477 # Define a temporary attribute for the distance value
1478 bpy
.types
.Scene
.measure_panel_dist
= bpy
.props
.FloatProperty(
1480 precision
=PRECISION
,
1482 bpy
.types
.Scene
.measure_panel_edge_length
= bpy
.props
.FloatProperty(
1484 precision
=PRECISION
,
1486 bpy
.types
.Scene
.measure_panel_area1
= bpy
.props
.FloatProperty(
1488 precision
=PRECISION
,
1490 bpy
.types
.Scene
.measure_panel_area2
= bpy
.props
.FloatProperty(
1492 precision
=PRECISION
,
1494 bpy
.types
.Scene
.measure_panel_normal1
= bpy
.props
.FloatVectorProperty(
1496 precision
=PRECISION
,
1498 bpy
.types
.Scene
.measure_panel_normal2
= bpy
.props
.FloatVectorProperty(
1500 precision
=PRECISION
,
1502 bpy
.types
.Scene
.measure_panel_volume1
= bpy
.props
.FloatProperty(
1504 precision
=PRECISION
,
1506 bpy
.types
.Scene
.measure_panel_volume2
= bpy
.props
.FloatProperty(
1508 precision
=PRECISION
,
1512 ("measure_global", "Global",
1513 "Calculate values in global space"),
1514 ("measure_local", "Local",
1515 "Calculate values inside the local object space")]
1517 # Define dropdown for the global/local setting
1518 bpy
.types
.Scene
.measure_panel_transform
= bpy
.props
.EnumProperty(
1520 description
="Choose in which space you want to measure",
1522 default
='measure_global')
1524 # Define property for the draw setting.
1525 bpy
.types
.Scene
.measure_panel_draw
= bpy
.props
.BoolProperty(
1526 name
="Draw distance",
1527 description
="Draw distances in 3D View",
1530 bpy
.types
.Scene
.measure_panel_calc_edge_length
= bpy
.props
.BoolProperty(
1531 description
="Calculate total length of (selected) edges",
1534 # Define property for the calc-area setting.
1535 # @todo prevent double calculations for each refresh automatically?
1536 bpy
.types
.Scene
.measure_panel_calc_area
= bpy
.props
.BoolProperty(
1537 description
="Calculate mesh surface area (heavy CPU "
1538 "usage on bigger meshes)",
1541 # Define property for the calc-volume setting.
1542 bpy
.types
.Scene
.measure_panel_calc_volume
= bpy
.props
.BoolProperty(
1543 description
="Calculate mesh volume (heavy CPU "
1544 "usage on bigger meshes)",
1547 # Define dropdown for the global/local setting
1548 bpy
.types
.Scene
.measure_panel_update
= bpy
.props
.BoolProperty(
1549 description
="Update CPU heavy calculations",
1552 # Callback code Daniel Ashby 2014-10-30
1553 # Runstate initially always set to False
1554 # note: it is not stored in the Scene, but in window manager:
1555 wm
= bpy
.types
.WindowManager
1556 wm
.display_measurements_runstate
= bpy
.props
.BoolProperty(default
=False)
1559 bpy
.utils
.register_class(c
)
1563 bpy
.app
.handlers
.scene_update_post
.remove(scene_update
)
1565 VIEW3D_OT_display_measurements
.handle_remove(bpy
.context
)
1568 bpy
.utils
.unregister_class(c
)
1570 # Remove properties.
1571 del bpy
.types
.Scene
.measure_panel_dist
1572 del bpy
.types
.Scene
.measure_panel_edge_length
1573 del bpy
.types
.Scene
.measure_panel_area1
1574 del bpy
.types
.Scene
.measure_panel_area2
1575 del bpy
.types
.Scene
.measure_panel_normal1
1576 del bpy
.types
.Scene
.measure_panel_normal2
1577 del bpy
.types
.Scene
.measure_panel_volume1
1578 del bpy
.types
.Scene
.measure_panel_volume2
1579 del bpy
.types
.Scene
.measure_panel_transform
1580 del bpy
.types
.Scene
.measure_panel_draw
1581 del bpy
.types
.Scene
.measure_panel_calc_edge_length
1582 del bpy
.types
.Scene
.measure_panel_calc_area
1583 del bpy
.types
.Scene
.measure_panel_calc_volume
1584 del bpy
.types
.Scene
.measure_panel_update
1586 if __name__
== "__main__":