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 "tracker_url": "https://projects.blender.org/tracker/index.php?" \
38 "func=detail&aid=21445",
39 "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
= context
.active_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)
765 # Get measured 3D points and colors.
766 line
= getMeasurePoints(context
)
771 # Get & convert the Perspective Matrix of the current view/region.
773 region
= view3d
.region_data
774 perspMatrix
= region
.perspective_matrix
775 tempMat
= [perspMatrix
[j
][i
] for i
in range(4) for j
in range(4)]
776 perspBuff
= bgl
.Buffer(bgl
.GL_FLOAT
, 16, tempMat
)
779 # Store previous OpenGL settings.
781 MatrixMode_prev
= bgl
.Buffer(bgl
.GL_INT
, [1])
782 bgl
.glGetIntegerv(bgl
.GL_MATRIX_MODE
, MatrixMode_prev
)
783 MatrixMode_prev
= MatrixMode_prev
[0]
785 # Store projection matrix
786 ProjMatrix_prev
= bgl
.Buffer(bgl
.GL_DOUBLE
, [16])
787 bgl
.glGetFloatv(bgl
.GL_PROJECTION_MATRIX
, ProjMatrix_prev
)
790 lineWidth_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [1])
791 bgl
.glGetFloatv(bgl
.GL_LINE_WIDTH
, lineWidth_prev
)
792 lineWidth_prev
= lineWidth_prev
[0]
795 blend_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
796 bgl
.glGetFloatv(bgl
.GL_BLEND
, blend_prev
)
797 blend_prev
= blend_prev
[0]
799 line_stipple_prev
= bgl
.Buffer(bgl
.GL_BYTE
, [1])
800 bgl
.glGetFloatv(bgl
.GL_LINE_STIPPLE
, line_stipple_prev
)
801 line_stipple_prev
= line_stipple_prev
[0]
804 color_prev
= bgl
.Buffer(bgl
.GL_FLOAT
, [4])
805 bgl
.glGetFloatv(bgl
.GL_COLOR
, color_prev
)
808 # Prepare for 3D drawing
810 bgl
.glMatrixMode(bgl
.GL_PROJECTION
)
811 bgl
.glLoadMatrixf(perspBuff
)
813 bgl
.glEnable(bgl
.GL_BLEND
)
814 bgl
.glEnable(bgl
.GL_LINE_STIPPLE
)
818 bgl
.glLineWidth(LINE_WIDTH_XYZ
)
820 bgl
.glColor4f(1, 0, 0, 0.8)
821 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
822 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
823 bgl
.glVertex3f(p2
[0], p1
[1], p1
[2])
826 bgl
.glColor4f(0, 1, 0, 0.8)
827 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
828 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
829 bgl
.glVertex3f(p1
[0], p2
[1], p1
[2])
832 bgl
.glColor4f(0, 0, 1, 0.8)
833 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
834 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
835 bgl
.glVertex3f(p1
[0], p1
[1], p2
[2])
839 bgl
.glLineWidth(LINE_WIDTH_DIST
)
840 bgl
.glColor4f(color
[0], color
[1], color
[2], color
[3])
841 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
842 bgl
.glVertex3f(p1
[0], p1
[1], p1
[2])
843 bgl
.glVertex3f(p2
[0], p2
[1], p2
[2])
847 # Restore previous OpenGL settings
849 bgl
.glMatrixMode(MatrixMode_prev
)
850 bgl
.glLoadMatrixf(ProjMatrix_prev
)
851 bgl
.glLineWidth(lineWidth_prev
)
853 bgl
.glDisable(bgl
.GL_BLEND
)
854 if not line_stipple_prev
:
855 bgl
.glDisable(bgl
.GL_LINE_STIPPLE
)
864 # We do this after drawing the lines so
865 # we can draw it OVER the line.
866 coord_2d
= location_3d_to_region_2d(
868 context
.space_data
.region_3d
,
870 dist
= (p1
- p2
).length
872 # Write distance value into the scene property,
873 # so we can display it in the panel & refresh the panel.
874 if hasattr(sce
, "measure_panel_dist"):
875 sce
.measure_panel_dist
= dist
876 context
.area
.tag_redraw()
880 ("X:", abs(p1
[0] - p2
[0])),
881 ("Y:", abs(p1
[1] - p2
[1])),
882 ("Z:", abs(p1
[2] - p2
[2]))]
885 # @todo Get user pref for text color in 3D View
886 bgl
.glColor4f(1.0, 1.0, 1.0, 1.0)
887 blf
.size(0, 12, 72) # Prevent font size to randomly change.
889 uinfo
= getUnitsInfo()
891 loc_x
= coord_2d
[0] + OFFSET_LINE
897 value
= convertDistance(t
[1], uinfo
)
899 blf
.position(0, loc_x
, loc_y
, 0)
901 blf
.position(0, loc_x
+ OFFSET_VALUE
, loc_y
, 0)
907 class VIEW3D_OT_display_measurements(bpy
.types
.Operator
):
908 """Display the measurements made in the 'Measure' panel"""
909 bl_idname
= "view3d.display_measurements"
910 bl_label
= "Display the measurements made in the" \
911 " 'Measure' panel in the 3D View"
912 bl_options
= {'REGISTER'}
914 def modal(self
, context
, event
):
915 context
.area
.tag_redraw()
918 def execute(self
, context
):
919 if context
.area
.type == 'VIEW_3D':
920 mgr_ops
= context
.window_manager
.operators
.values()
921 if not self
.bl_idname
in [op
.bl_idname
for op
in mgr_ops
]:
922 # Add the region OpenGL drawing callback
924 # XXX, this is never removed!, it should be! (at least when disabling the addon)
925 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(
926 draw_measurements_callback
,
928 'WINDOW', 'POST_PIXEL')
930 print("Measure panel display callback added")
932 # XXX, never removed!
933 context
.window_manager
.modal_handler_add(self
)
934 return {'RUNNING_MODAL'}
939 self
.report({'WARNING'}, "View3D not found, cannot run operator")
943 class VIEW3D_OT_activate_measure_panel(bpy
.types
.Operator
):
944 bl_label
= "Activate"
945 bl_idname
= "view3d.activate_measure_panel"
946 bl_description
= "Activate the callback needed to draw the lines"
947 bl_options
= {'REGISTER'}
949 def invoke(self
, context
, event
):
951 # Execute operator (this adds the callback)
952 # if it wasn't done yet.
953 bpy
.ops
.view3d
.display_measurements()
957 class VIEW3D_OT_reenter_editmode(bpy
.types
.Operator
):
958 bl_label
= "Re-enter EditMode"
959 bl_idname
= "view3d.reenter_editmode"
960 bl_description
= "Update mesh data of an active mesh object " \
961 "(this is done by exiting and re-entering mesh edit mode)"
962 bl_options
= {'REGISTER'}
964 def invoke(self
, context
, event
):
966 # Get the active object.
967 obj
= context
.active_object
970 if obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH':
971 # Exit and re-enter mesh EditMode.
972 bpy
.ops
.object.mode_set(mode
='OBJECT')
973 bpy
.ops
.object.mode_set(mode
='EDIT')
974 sce
.measure_panel_update
= 1
980 class VIEW3D_PT_measure(bpy
.types
.Panel
):
981 bl_space_type
= 'VIEW_3D'
982 bl_region_type
= 'UI'
984 bl_options
= {'DEFAULT_CLOSED'}
987 def poll(cls
, context
):
988 # Only display this panel in the object and edit mode 3D view.
990 if (context
.area
.type == 'VIEW_3D' and
991 (mode
== 'EDIT_MESH' or mode
== 'OBJECT')):
996 def draw_header(self
, context
):
1000 mgr_ops
= context
.window_manager
.operators
.values()
1001 if (not "VIEW3D_OT_display_measurements"
1002 in [op
.bl_idname
for op
in mgr_ops
]):
1003 layout
.operator("view3d.activate_measure_panel",
1006 def draw(self
, context
):
1007 layout
= self
.layout
1011 # Get a single selected object (or nothing).
1012 obj
= getSingleObject()
1014 drawTansformButtons
= 1
1016 if mode
== 'EDIT_MESH':
1017 obj
= context
.active_object
1020 row
.operator("view3d.reenter_editmode",
1021 text
="Update selection")
1023 # description="The calculated values can" \
1024 # " not be updated in mesh edit mode" \
1025 # " automatically. Press this button" \
1026 # " to do this manually, after you changed" \
1029 if obj
and obj
.type == 'MESH' and obj
.data
:
1030 # "Note: a Mesh will return the selection state of the mesh
1031 # when EditMode was last exited. A Python script operating
1032 # in EditMode must exit EditMode before getting the current
1033 # selection state of the mesh."
1034 # http://www.blender.org/documentation/249PythonDoc/
1035 # /Mesh.MVert-class.html#sel
1036 # We can only provide this by existing & re-entering EditMode.
1037 # @todo: Better way to do this?
1039 # Get mesh data from Object.
1042 # Get transformation matrix from object.
1043 ob_mat
= obj
.matrix_world
1044 # Also make an inversed copy! of the matrix.
1045 ob_mat_inv
= ob_mat
.copy()
1046 Matrix
.invert(ob_mat_inv
)
1048 # Get the selected vertices.
1049 # @todo: Better (more efficient) way to do this?
1050 verts_selected
= [v
for v
in mesh
.vertices
if v
.select
== 1]
1052 if len(verts_selected
) == 0:
1054 # We measure the distance from...
1055 # local ... the object center to the 3D cursor.
1056 # global ... the origin to the 3D cursor.
1057 layout
.label(text
="Distance")
1061 row
.prop(sce
, "measure_panel_dist")
1064 row
.label(text
="", icon
='CURSOR')
1065 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1066 if measureLocal(sce
):
1067 row
.label(text
="Obj. Center")
1069 row
.label(text
="Origin [0,0,0]")
1071 layout
.prop(sce
, "measure_panel_draw")
1073 elif len(verts_selected
) == 1:
1074 # One vertex selected.
1075 # We measure the distance from the
1076 # selected vertex object to the 3D cursor.
1077 layout
.label(text
="Distance")
1081 row
.prop(sce
, "measure_panel_dist")
1084 row
.label(text
="", icon
='CURSOR')
1085 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1086 row
.label(text
="", icon
='VERTEXSEL')
1088 layout
.prop(sce
, "measure_panel_draw")
1090 elif len(verts_selected
) == 2:
1091 # Two vertices selected.
1092 # We measure the distance between the
1093 # two selected vertices.
1094 layout
.label(text
="Distance")
1098 row
.prop(sce
, "measure_panel_dist")
1101 row
.label(text
="", icon
='VERTEXSEL')
1102 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1103 row
.label(text
="", icon
='VERTEXSEL')
1105 layout
.prop(sce
, "measure_panel_draw")
1107 edges_selected
= [ed
for ed
in mesh
.edges
if ed
.select
== 1]
1108 if len(edges_selected
) >= 1:
1110 row
.prop(sce
, "measure_panel_calc_edge_length",
1111 text
="Edge Length (selected edges)")
1113 if sce
.measure_panel_calc_edge_length
:
1114 if sce
.measure_panel_edge_length
>= 0:
1118 text
=str(len(edges_selected
)),
1122 row
.label(text
="Length")
1123 row
.prop(sce
, "measure_panel_edge_length")
1125 if len(verts_selected
) > 2:
1127 row
.prop(sce
, "measure_panel_calc_area",
1128 text
="Surface area (selected faces)")
1130 if sce
.measure_panel_calc_area
:
1131 # Get selected faces
1132 # @todo: Better (more efficient) way to do this?
1133 polys_selected
= [p
for p
in mesh
.polygons
1136 if len(polys_selected
) > 0:
1137 if sce
.measure_panel_area1
>= 0:
1141 text
=str(len(polys_selected
)),
1145 row
.label(text
="Area")
1146 row
.prop(sce
, "measure_panel_area1")
1149 row
.label(text
="Normal")
1151 row
.prop(sce
, "measure_panel_normal1")
1155 row
.label(text
="Selection not supported",
1158 if drawTansformButtons
:
1161 "measure_panel_transform",
1164 elif mode
== 'OBJECT':
1165 # We are working in object mode.
1167 mesh_objects
= [o
for o
in context
.selected_objects
1168 if o
.type == 'MESH']
1170 if len(context
.selected_objects
) > 2:
1171 # We have more that 2 objects selected...
1175 row
.prop(sce
, "measure_panel_calc_edge_length",
1178 if sce
.measure_panel_calc_edge_length
:
1179 if len(mesh_objects
) > 0:
1183 row
.label(text
="Total edge length")
1184 row
.prop(sce
, "measure_panel_edge_length")
1188 row
.prop(sce
, "measure_panel_calc_area",
1189 text
="Surface area")
1191 if sce
.measure_panel_calc_area
:
1192 if len(mesh_objects
) > 0:
1193 # ... and at least one of them is a mesh.
1195 # Calculate and display surface area of the objects.
1196 # @todo: Convert to scene units! We do not have a
1197 # FloatProperty field here for automatic conversion.
1200 row
.label(text
="Multiple objects not yet supported",
1203 row
.label(text
="(= More than two meshes)",
1205 # @todo Make this work again.
1206 # for o in mesh_objects:
1207 # area = objectSurfaceArea(o, False,
1208 # measureGlobal(sce))
1210 # row = layout.row()
1211 # row.label(text=o.name, icon='OBJECT_DATA')
1212 # row.label(text=str(round(area, PRECISION))
1215 elif len(context
.selected_objects
) == 2:
1216 # 2 objects selected.
1217 # We measure the distance between the 2 selected objects.
1218 layout
.label(text
="Distance")
1220 obj1
, obj2
= context
.selected_objects
1224 row
.prop(sce
, "measure_panel_dist")
1227 row
.label(text
="", icon
='OBJECT_DATA')
1228 row
.prop(obj1
, "name", text
="")
1230 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1232 row
.label(text
="", icon
='OBJECT_DATA')
1233 row
.prop(obj2
, "name", text
="")
1235 layout
.prop(sce
, "measure_panel_draw")
1239 row
.prop(sce
, "measure_panel_calc_edge_length",
1242 if sce
.measure_panel_calc_edge_length
:
1243 if sce
.measure_panel_edge_length
>= 0:
1244 if len(mesh_objects
) > 0:
1248 row
.label(text
="Total edge length")
1249 row
.prop(sce
, "measure_panel_edge_length")
1254 row
.prop(sce
, "measure_panel_calc_area",
1255 text
="Surface area")
1257 if sce
.measure_panel_calc_area
:
1258 # Display surface area of the objects.
1259 if (sce
.measure_panel_area1
>= 0
1260 or sce
.measure_panel_area2
>= 0):
1261 if sce
.measure_panel_area1
>= 0:
1264 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1267 row
.label(text
="Area")
1268 row
.prop(sce
, "measure_panel_area1")
1271 row
.label(text
="Normal")
1273 row
.prop(sce
, "measure_panel_normal1")
1275 if sce
.measure_panel_area2
>= 0:
1278 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1281 row
.label(text
="Area")
1282 row
.prop(sce
, "measure_panel_area2")
1285 row
.label(text
="Normal")
1287 row
.prop(sce
, "measure_panel_normal2")
1291 row
.prop(sce
, "measure_panel_calc_volume",
1294 if sce
.measure_panel_calc_volume
:
1295 # Display volume of the objects.
1296 if sce
.measure_panel_volume1
>= -2:
1299 row
.label(text
=obj1
.name
, icon
='OBJECT_DATA')
1301 if sce
.measure_panel_volume1
>= 0:
1303 row
.label(text
="Volume")
1304 row
.prop(sce
, "measure_panel_volume1")
1305 elif sce
.measure_panel_volume1
>= -1:
1307 row
.label(text
="Mesh is non-manifold!",
1311 row
.label(text
="Mesh has n-gons (faces with " \
1312 "more than 4 edges)!",
1315 if sce
.measure_panel_volume2
>= -2:
1318 row
.label(text
=obj2
.name
, icon
='OBJECT_DATA')
1320 if sce
.measure_panel_volume2
>= 0:
1322 row
.label(text
="Volume")
1323 row
.prop(sce
, "measure_panel_volume2")
1324 elif sce
.measure_panel_volume2
>= -1:
1326 row
.label(text
="Mesh is non-manifold!",
1330 row
.label(text
="Mesh has n-gons (faces with " \
1331 "more than 4 edges)!",
1335 # One object selected.
1336 # We measure the distance from the object to the 3D cursor.
1337 layout
.label(text
="Distance")
1341 row
.prop(sce
, "measure_panel_dist")
1344 row
.label(text
="", icon
='CURSOR')
1346 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1348 row
.label(text
="", icon
='OBJECT_DATA')
1349 row
.prop(obj
, "name", text
="")
1351 layout
.prop(sce
, "measure_panel_draw")
1355 row
.prop(sce
, "measure_panel_calc_edge_length",
1358 if sce
.measure_panel_calc_edge_length
:
1359 if sce
.measure_panel_edge_length
>= 0:
1360 if len(mesh_objects
) > 0:
1364 row
.label(text
="Total edge length")
1365 row
.prop(sce
, "measure_panel_edge_length")
1369 row
.prop(sce
, "measure_panel_calc_area",
1370 text
="Surface area")
1372 if sce
.measure_panel_calc_area
:
1373 # Display surface area of the object.
1375 if sce
.measure_panel_area1
>= 0.0:
1378 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1381 row
.label(text
="Area")
1382 row
.prop(sce
, "measure_panel_area1")
1385 row
.label(text
="Normal")
1387 row
.prop(sce
, "measure_panel_normal1")
1391 row
.prop(sce
, "measure_panel_calc_volume",
1394 if sce
.measure_panel_calc_volume
:
1395 # Display volume of the objects.
1396 if sce
.measure_panel_volume1
>= -2:
1399 row
.label(text
=obj
.name
, icon
='OBJECT_DATA')
1401 if sce
.measure_panel_volume1
>= 0:
1403 row
.label(text
="Volume")
1404 row
.prop(sce
, "measure_panel_volume1")
1405 elif sce
.measure_panel_volume1
>= -1:
1407 row
.label(text
="Mesh is non-manifold!",
1411 row
.label(text
="Mesh has n-gons (faces with " \
1412 "more than 4 edges)!",
1415 elif not context
.selected_objects
:
1417 # We measure the distance from the origin to the 3D cursor.
1418 layout
.label(text
="Distance")
1422 row
.prop(sce
, "measure_panel_dist")
1425 row
.label(text
="", icon
='CURSOR')
1426 row
.label(text
="", icon
='ARROW_LEFTRIGHT')
1427 row
.label(text
="Origin [0,0,0]")
1429 layout
.prop(sce
, "measure_panel_draw")
1433 row
.label(text
="Selection not supported",
1436 if drawTansformButtons
:
1439 "measure_panel_transform",
1444 bpy
.utils
.register_module(__name__
)
1446 bpy
.app
.handlers
.scene_update_post
.append(scene_update
)
1448 # Define a temporary attribute for the distance value
1449 bpy
.types
.Scene
.measure_panel_dist
= bpy
.props
.FloatProperty(
1451 precision
=PRECISION
,
1453 bpy
.types
.Scene
.measure_panel_edge_length
= bpy
.props
.FloatProperty(
1455 precision
=PRECISION
,
1457 bpy
.types
.Scene
.measure_panel_area1
= bpy
.props
.FloatProperty(
1459 precision
=PRECISION
,
1461 bpy
.types
.Scene
.measure_panel_area2
= bpy
.props
.FloatProperty(
1463 precision
=PRECISION
,
1465 bpy
.types
.Scene
.measure_panel_normal1
= bpy
.props
.FloatVectorProperty(
1467 precision
=PRECISION
,
1469 bpy
.types
.Scene
.measure_panel_normal2
= bpy
.props
.FloatVectorProperty(
1471 precision
=PRECISION
,
1473 bpy
.types
.Scene
.measure_panel_volume1
= bpy
.props
.FloatProperty(
1475 precision
=PRECISION
,
1477 bpy
.types
.Scene
.measure_panel_volume2
= bpy
.props
.FloatProperty(
1479 precision
=PRECISION
,
1483 ("measure_global", "Global",
1484 "Calculate values in global space"),
1485 ("measure_local", "Local",
1486 "Calculate values inside the local object space")]
1488 # Define dropdown for the global/local setting
1489 bpy
.types
.Scene
.measure_panel_transform
= bpy
.props
.EnumProperty(
1491 description
="Choose in which space you want to measure",
1493 default
='measure_global')
1495 # Define property for the draw setting.
1496 bpy
.types
.Scene
.measure_panel_draw
= bpy
.props
.BoolProperty(
1497 name
="Draw distance",
1498 description
="Draw distances in 3D View",
1501 bpy
.types
.Scene
.measure_panel_calc_edge_length
= bpy
.props
.BoolProperty(
1502 description
="Calculate total length of (selected) edges",
1505 # Define property for the calc-area setting.
1506 # @todo prevent double calculations for each refresh automatically?
1507 bpy
.types
.Scene
.measure_panel_calc_area
= bpy
.props
.BoolProperty(
1508 description
="Calculate mesh surface area (heavy CPU "
1509 "usage on bigger meshes)",
1512 # Define property for the calc-volume setting.
1513 bpy
.types
.Scene
.measure_panel_calc_volume
= bpy
.props
.BoolProperty(
1514 description
="Calculate mesh volume (heavy CPU "
1515 "usage on bigger meshes)",
1518 # Define dropdown for the global/local setting
1519 bpy
.types
.Scene
.measure_panel_update
= bpy
.props
.BoolProperty(
1520 description
="Update CPU heavy calculations",
1527 bpy
.utils
.unregister_module(__name__
)
1529 bpy
.app
.handlers
.scene_update_post
.remove(scene_update
)
1531 # Remove properties.
1532 del bpy
.types
.Scene
.measure_panel_dist
1533 del bpy
.types
.Scene
.measure_panel_edge_length
1534 del bpy
.types
.Scene
.measure_panel_area1
1535 del bpy
.types
.Scene
.measure_panel_area2
1536 del bpy
.types
.Scene
.measure_panel_normal1
1537 del bpy
.types
.Scene
.measure_panel_normal2
1538 del bpy
.types
.Scene
.measure_panel_volume1
1539 del bpy
.types
.Scene
.measure_panel_volume2
1540 del bpy
.types
.Scene
.measure_panel_transform
1541 del bpy
.types
.Scene
.measure_panel_draw
1542 del bpy
.types
.Scene
.measure_panel_calc_edge_length
1543 del bpy
.types
.Scene
.measure_panel_calc_area
1544 del bpy
.types
.Scene
.measure_panel_calc_volume
1545 del bpy
.types
.Scene
.measure_panel_update
1549 if __name__
== "__main__":