Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / mesh_extra_tools / mesh_pen_tool.py
blob76d5e0aa6c60eee9b5828076b3d978b1e8f95a24
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 #####
21 bl_info = {
22 "name": "Pen Tool",
23 "author": "zmj100",
24 "version": (0, 3, 1),
25 "blender": (2, 78, 0),
26 "location": "View3D > Tool Shelf",
27 "description": "",
28 "warning": "",
29 "wiki_url": "",
30 "category": "Mesh",
33 import bpy
34 import bpy_extras
35 import blf
36 import bgl
37 import bmesh
38 from bpy.types import (
39 Operator,
40 PropertyGroup,
41 Panel
43 from bpy.props import (
44 FloatProperty,
45 IntProperty,
46 PointerProperty,
47 BoolProperty
49 from bpy_extras.view3d_utils import (
50 region_2d_to_location_3d,
51 location_3d_to_region_2d,
53 from mathutils import (
54 Vector,
55 Matrix,
57 from math import degrees
60 def edit_mode_out():
61 bpy.ops.object.mode_set(mode='OBJECT')
64 def edit_mode_in():
65 bpy.ops.object.mode_set(mode='EDIT')
68 def get_direction_(bme, list_, ob_act):
69 n = len(list_)
70 for i in range(n):
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:
76 continue
77 ang = round(degrees((p - p1).angle((p - p2), any)))
78 if ang == 0 or ang == 180:
79 continue
80 elif ang != 0 or ang != 180:
81 return(((p - p1).cross((p - p2))).normalized())
82 break
85 def store_restore_view(context, store=True):
86 if not context.scene.pen_tool_props.restore_view:
87 return
89 if store is True:
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()
93 else:
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):
111 font_id = 0
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)
116 bgl.glPointSize(4.0)
117 bgl.glBegin(bgl.GL_POINTS)
118 bgl.glVertex2f(pt_buf.x, pt_buf.y)
119 bgl.glEnd()
120 bgl.glDisable(bgl.GL_BLEND)
122 # location 3d
123 if context.scene.pen_tool_props.b2 is True:
124 mloc3d = region_2d_to_location_3d(
125 context.region,
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)
131 blf.draw(font_id,
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)
137 if n != 0:
138 # add points
139 bgl.glEnable(bgl.GL_BLEND)
140 bgl.glPointSize(4.0)
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])
147 bgl.glEnd()
148 bgl.glDisable(bgl.GL_BLEND)
150 # text next to the mouse
151 m_loc_3d = region_2d_to_location_3d(
152 context.region,
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
162 if n >= 2:
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:
165 pass
166 else:
167 ang = vec0.angle(vec1)
169 if round(degrees(ang), 2) == 180.0:
170 text_0 = '0.0'
171 elif round(degrees(ang), 2) == 0.0:
172 text_0 = '180.0'
173 else:
174 text_0 = str(round(degrees(ang), 2))
176 loc_4 = location_3d_to_region_2d(
177 context.region,
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(
194 context.region,
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)
200 bgl.glEnd()
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])
209 bgl.glEnd()
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)
223 blf.draw(font_id,
224 str(round((pt_buf.list_m_loc_3d[k] - pt_buf.list_m_loc_3d[(k + 1) % n]).length, 4)))
226 # draw all angles
227 if context.scene.pen_tool_props.b0 is True:
228 for h in range(n - 1):
229 if n >= 2:
230 if h == 0:
231 pass
232 else:
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:
237 pass
238 else:
239 ang = vec_.angle(vec_1_ - vec_2_)
240 if round(degrees(ang)) == 0.0:
241 pass
242 else:
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)) + '')
251 # tools on / off
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):
261 a: FloatProperty(
262 name="Alpha",
263 description="Set Font Alpha",
264 default=1.0,
265 min=0.1, max=1.0,
266 step=10,
267 precision=1
269 fs: IntProperty(
270 name="Size",
271 description="Set Font Size",
272 default=14,
273 min=12, max=40,
274 step=1
276 b0: BoolProperty(
277 name="Angles",
278 description="Display All Angles on Drawn Edges",
279 default=False
281 b1: BoolProperty(
282 name="Edge Length",
283 description="Display All Lengths of Drawn Edges",
284 default=False
286 b2: BoolProperty(
287 name="Mouse Location 3D",
288 description="Display the location coordinates of the mouse cursor",
289 default=False
291 restore_view: BoolProperty(
292 name="Restore View",
293 description="After the tool has finished, is the Viewport restored\n"
294 "to it's previous state",
295 default=True
299 class pt_buf():
300 list_m_loc_2d = []
301 list_m_loc_3d = []
302 x = 0
303 y = 0
304 sws = 'off'
305 depth_location = Vector((0.0, 0.0, 0.0))
306 alt = False
307 shift = False
308 ctrl = False
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):
323 layout = self.layout
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")
329 else:
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"
348 # Operator
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(
355 name="",
356 default=0,
357 options={'HIDDEN'}
360 @classmethod
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):
367 edit_mode_out()
368 ob_act = context.active_object
369 bme = bmesh.new()
370 bme.from_mesh(ob_act.data)
372 mtrx = ob_act.matrix_world.inverted() # ob_act matrix world inverted
374 # add vertices
375 list_ = []
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])
382 # add edges
383 n = len(list_)
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)
390 edit_mode_in()
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)
397 pt_buf.ctrl = False
399 context.area.tag_redraw()
400 return {'FINISHED'}
402 def modal(self, context, event):
403 context.area.tag_redraw()
405 # allow moving in the 3D View
406 if event.type in {
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':
414 pt_buf.alt = True
415 if event.value == 'RELEASE':
416 pt_buf.alt = False
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':
426 pt_buf.shift = True
427 if event.value == 'RELEASE':
428 pt_buf.shift = False
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(
434 context.region,
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]
444 else:
445 pt_buf.x = event.mouse_region_x
446 pt_buf.y = event.mouse_region_y
447 else:
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
464 if pt_buf.ctrl:
465 try:
466 bpy.ops.mesh.dupli_extrude_cursor('INVOKE_DEFAULT', rotate_source=False)
467 except:
468 pass
469 elif event.value == 'RELEASE':
470 pass
471 elif event.type == 'RIGHTMOUSE':
472 context.space_data.draw_handler_remove(self._handle_px, 'WINDOW')
473 self.execute(context)
474 pt_buf.sws = 'off'
475 return {'FINISHED'}
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))
482 pt_buf.sws = 'off'
483 pt_buf.store_view_matrix = Matrix()
484 pt_buf.view_location = (0.0, 0.0, 0.0)
485 pt_buf.ctrl = False
486 return {'CANCELLED'}
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]
496 if len(list_f) != 0:
497 f = list_f[0]
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
508 if overlap:
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(
519 draw_callback_px,
520 (self, context),
521 'WINDOW', 'POST_PIXEL'
523 pt_buf.sws = 'on'
524 return {'RUNNING_MODAL'}
525 else:
526 self.report({'WARNING'}, "Pen Tool: Operation Cancelled. View3D not found")
527 return {'CANCELLED'}
530 class_list = (
531 pen_tool_panel,
532 pen_tool_operator,
533 pen_tool_properties
537 KEYMAPS = (
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},
544 ()),
549 def register():
550 for c in class_list:
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)
558 def unregister():
559 bpy_extras.keyconfig_utils.addon_keymap_unregister(bpy.context.window_manager, KEYMAPS)
561 del bpy.types.Scene.pen_tool_props
563 for c in class_list:
564 bpy.utils.unregister_class(c)
567 if __name__ == "__main__":
568 register()