1 # -*- coding: utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
25 "blender": (2, 78, 0),
26 "location": "View3D > Tool Shelf",
38 from bpy
.types
import (
43 from bpy
.props
import (
49 from bpy_extras
.view3d_utils
import (
50 region_2d_to_location_3d
,
51 location_3d_to_region_2d
,
53 from mathutils
import (
57 from math
import degrees
61 bpy
.ops
.object.mode_set(mode
='OBJECT')
65 bpy
.ops
.object.mode_set(mode
='EDIT')
68 def get_direction_(bme
, list_
, ob_act
):
71 p
= ob_act
.matrix_world
* (bme
.verts
[list_
[i
]].co
).copy()
72 p1
= ob_act
.matrix_world
* (bme
.verts
[list_
[(i
- 1) % n
]].co
).copy()
73 p2
= ob_act
.matrix_world
* (bme
.verts
[list_
[(i
+ 1) % n
]].co
).copy()
75 if p
== p1
or p
== p2
:
77 ang
= round(degrees((p
- p1
).angle((p
- p2
), any
)))
78 if ang
== 0 or ang
== 180:
80 elif ang
!= 0 or ang
!= 180:
81 return(((p
- p1
).cross((p
- p2
))).normalized())
85 def store_restore_view(context
, store
=True):
86 if not context
.scene
.pen_tool_props
.restore_view
:
90 # copy the original view_matrix and rotation for restoring
91 pt_buf
.store_view_matrix
= context
.space_data
.region_3d
.view_matrix
.copy()
92 pt_buf
.view_location
= context
.space_data
.region_3d
.view_location
.copy()
94 context
.space_data
.region_3d
.view_matrix
= pt_buf
.store_view_matrix
95 context
.space_data
.region_3d
.view_location
= pt_buf
.view_location
98 def align_view_to_face_(context
, bme
, f
):
99 store_restore_view(context
, True)
100 ob_act
= context
.active_object
101 list_e
= [[v
.index
for v
in e
.verts
] for e
in f
.edges
][0]
102 vec0
= -get_direction_(bme
, [v
.index
for v
in f
.verts
], ob_act
)
103 vec1
= ((ob_act
.matrix_world
* bme
.verts
[list_e
[0]].co
.copy()) -
104 (ob_act
.matrix_world
* bme
.verts
[list_e
[1]].co
.copy())).normalized()
105 vec2
= (vec0
.cross(vec1
)).normalized()
106 context
.space_data
.region_3d
.view_matrix
= ((Matrix((vec1
, vec2
, vec0
))).to_4x4()).inverted()
107 context
.space_data
.region_3d
.view_location
= f
.calc_center_median()
110 def draw_callback_px(self
, context
):
112 alpha
= context
.scene
.pen_tool_props
.a
113 font_size
= context
.scene
.pen_tool_props
.fs
115 bgl
.glColor4f(0.0, 0.6, 1.0, alpha
)
117 bgl
.glBegin(bgl
.GL_POINTS
)
118 bgl
.glVertex2f(pt_buf
.x
, pt_buf
.y
)
120 bgl
.glDisable(bgl
.GL_BLEND
)
123 if context
.scene
.pen_tool_props
.b2
is True:
124 mloc3d
= region_2d_to_location_3d(
126 context
.space_data
.region_3d
, Vector((pt_buf
.x
, pt_buf
.y
)),
127 pt_buf
.depth_location
129 blf
.position(font_id
, pt_buf
.x
+ 15, pt_buf
.y
- 15, 0)
130 blf
.size(font_id
, font_size
, context
.preferences
.system
.dpi
)
132 '(' + str(round(mloc3d
[0], 4)) + ', ' + str(round(mloc3d
[1], 4)) +
133 ', ' + str(round(mloc3d
[2], 4)) + ')')
135 n
= len(pt_buf
.list_m_loc_3d
)
139 bgl
.glEnable(bgl
.GL_BLEND
)
141 bgl
.glBegin(bgl
.GL_POINTS
)
142 for i
in pt_buf
.list_m_loc_3d
:
143 loc_0
= location_3d_to_region_2d(
144 context
.region
, context
.space_data
.region_3d
, i
146 bgl
.glVertex2f(loc_0
[0], loc_0
[1])
148 bgl
.glDisable(bgl
.GL_BLEND
)
150 # text next to the mouse
151 m_loc_3d
= region_2d_to_location_3d(
153 context
.space_data
.region_3d
, Vector((pt_buf
.x
, pt_buf
.y
)),
154 pt_buf
.depth_location
156 vec0
= pt_buf
.list_m_loc_3d
[-1] - m_loc_3d
157 blf
.position(font_id
, pt_buf
.x
+ 15, pt_buf
.y
+ 15, 0)
158 blf
.size(font_id
, font_size
, context
.preferences
.system
.dpi
)
159 blf
.draw(font_id
, str(round(vec0
.length
, 4)))
161 # angle first after mouse
163 vec1
= pt_buf
.list_m_loc_3d
[-2] - pt_buf
.list_m_loc_3d
[-1]
164 if vec0
.length
== 0.0 or vec1
.length
== 0.0:
167 ang
= vec0
.angle(vec1
)
169 if round(degrees(ang
), 2) == 180.0:
171 elif round(degrees(ang
), 2) == 0.0:
174 text_0
= str(round(degrees(ang
), 2))
176 loc_4
= location_3d_to_region_2d(
178 context
.space_data
.region_3d
,
179 pt_buf
.list_m_loc_3d
[-1]
181 bgl
.glColor4f(0.0, 1.0, 0.525, alpha
)
182 blf
.position(font_id
, loc_4
[0] + 10, loc_4
[1] + 10, 0)
183 blf
.size(font_id
, font_size
, context
.preferences
.system
.dpi
)
184 blf
.draw(font_id
, text_0
+ '')
186 bgl
.glLineStipple(4, 0x5555)
187 bgl
.glEnable(bgl
.GL_LINE_STIPPLE
) # enable line stipple
189 bgl
.glColor4f(0.0, 0.6, 1.0, alpha
)
190 # draw line between last point and mouse
191 bgl
.glEnable(bgl
.GL_BLEND
)
192 bgl
.glBegin(bgl
.GL_LINES
)
193 loc_1
= location_3d_to_region_2d(
195 context
.space_data
.region_3d
,
196 pt_buf
.list_m_loc_3d
[-1]
198 bgl
.glVertex2f(loc_1
[0], loc_1
[1])
199 bgl
.glVertex2f(pt_buf
.x
, pt_buf
.y
)
201 bgl
.glDisable(bgl
.GL_BLEND
)
203 # draw lines between points
204 bgl
.glEnable(bgl
.GL_BLEND
)
205 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
206 for j
in pt_buf
.list_m_loc_3d
:
207 loc_2
= location_3d_to_region_2d(context
.region
, context
.space_data
.region_3d
, j
)
208 bgl
.glVertex2f(loc_2
[0], loc_2
[1])
210 bgl
.glDisable(bgl
.GL_BLEND
)
212 bgl
.glDisable(bgl
.GL_LINE_STIPPLE
) # disable line stipple
214 # draw line length between points
215 if context
.scene
.pen_tool_props
.b1
is True:
216 for k
in range(n
- 1):
217 loc_3
= location_3d_to_region_2d(
218 context
.region
, context
.space_data
.region_3d
,
219 (pt_buf
.list_m_loc_3d
[k
] + pt_buf
.list_m_loc_3d
[(k
+ 1) % n
]) * 0.5
221 blf
.position(font_id
, loc_3
[0] + 10, loc_3
[1] + 10, 0)
222 blf
.size(font_id
, font_size
, context
.preferences
.system
.dpi
)
224 str(round((pt_buf
.list_m_loc_3d
[k
] - pt_buf
.list_m_loc_3d
[(k
+ 1) % n
]).length
, 4)))
227 if context
.scene
.pen_tool_props
.b0
is True:
228 for h
in range(n
- 1):
233 vec_
= pt_buf
.list_m_loc_3d
[h
] - pt_buf
.list_m_loc_3d
[(h
- 1) % n
]
234 vec_1_
= pt_buf
.list_m_loc_3d
[h
]
235 vec_2_
= pt_buf
.list_m_loc_3d
[(h
- 1) % n
]
236 if vec_
.length
== 0.0 or vec_1_
.length
== 0.0 or vec_2_
.length
== 0.0:
239 ang
= vec_
.angle(vec_1_
- vec_2_
)
240 if round(degrees(ang
)) == 0.0:
243 loc_4
= location_3d_to_region_2d(
244 context
.region
, context
.space_data
.region_3d
,
245 pt_buf
.list_m_loc_3d
[h
]
247 bgl
.glColor4f(0.0, 1.0, 0.525, alpha
)
248 blf
.position(font_id
, loc_4
[0] + 10, loc_4
[1] + 10, 0)
249 blf
.size(font_id
, font_size
, context
.preferences
.system
.dpi
)
250 blf
.draw(font_id
, str(round(degrees(ang
), 2)) + '')
252 bgl
.glColor4f(1.0, 1.0, 1.0, 1.0)
253 blf
.position(font_id
, self
.text_location
, 20, 0)
254 blf
.size(font_id
, 15, context
.preferences
.system
.dpi
)
255 blf
.draw(font_id
, "Draw On")
256 blf
.position(font_id
, self
.text_location
, 40, 0)
257 blf
.draw(font_id
, "Extrude On" if pt_buf
.ctrl
else "Extrude Off")
260 class pen_tool_properties(PropertyGroup
):
263 description
="Set Font Alpha",
271 description
="Set Font Size",
278 description
="Display All Angles on Drawn Edges",
283 description
="Display All Lengths of Drawn Edges",
287 name
="Mouse Location 3D",
288 description
="Display the location coordinates of the mouse cursor",
291 restore_view
: BoolProperty(
293 description
="After the tool has finished, is the Viewport restored\n"
294 "to it's previous state",
305 depth_location
= Vector((0.0, 0.0, 0.0))
309 store_view_matrix
= Matrix()
310 view_location
= (0.0, 0.0, 0.0)
313 # ------ Panel ------
314 class pen_tool_panel(Panel
):
315 bl_space_type
= "VIEW_3D"
316 bl_region_type
= "TOOLS"
317 bl_category
= "Tools"
318 bl_label
= "Pen Tool"
319 bl_context
= "mesh_edit"
320 bl_options
= {"DEFAULT_CLOSED"}
322 def draw(self
, context
):
324 pen_tool_props
= context
.scene
.pen_tool_props
326 if pt_buf
.sws
== "on":
327 layout
.active
= False
328 layout
.label(text
="Pen Tool Active", icon
="INFO")
330 col
= layout
.column(align
=True)
331 col
.label(text
="Font:")
332 col
.prop(pen_tool_props
, "fs", text
="Size", slider
=True)
333 col
.prop(pen_tool_props
, "a", text
="Alpha", slider
=True)
335 col
= layout
.column(align
=True)
336 col
.label(text
="Settings:")
337 col
.prop(pen_tool_props
, "b0", text
="Angles", toggle
=True)
338 col
.prop(pen_tool_props
, "b1", text
="Edge Length", toggle
=True)
339 col
.prop(pen_tool_props
, "b2", text
="Mouse Location 3D", toggle
=True)
340 col
.prop(pen_tool_props
, "restore_view", text
="Restore View", toggle
=True)
342 split
= layout
.split(0.80, align
=True)
343 split
.operator("pen_tool.operator", text
="Draw")
344 split
.operator("mesh.extra_tools_help",
345 icon
="LAYER_USED").help_ids
= "mesh_pen_tool"
349 class pen_tool_operator(Operator
):
350 bl_idname
= "pen_tool.operator"
351 bl_label
= "Pen Tool"
352 bl_options
= {"REGISTER", "UNDO", "INTERNAL"}
354 text_location
: IntProperty(
361 def poll(cls
, context
):
362 # do not run in object mode
363 return (context
.active_object
and context
.active_object
.type == 'MESH' and
364 context
.mode
== 'EDIT_MESH')
366 def execute(self
, context
):
368 ob_act
= context
.active_object
370 bme
.from_mesh(ob_act
.data
)
372 mtrx
= ob_act
.matrix_world
.inverted() # ob_act matrix world inverted
376 for i
in pt_buf
.list_m_loc_3d
:
377 bme
.verts
.new(mtrx
* i
)
378 bme
.verts
.index_update()
379 bme
.verts
.ensure_lookup_table()
380 list_
.append(bme
.verts
[-1])
384 for j
in range(n
- 1):
385 bme
.edges
.new((list_
[j
], list_
[(j
+ 1) % n
]))
386 bme
.edges
.index_update()
388 bme
.to_mesh(ob_act
.data
)
389 store_restore_view(context
, False)
392 pt_buf
.list_m_loc_2d
[:] = []
393 pt_buf
.list_m_loc_3d
[:] = []
394 pt_buf
.depth_location
= Vector((0.0, 0.0, 0.0))
395 pt_buf
.store_view_matrix
= Matrix()
396 pt_buf
.view_location
= (0.0, 0.0, 0.0)
399 context
.area
.tag_redraw()
402 def modal(self
, context
, event
):
403 context
.area
.tag_redraw()
405 # allow moving in the 3D View
407 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
408 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6',
409 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}:
410 return {'PASS_THROUGH'}
412 if event
.type in {'LEFT_ALT', 'RIGHT_ALT'}:
413 if event
.value
== 'PRESS':
415 if event
.value
== 'RELEASE':
417 return {'RUNNING_MODAL'}
419 elif event
.type in {'LEFT_CTRL', 'RIGHT_CTRL'}:
420 if event
.value
== 'PRESS':
421 pt_buf
.ctrl
= not pt_buf
.ctrl
422 return {'RUNNING_MODAL'}
424 elif event
.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}:
425 if event
.value
== 'PRESS':
427 if event
.value
== 'RELEASE':
429 return {'RUNNING_MODAL'}
431 elif event
.type == 'MOUSEMOVE':
432 if pt_buf
.list_m_loc_2d
!= []:
433 pt_buf_list_m_loc_3d_last_2d
= location_3d_to_region_2d(
435 context
.space_data
.region_3d
,
436 pt_buf
.list_m_loc_3d
[-1]
438 if pt_buf
.alt
is True:
439 pt_buf
.x
= pt_buf_list_m_loc_3d_last_2d
[0]
440 pt_buf
.y
= event
.mouse_region_y
441 elif pt_buf
.shift
is True:
442 pt_buf
.x
= event
.mouse_region_x
443 pt_buf
.y
= pt_buf_list_m_loc_3d_last_2d
[1]
445 pt_buf
.x
= event
.mouse_region_x
446 pt_buf
.y
= event
.mouse_region_y
448 pt_buf
.x
= event
.mouse_region_x
449 pt_buf
.y
= event
.mouse_region_y
451 elif event
.type == 'LEFTMOUSE':
452 if event
.value
== 'PRESS':
453 mouse_loc_2d
= Vector((pt_buf
.x
, pt_buf
.y
))
454 pt_buf
.list_m_loc_2d
.append(mouse_loc_2d
)
456 mouse_loc_3d
= region_2d_to_location_3d(
457 context
.region
, context
.space_data
.region_3d
,
458 mouse_loc_2d
, pt_buf
.depth_location
460 pt_buf
.list_m_loc_3d
.append(mouse_loc_3d
)
462 pt_buf
.depth_location
= pt_buf
.list_m_loc_3d
[-1] # <-- depth location
463 # run Extrude at cursor
466 bpy
.ops
.mesh
.dupli_extrude_cursor('INVOKE_DEFAULT', rotate_source
=False)
469 elif event
.value
== 'RELEASE':
471 elif event
.type == 'RIGHTMOUSE':
472 context
.space_data
.draw_handler_remove(self
._handle
_px
, 'WINDOW')
473 self
.execute(context
)
476 elif event
.type == 'ESC':
477 context
.space_data
.draw_handler_remove(self
._handle
_px
, 'WINDOW')
478 store_restore_view(context
, False)
479 pt_buf
.list_m_loc_2d
[:] = []
480 pt_buf
.list_m_loc_3d
[:] = []
481 pt_buf
.depth_location
= Vector((0.0, 0.0, 0.0))
483 pt_buf
.store_view_matrix
= Matrix()
484 pt_buf
.view_location
= (0.0, 0.0, 0.0)
488 # Return has to be modal or the tool can crash
489 # It's better to define PASS_THROUGH as the exception and not the default
490 return {'RUNNING_MODAL'}
492 def invoke(self
, context
, event
):
493 bme
= bmesh
.from_edit_mesh(context
.active_object
.data
)
494 list_f
= [f
for f
in bme
.faces
if f
.select
]
498 pt_buf
.depth_location
= f
.calc_center_median()
499 align_view_to_face_(context
, bme
, f
)
501 if context
.area
.type == 'VIEW_3D':
502 # pre-compute the text location (thanks to the Carver add-on)
503 self
.text_location
= 100
504 overlap
= context
.preferences
.system
.use_region_overlap
505 for region
in context
.area
.regions
:
506 if region
.type == "WINDOW":
507 self
.text_location
= region
.width
- 100
509 for region
in context
.area
.regions
:
510 # The Properties Region on the right is of UI type
511 if region
.type == "UI":
512 self
.text_location
= self
.text_location
- region
.width
514 if pt_buf
.sws
== 'on':
515 return {'RUNNING_MODAL'}
516 elif pt_buf
.sws
!= 'on':
517 context
.window_manager
.modal_handler_add(self
)
518 self
._handle
_px
= context
.space_data
.draw_handler_add(
521 'WINDOW', 'POST_PIXEL'
524 return {'RUNNING_MODAL'}
526 self
.report({'WARNING'}, "Pen Tool: Operation Cancelled. View3D not found")
538 # First, keymap identifiers (last bool is True for modal km).
539 (("3D View", "VIEW_3D", "WINDOW", False), (
540 # Then a tuple of keymap items, defined by a dict of kwargs
541 # for the km new func, and a tuple of tuples (name, val)
542 # for ops properties, if needing non-default values.
543 ({"idname": pen_tool_operator
.bl_idname
, "type": 'D', "value": 'PRESS', "ctrl": True},
551 bpy
.utils
.register_class(c
)
553 bpy
.types
.Scene
.pen_tool_props
= PointerProperty(type=pen_tool_properties
)
555 bpy_extras
.keyconfig_utils
.addon_keymap_register(bpy
.context
.window_manager
, KEYMAPS
)
559 bpy_extras
.keyconfig_utils
.addon_keymap_unregister(bpy
.context
.window_manager
, KEYMAPS
)
561 del bpy
.types
.Scene
.pen_tool_props
564 bpy
.utils
.unregister_class(c
)
567 if __name__
== "__main__":