Update for API change: scene.cursor_location -> scene.cursor.location
[blender-addons-contrib.git] / np_station / np_point_scale.py
1
2 # ##### BEGIN GPL LICENSE BLOCK #####
3 #
4 #  This program is free software; you can redistribute it and/or
5 #  modify it under the terms of the GNU General Public License
6 #  as published by the Free Software Foundation; either version 2
7 #  of the License, or (at your option) any later version.
8 #
9 #  This program is distributed in the hope that it will be useful,
10 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #  GNU General Public License for more details.
13 #
14 #  You should have received a copy of the GNU General Public License
15 #  along with this program; if not, write to the Free Software Foundation,
16 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 #
18 # ##### END GPL LICENSE BLOCK #####
19
20
21 '''
22 DESCRIPTION
23
24 Scales and snap-stretches one or more objects with one end fixed. It's main advantage is stretching with precision. I am used of working with similar commands a lot, during my architecture design processes. Good for construction beams, openings, table-tops, carpets and other cubic shapes. Besides that, it doesn't require object origin and 3D cursor manipulation.
25
26
27 INSTALLATION
28
29 Unzip and place .py file to scripts / addons_contrib folder. In User Preferences / Addons tab, search with Testing filter - NP Point Scale and check the box.
30 Now you have the operator in your system. If you press Save User Preferences, you will have it at your disposal every time you run Blender.
31
32
33 SHORTCUTS
34
35 After successful installation of the addon, the NP Point Scale operator should be registered in your system. Enter User Preferences / Input, and under that, 3DView / Object mode. At the bottom of the list click the 'Add new' button. In the operator field type object.np_point_scale_xxx (xxx being the number of the version) and assign a shortcut key of your preference. I suggest assigning S, replacing the standard scale command since it is incorporated in this operator. Assign it only for the Object Mode because the addon doesn't work in other modes. This way the basic S command should be available in all other editing environments and you can copy it back to Object mode if needed. Note, this operator is still a prototype.
36
37
38 USAGE
39
40 Select one or more objects.
41 Run operator (spacebar search - NP Cage Scale, or keystroke if you assigned it)
42 Pick-snap one of the points on the mode-cage.
43 Scale or stretch with CTRL-snap
44
45 You can run the operator with spacebar search - NP Point Copy, or shortcut key if you assigned it.
46 Select a point anywhere in the scene (holding CTRL enables snapping). This will be your 'take' point.
47 Move your mouse and click to a point anywhere in the scene with the left mouse button (LMB), in relation to the 'take' point and the operator will duplicate the selected objects at that position (again CTRL - snap enables snapping to objects around the scene). You can continue duplicating objects in the same way. When you want to finish the process, press ESC or RMB. If you want to make an array of the copied objects in relation to the last pair, press the 'ENTER' button (ENT). The command will automatically read the direction and the distance between the last pair of copied objects and present an interface to specify the number of arrayed copies. You specify the number with CTRL + mouse scroll, with the possibility to go below the amount of 2 which changes the mode of array to division.  You confirm the array with RMB / ENTER / TAB key or cancel it with ESC. Pressing RMB at the end will confirm the array and keep it as a modifier in the modifier stack, ENTER will apply the modifier as a single object and remove the modifier, while TAB would apply the modifier as an array of separate individual objects and remove the modifier from the modifier stack.
48 If at any point you lose sight of the next point you want to snap to, you can press SPACE to go to NAVIGATION mode in which you can change the point of view. When your next point is clearly in your field of view, you return to normal mode by pressing SPACE again or LMB.
49 Middle mouse button (MMB) enables axis constraint during snapping, while numpad keys enable numerical input for the copy distance.
50
51
52 ADDON SETTINGS:
53
54 Below the addon name in the user preferences / addon tab, you can find a couple of settings that control the behavior of the addon:
55
56 Unit scale: Distance multiplier for various unit scenarios
57 Suffix: Unit abbreviation after the numerical distance
58 Custom colors: Default or custom colors for graphical elements
59 Mouse badge: Option to display a small cursor label
60
61
62 IMPORTANT PERFORMANCE NOTES
63
64 All origins of objects participating are centered to their geometry after scaling
65 In flipping, script goes through edit mode of objects
66 Scale and rotate are applied to all objects participating
67
68
69 WISH LIST
70
71 Ability to include lattices and other non-mesh types in the transformation
72 Ability to adjust the rotation of the cage to better suite the geometry of scaled object
73 Bgl overlay for mode points and eventually the mode cage
74 Blf instructions on screen, preferably interactive
75 Cleaner code and faster performance
76
77
78 WARNINGS
79
80 The addon is still in beta and is probably not immune to heavy geometry and has not been tested in all scenarios, so avoid production use...
81 '''
82
83 bl_info = {
84     'name': 'NP 020 Point Scale',
85     'author': 'Okavango & the Blenderartists community',
86     'version': (0, 2, 0),
87     'blender': (2, 75, 0),
88     'location': 'View3D',
89     'warning': '',
90     'description': 'Scales selected objects using bounding box with handles',
91     'wiki_url': '',
92     'category': '3D View'}
93
94 import bpy
95 import copy
96 #import math
97 import mathutils
98 import bgl
99 import blf
100 from bpy_extras import view3d_utils
101 from bpy.app.handlers import persistent
102
103 from .utils_geometry import *
104 from .utils_graphics import *
105 from .utils_function import *
106
107 # Defining the main class - the macro:
108
109 class NP020PointScale(bpy.types.Macro):
110     bl_idname = 'object.np_020_point_scale'
111     bl_label = 'NP 020 Point Scale'
112     bl_options = {'UNDO'}
113
114
115 # Defining the storage class that will serve as a variable bank for exchange among the classes. Later, this bank will receive more variables with their values for safe keeping, as the program goes on:
116
117 class NP020PS:
118
119     flag = 'DISPLAY'
120
121
122 # Defining the scene update algorithm that will track the state of the objects during modal transforms, which is otherwise impossible:
123
124 @persistent
125 def NPPS_scene_update(context):
126
127     if bpy.data.objects.is_updated:
128         a = 1
129
130
131 # Defining the first of the classes from the macro, that will gather the current system settings set by the user. Some of the system settings will be changed during the process, and will be restored when macro has completed.
132
133 class NPPSGetContext(bpy.types.Operator):
134     bl_idname = 'object.np_ps_get_context'
135     bl_label = 'NP PS Get Context'
136     bl_options = {'INTERNAL'}
137
138     def execute(self, context):
139         if bpy.context.selected_objects == []:
140             self.report({'WARNING'}, "Please select objects first")
141             return {'CANCELLED'}
142         NP020PS.use_snap = copy.deepcopy(bpy.context.tool_settings.use_snap)
143         NP020PS.snap_element = copy.deepcopy(bpy.context.tool_settings.snap_element)
144         NP020PS.snap_target = copy.deepcopy(bpy.context.tool_settings.snap_target)
145         NP020PS.pivot_point = copy.deepcopy(bpy.context.space_data.pivot_point)
146         NP020PS.trans_orient = copy.deepcopy(bpy.context.space_data.transform_orientation)
147         NP020PS.curloc = copy.deepcopy(bpy.context.scene.cursor.location)
148         NP020PS.acob = bpy.context.active_object
149         if bpy.context.mode == 'OBJECT':
150             NP020PS.edit_mode = 'OBJECT'
151         elif bpy.context.mode in ('EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_TEXT', 'EDIT_ARMATURE', 'EDIT_METABALL', 'EDIT_LATTICE'):
152             NP020PS.edit_mode = 'EDIT'
153         elif bpy.context.mode == 'POSE':
154             NP020PS.edit_mode = 'POSE'
155         elif bpy.context.mode == 'SCULPT':
156             NP020PS.edit_mode = 'SCULPT'
157         elif bpy.context.mode == 'PAINT_WEIGHT':
158             NP020PS.edit_mode = 'WEIGHT_PAINT'
159         elif bpy.context.mode == 'PAINT_TEXTURE':
160             NP020PS.edit_mode = 'TEXTURE_PAINT'
161         elif bpy.context.mode == 'PAINT_VERTEX':
162             NP020PS.edit_mode = 'VERTEX_PAINT'
163         elif bpy.context.mode == 'PARTICLE':
164             NP020PS.edit_mode = 'PARTICLE_EDIT'
165         #in case the proces is interrupted from outside warning on multiple users and storage is stuck with old flags
166         NP020PS.flag = 'DISPLAY'
167         NP020PS.flag_con = False
168         NP020PS.flag_cenpivot = False
169         NP020PS.flag_lev = False
170         NP020PS.flag_force = False
171         NP020PS.mode = None
172         return {'FINISHED'}
173
174
175 # Defining the class that will gather the list of selected objects and acquire the exact dimensions and the location of the selection - it reads the selection and scans bounding-box dimensions of all the objects included, searching for the farthest geometry in all directions. In this way it acquires the dimensions of the selection. The location of the selected group is calculated as the middle point afterwards:
176
177 class NPPSGetSelection(bpy.types.Operator):
178     bl_idname = 'object.np_ps_get_selection'
179     bl_label = 'NP PS Get Selection'
180     bl_options = {'INTERNAL'}
181
182     def execute(self, context):
183         if bpy.context.mode not in ('OBJECT'):
184             bpy.ops.object.mode_set(mode = 'OBJECT')
185         selob = bpy.context.selected_objects
186         NP020PS.selob = selob
187
188         minx, miny, minz = (999999.0,)*3
189         maxx, maxy, maxz = (-999999.0,)*3
190         for ob in selob:
191             for v in ob.bound_box:
192                 v_world = ob.matrix_world * mathutils.Vector((v[0],v[1],v[2]))
193
194                 if v_world[0] < minx:
195                     minx = v_world[0]
196                 if v_world[0] > maxx:
197                     maxx = v_world[0]
198
199                 if v_world[1] < miny:
200                     miny = v_world[1]
201                 if v_world[1] > maxy:
202                     maxy = v_world[1]
203
204                 if v_world[2] < minz:
205                     minz = v_world[2]
206                 if v_world[2] > maxz:
207                     maxz = v_world[2]
208
209         cage3d = {}
210         cage3d[0] = (minx, miny, minz)
211         cage3d[1] = (maxx, miny, minz)
212         cage3d[2] = (maxx, maxy, minz)
213         cage3d[3] = (minx, maxy, minz)
214         cage3d[4] = (minx, miny, maxz)
215         cage3d[5] = (maxx, miny, maxz)
216         cage3d[6] = (maxx, maxy, maxz)
217         cage3d[7] = (minx, maxy, maxz)
218         cage3d[10] = ((minx+maxx)/2, miny, minz)
219         cage3d[12] = (maxx, (miny+maxy)/2, minz)
220         cage3d[23] = ((minx+maxx)/2, maxy, minz)
221         cage3d[30] = (minx, (miny+maxy)/2, minz)
222         cage3d[40] = (minx, miny, (minz+maxz)/2)
223         cage3d[15] = (maxx, miny, (minz+maxz)/2)
224         cage3d[26] = (maxx, maxy, (minz+maxz)/2)
225         cage3d[37] = (minx, maxy, (minz+maxz)/2)
226         cage3d[45] = ((minx+maxx)/2, miny, maxz)
227         cage3d[56] = (maxx, (miny+maxy)/2, maxz)
228         cage3d[67] = ((minx+maxx)/2, maxy, maxz)
229         cage3d[47] = (minx, (miny+maxy)/2, maxz)
230         cross3d = {}
231         cross3d['xmin'] = (minx, (miny + maxy)/2, (minz + maxz)/2)
232         cross3d['xmax'] = (maxx, (miny + maxy)/2, (minz + maxz)/2)
233         cross3d['ymin'] = ((minx + maxx)/2, miny, (minz + maxz)/2)
234         cross3d['ymax'] = ((minx + maxx)/2, maxy, (minz + maxz)/2)
235         cross3d['zmin'] = ((minx + maxx)/2, (miny + maxy)/2, minz)
236         cross3d['zmax'] = ((minx + maxx)/2, (miny + maxy)/2, maxz)
237         c3d = ((minx + maxx)/2, (miny + maxy)/2, (minz + maxz)/2)
238         NP020PS.cage3d = cage3d
239         NP020PS.cross3d = cross3d
240         NP020PS.c3d = c3d
241
242         '''
243         NP020PS.minx = minx
244         NP020PS.maxx = maxx
245         NP020PS.miny = miny
246         NP020PS.maxy = maxy
247         NP020PS.minz = minz
248         NP020PS.maxz = maxz
249         '''
250         return {'FINISHED'}
251
252
253 # Defining the operator that will read the user input made during display cage phase and set the next operator accordingly:
254
255 class NPPSPrepareContext(bpy.types.Operator):
256     bl_idname = 'object.np_ps_prepare_context'
257     bl_label = 'NP PS Prepare Context'
258     bl_options = {'INTERNAL'}
259
260     def execute(self, context):
261
262         flag = NP020PS.flag
263         NP020PS.trans_custom = False  #..........................................
264         np_print('prepare, NP020PS.flag = ', flag)
265
266         if flag == 'DISPLAY':
267             bpy.ops.object.select_all(action = 'DESELECT')
268
269         elif flag == 'RUNRESIZE' :
270             selob = NP020PS.selob
271             mode = NP020PS.mode
272             flag_con = NP020PS.flag_con
273             flag_cenpivot = NP020PS.flag_cenpivot
274             flag_lev = NP020PS.flag_lev
275             flag_force = NP020PS.flag_force
276             cage3d = NP020PS.cage3d
277             cross3d = NP020PS.cross3d
278             c3d = NP020PS.c3d
279             curloc = NP020PS.curloc
280             bpy.context.tool_settings.use_snap = False
281             bpy.context.tool_settings.snap_element = 'VERTEX'
282             bpy.context.tool_settings.snap_target = 'ACTIVE'
283             bpy.context.space_data.pivot_point = 'CURSOR'
284             bpy.context.space_data.transform_orientation = 'GLOBAL'
285             for ob in selob:
286                 ob.select_set(True)
287                 bpy.context.view_layer.objects.active = ob
288             axis = (False, False, False)
289             if mode == 0:
290                 curloc = cage3d[6]
291             elif mode == 1:
292                 curloc = cage3d[7]
293             elif mode == 2:
294                 curloc = cage3d[4]
295             elif mode == 3:
296                 curloc = cage3d[5]
297             elif mode == 4:
298                 curloc = cage3d[2]
299             elif mode == 5:
300                 curloc = cage3d[3]
301             elif mode == 6:
302                 curloc = cage3d[0]
303             elif mode == 7:
304                 curloc = cage3d[1]
305             elif mode == 'xmin':
306                 curloc = cross3d['xmax']
307                 axis = (True, False, False)
308             elif mode == 'xmax':
309                 curloc = cross3d['xmin']
310                 axis = (True, False, False)
311             elif mode == 'ymin':
312                 curloc = cross3d['ymax']
313                 axis = (False, True, False)
314             elif mode == 'ymax':
315                 curloc = cross3d['ymin']
316                 axis = (False, True, False)
317             elif mode == 'zmin':
318                 curloc = cross3d['zmax']
319                 axis = (False, False, True)
320             elif mode == 'zmax':
321                 curloc = cross3d['zmin']
322                 axis = (False, False, True)
323             if flag_con: axis = (False, False, False)
324             if flag_cenpivot: curloc = c3d
325             if flag_force: bpy.ops.object.transform_apply(location=False,rotation=True,scale=True)
326             bpy.context.scene.cursor.location = curloc
327             NP020PS.axis = axis
328
329         return{'FINISHED'}
330
331 # Defining the class for adding a representation of the scale cage to the viewport. By interacting with graphical elements of the cage, the user picks the type of scaling he wants to be performed:
332
333 class NPPSDisplayCage(bpy.types.Operator):
334     bl_idname = 'object.np_ps_display_cage'
335     bl_label = 'NP PS Display Cage'
336     bl_options = {'INTERNAL'}
337
338     def modal(self, context, event):
339         context.area.tag_redraw()
340         flag = NP020PS.flag
341
342         if not event.ctrl and not event.shift and not event.alt and event.type == 'MOUSEMOVE':
343             self.mode = None
344             self.flag_con = False
345             self.flag_cenpivot = False
346             self.flag_lev = False
347             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
348
349         if event.ctrl and event.type == 'MOUSEMOVE':
350             self.mode = None
351             self.flag_con = True
352             self.flag_cenpivot = False
353             self.flag_lev = False
354             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
355
356         if event.shift and event.type == 'MOUSEMOVE':
357             self.mode = None
358             self.flag_con = False
359             self.flag_cenpivot = True
360             self.flag_lev = False
361             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
362
363         if event.alt and event.type == 'MOUSEMOVE':
364             self.mode = None
365             self.flag_con = False
366             self.flag_cenpivot = False
367             self.flag_lev = True
368             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
369
370         if event.ctrl and event.shift and event.type == 'MOUSEMOVE':
371             self.mode = None
372             self.flag_con = True
373             self.flag_cenpivot = True
374             self.flag_lev = False
375             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
376
377         if event.ctrl and event.alt and event.type == 'MOUSEMOVE':
378             self.mode = None
379             self.flag_con = True
380             self.flag_cenpivot = False
381             self.flag_lev = True
382             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
383
384         if event.shift and event.alt and event.type == 'MOUSEMOVE':
385             self.mode = None
386             self.flag_con = False
387             self.flag_cenpivot = True
388             self.flag_lev = True
389             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
390
391         if event.ctrl and event.shift and event.alt and event.type == 'MOUSEMOVE':
392             self.mode = None
393             self.flag_con = True
394             self.flag_cenpivot = True
395             self.flag_lev = True
396             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
397
398
399         if event.type == 'LEFT_CTRL' and event.value == 'PRESS':
400             self.flag_con = True
401             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
402
403         if event.type == 'LEFT_CTRL' and event.value == 'RELEASE':
404             self.flag_con = False
405             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
406
407         if event.type == 'LEFT_SHIFT' and event.value == 'PRESS':
408             self.flag_cenpivot = True
409             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
410
411         if event.type == 'LEFT_SHIFT' and event.value == 'RELEASE':
412             self.flag_cenpivot = False
413             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
414
415         if event.type == 'LEFT_ALT' and event.value == 'PRESS':
416             self.flag_lev = True
417             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
418
419         if event.type == 'LEFT_ALT' and event.value == 'RELEASE':
420             self.flag_lev = False
421             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
422
423         if event.type == 'LEFT_CTRL' and event.type == 'LEFT_SHIFT' and event.value == 'PRESS':
424             self.flag_con = True
425             self.flag_cenpivot = True
426             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
427
428         if event.type == 'LEFT_CTRL' and event.type == 'LEFT_SHIFT' and event.value == 'RELEASE':
429             self.flag_con = False
430             self.flag_cenpivot = False
431             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
432
433         if event.type == 'LEFT_ALT' and event.type == 'LEFT_SHIFT' and event.value == 'PRESS':
434             self.flag_lev = True
435             self.flag_cenpivot = True
436             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
437
438         if event.type == 'LEFT_ALT' and event.type == 'LEFT_SHIFT' and event.value == 'RELEASE':
439             self.flag_lev = False
440             self.flag_cenpivot = False
441             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
442
443         if event.type == 'LEFT_CTRL' and event.type == 'LEFT_ALT' and event.value == 'PRESS':
444             self.flag_con = True
445             self.flag_lev = True
446             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
447
448         if event.type == 'LEFT_CTRL' and event.type == 'LEFT_ALT' and event.value == 'RELEASE':
449             self.flag_con = False
450             self.flag_lev = False
451             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
452
453         if event.type == 'TAB' and event.value == 'PRESS':
454             if self.flag_force == True:
455                 self.flag_force = False
456             else:
457                 self.flag_force = True
458
459         if event.type in ('LEFTMOUSE', 'RIGHTMOUSE', 'RET', 'NUMPAD_ENTER') and event.value == 'PRESS':
460             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
461             NP020PS.flag_con = self.flag_con
462             NP020PS.flag_cenpivot = self.flag_cenpivot
463             NP020PS.flag_lev = self.flag_lev
464             NP020PS.flag_force = self.flag_force
465             NP020PS.mode = self.mode
466             NP020PS.flag = 'RUNRESIZE'
467             return {'FINISHED'}
468
469         if event.type == 'ESC':
470             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
471             NP020PS.flag = 'EXIT'
472             return {'FINISHED'}
473
474         if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
475             return {'PASS_THROUGH'}
476
477         return {'RUNNING_MODAL'}
478
479     def invoke(self, context, event):
480         if context.area.type == 'VIEW_3D':
481             args = (self, context)
482             self._handle = bpy.types.SpaceView3D.draw_handler_add(DRAW_DisplayCage, args, 'WINDOW', 'POST_PIXEL')
483             context.window_manager.modal_handler_add(self)
484             self.co2d = ((event.mouse_region_x, event.mouse_region_y))
485             self.flag_con = False
486             self.flag_cenpivot = False
487             self.flag_lev = False
488             self.flag_force = False
489             self.mode = None
490             return {'RUNNING_MODAL'}
491         else:
492             self.report({'WARNING'}, "View3D not found, cannot run operator")
493             return {'CANCELLED'}
494
495
496 # Defining the set of instructions that will draw the graphical OpenGL elements on the screen during the execution of DisplayCage operator:
497
498 def DRAW_DisplayCage(self, context):
499
500     flag = NP020PS.flag
501     flag = 'DISPLAY'
502     cage3d = NP020PS.cage3d
503     cross3d = NP020PS.cross3d
504     c3d = NP020PS.c3d
505
506     region = context.region
507     rv3d = context.region_data
508     cage2d = copy.deepcopy(cage3d)
509     cross2d = copy.deepcopy(cross3d)
510
511     for co in cage3d.items():
512         #np_print(co[0])
513         cage2d[co[0]] = view3d_utils.location_3d_to_region_2d(region, rv3d, co[1])
514     for co in cross3d.items():
515         cross2d[co[0]] = view3d_utils.location_3d_to_region_2d(region, rv3d, co[1])
516     c2d = view3d_utils.location_3d_to_region_2d(region, rv3d, c3d)
517
518     points = copy.deepcopy(cage2d)
519     points.update(cross2d)
520
521
522
523     # DRAWING START:
524     bgl.glEnable(bgl.GL_BLEND)
525
526     if flag == 'DISPLAY':
527         instruct = 'select a handle'
528         keys_aff = 'LMB - confirm, CTRL - force proportional, SHIFT - force central, TAB - apply scale / rotation'
529         keys_nav = ''
530         keys_neg = 'ESC - quit'
531
532     if self.flag_con:
533         mark_col = (1.0, 0.5, 0.0, 1.0)
534     else:
535         mark_col = (0.3, 0.6, 1.0, 1.0)
536
537
538     # ON-SCREEN INSTRUCTIONS:
539
540     display_instructions(region, rv3d, instruct, keys_aff, keys_nav, keys_neg)
541
542
543     sensor = 60
544     self.mode = None
545     co2d = mathutils.Vector(self.co2d)
546     distmin = mathutils.Vector(co2d - c2d).length
547
548     # Hover markers - detection
549     for co in points.items():
550         dist = mathutils.Vector(co2d - co[1]).length
551         if dist < distmin:
552             distmin = dist
553             if distmin < sensor:
554                 self.mode = co[0]
555                 self.hoverco = co[1]
556
557     # Drawing the graphical representation of scale cage, calculated from selection's bound box:
558     if self.mode == None:
559         bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
560         bgl.glLineWidth(1.4)
561     else:
562         bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
563         bgl.glLineWidth(1.0)
564     bgl.glBegin(bgl.GL_LINE_STRIP)
565     bgl.glVertex2f(*cage2d[0])
566     bgl.glVertex2f(*cage2d[1])
567     bgl.glVertex2f(*cage2d[2])
568     bgl.glVertex2f(*cage2d[3])
569     bgl.glVertex2f(*cage2d[0])
570     bgl.glEnd()
571     bgl.glBegin(bgl.GL_LINE_STRIP)
572     bgl.glVertex2f(*cage2d[4])
573     bgl.glVertex2f(*cage2d[5])
574     bgl.glVertex2f(*cage2d[6])
575     bgl.glVertex2f(*cage2d[7])
576     bgl.glVertex2f(*cage2d[4])
577     bgl.glEnd()
578     bgl.glBegin(bgl.GL_LINE_STRIP)
579     bgl.glVertex2f(*cage2d[0])
580     bgl.glVertex2f(*cage2d[4])
581     bgl.glEnd()
582     bgl.glBegin(bgl.GL_LINE_STRIP)
583     bgl.glVertex2f(*cage2d[1])
584     bgl.glVertex2f(*cage2d[5])
585     bgl.glEnd()
586     bgl.glBegin(bgl.GL_LINE_STRIP)
587     bgl.glVertex2f(*cage2d[2])
588     bgl.glVertex2f(*cage2d[6])
589     bgl.glEnd()
590     bgl.glBegin(bgl.GL_LINE_STRIP)
591     bgl.glVertex2f(*cage2d[3])
592     bgl.glVertex2f(*cage2d[7])
593     bgl.glEnd()
594     bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
595     bgl.glLineWidth(1.0)
596     bgl.glBegin(bgl.GL_LINE_STRIP)
597     bgl.glVertex2f(*cross2d['xmin'])
598     bgl.glVertex2f(*cross2d['xmax'])
599     bgl.glEnd()
600     bgl.glBegin(bgl.GL_LINE_STRIP)
601     bgl.glVertex2f(*cross2d['ymin'])
602     bgl.glVertex2f(*cross2d['ymax'])
603     bgl.glEnd()
604     bgl.glBegin(bgl.GL_LINE_STRIP)
605     bgl.glVertex2f(*cross2d['zmin'])
606     bgl.glVertex2f(*cross2d['zmax'])
607     bgl.glEnd()
608     #bgl.glDepthRange(0,0)
609     bgl.glColor4f(0.35, 0.65, 1.0, 1.0)
610     bgl.glPointSize(9)
611     bgl.glBegin(bgl.GL_POINTS)
612     for [a,b] in cross2d.values():
613        bgl.glVertex2f(a,b)
614     bgl.glEnd()
615     bgl.glEnable(bgl.GL_POINT_SMOOTH)
616     bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
617     bgl.glPointSize(11)
618     bgl.glBegin(bgl.GL_POINTS)
619     for co in cage2d.items():
620        if len(str(co[0])) == 1:
621            bgl.glVertex2f(*co[1])
622     bgl.glEnd()
623     bgl.glDisable(bgl.GL_POINT_SMOOTH)
624
625     markers = {}
626     markers[0] = (points[0], points[10], points[40], points[0], points[30], points[40], points[0], points[10], points[30])
627     markers[1] = (points[1], points[10], points[15], points[1], points[12], points[15], points[1], points[10], points[12])
628     markers[2] = (points[2], points[12], points[26], points[2], points[23], points[26], points[2], points[12], points[23])
629     markers[3] = (points[3], points[30], points[37], points[3], points[23], points[37], points[3], points[30], points[23])
630     markers[4] = (points[4], points[45], points[47], points[4], points[40], points[47], points[4], points[40], points[45])
631     markers[5] = (points[5], points[45], points[56], points[5], points[15], points[56], points[5], points[15], points[45])
632     markers[6] = (points[6], points[26], points[67], points[6], points[56], points[67], points[6], points[26], points[56])
633     markers[7] = (points[7], points[37], points[67], points[7], points[47], points[67], points[7], points[37], points[47])
634     markers['xmin'] = (points[0], points[3], points[7], points[4], points[0])
635     markers['xmax'] = (points[1], points[2], points[6], points[5], points[1])
636     markers['ymin'] = (points[0], points[1], points[5], points[4], points[0])
637     markers['ymax'] = (points[2], points[3], points[7], points[6], points[2])
638     markers['zmin'] = (points[0], points[1], points[2], points[3], points[0])
639     markers['zmax'] = (points[4], points[5], points[6], points[7], points[4])
640
641     pivot = {}
642     if self.flag_cenpivot:
643         pivot[0] = c2d
644         pivot[1] = c2d
645         pivot[2] = c2d
646         pivot[3] = c2d
647         pivot[4] = c2d
648         pivot[5] = c2d
649         pivot[6] = c2d
650         pivot[7] = c2d
651         pivot['xmin'] = c2d
652         pivot['xmax'] = c2d
653         pivot['ymin'] = c2d
654         pivot['ymax'] = c2d
655         pivot['zmin'] = c2d
656         pivot['zmax'] = c2d
657     else:
658         pivot[0] = points[6]
659         pivot[1] = points[7]
660         pivot[2] = points[4]
661         pivot[3] = points[5]
662         pivot[4] = points[2]
663         pivot[5] = points[3]
664         pivot[6] = points[0]
665         pivot[7] = points[1]
666         pivot['xmin'] = points['xmax']
667         pivot['xmax'] = points['xmin']
668         pivot['ymin'] = points['ymax']
669         pivot['ymax'] = points['ymin']
670         pivot['zmin'] = points['zmax']
671         pivot['zmax'] = points['zmin']
672
673     np_print('self.mode = ', self.mode)
674     fields = False
675     field_contours = False
676     pivots = True
677     pivot_lines = True
678     if fields:
679         # Hover markers - fields
680         bgl.glColor4f(0.65, 0.85, 1.0, 0.35)
681         for mark in markers.items():
682             if mark[0] == self.mode:
683                 #if type(mark[0]) is not str:
684                     #bgl.glColor4f(1.0, 1.0, 1.0, 0.3)
685                 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
686                 for x, y in mark[1]:
687                     bgl.glVertex2f(x, y)
688                 bgl.glEnd()
689
690     if field_contours:
691         # Hover markers - contours
692         bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
693         bgl.glLineWidth(1.2)
694         for mark in markers.items():
695             if mark[0] == self.mode:
696                 if type(mark[0]) is not str:
697                     #bgl.glColor4f(0.3, 0.6, 1.0, 0.5)
698                     bgl.glColor4f(1.0, 1.0, 1.0, 0.75)
699                     bgl.glLineWidth(1.0)
700                 bgl.glBegin(bgl.GL_LINE_STRIP)
701                 for x, y in mark[1]:
702                     bgl.glVertex2f(x, y)
703                 bgl.glEnd()
704
705
706     # Hover markers - pivot
707
708     bgl.glEnable(bgl.GL_POINT_SMOOTH)
709
710     bgl.glColor4f(*mark_col)
711     bgl.glPointSize(12)
712     for p in pivot.items():
713         if p[0] == self.mode:
714             if pivots:
715                 bgl.glBegin(bgl.GL_POINTS)
716                 #np_print(p[1])
717                 bgl.glVertex2f(*p[1])
718                 bgl.glEnd()
719
720             if pivot_lines:
721                 bgl.glLineWidth(1.0)
722                 #bgl.glEnable(bgl.GL_LINE_STIPPLE)
723                 bgl.glBegin(bgl.GL_LINE_STRIP)
724                 bgl.glVertex2f(*points[self.mode])
725                 bgl.glVertex2f(*p[1])
726                 bgl.glEnd()
727                 #bgl.glDisable(bgl.GL_LINE_STIPPLE)
728
729     if self.flag_cenpivot:
730         bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
731         bgl.glPointSize(12)
732         bgl.glBegin(bgl.GL_POINTS)
733         bgl.glVertex2f(*c2d)
734         bgl.glEnd()
735
736     bgl.glDisable(bgl.GL_POINT_SMOOTH)
737
738     # Hover markers - points
739     bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
740     bgl.glPointSize(16)
741     for mark in markers.items():
742         if mark[0] == self.mode:
743             if type(mark[0]) is not str:
744                 bgl.glColor4f(*mark_col)
745                 bgl.glEnable(bgl.GL_POINT_SMOOTH)
746                 bgl.glPointSize(18)
747             bgl.glBegin(bgl.GL_POINTS)
748             bgl.glVertex2f(*points[self.mode])
749             bgl.glEnd()
750             bgl.glColor4f(*mark_col)
751             bgl.glPointSize(12)
752             if type(mark[0]) is not str:
753                 bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
754                 bgl.glEnable(bgl.GL_POINT_SMOOTH)
755                 bgl.glPointSize(14)
756             bgl.glBegin(bgl.GL_POINTS)
757             bgl.glVertex2f(*points[self.mode])
758             bgl.glEnd()
759             bgl.glDisable(bgl.GL_POINT_SMOOTH)
760
761     if self.flag_force:
762         flash_size = 7
763         flash = [[0, 0], [2, 2], [1, 2], [2, 4], [0, 2], [1, 2], [0, 0]]
764         for co in flash:
765             co[0] = round((co[0] * flash_size),0) + self.co2d[0] + flash_size * 2
766             co[1] = round((co[1] * flash_size),0) + self.co2d[1] - flash_size * 2
767
768         bgl.glColor4f(0.95, 0.95, 0.0, 1.0)
769         bgl.glBegin(bgl.GL_TRIANGLE_FAN)
770         for i, co in enumerate(flash):
771             if i in range(0,3): bgl.glVertex2f(*co)
772         bgl.glEnd()
773         bgl.glBegin(bgl.GL_TRIANGLE_FAN)
774         for i, co in enumerate(flash):
775             if i in range(3,6): bgl.glVertex2f(*co)
776         bgl.glEnd()
777         bgl.glColor4f(1.0, 0.7, 0.0, 1.0)
778         bgl.glBegin(bgl.GL_LINE_STRIP)
779         for co in flash:
780             bgl.glVertex2f(*co)
781         bgl.glEnd()
782
783
784
785     # Restore opengl defaults
786     bgl.glDisable(bgl.GL_POINTS)
787     bgl.glLineWidth(1)
788     bgl.glDisable(bgl.GL_BLEND)
789     bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
790
791
792 # Defining the operator that will do the actual scaling. It is basically a standard resize operator equipped with couple of additions. It also uses some listening operators that clean up the leftovers should the user interrupt the command. Many thanks to CoDEmanX and lukas_t:
793
794 class NPPSRunResize(bpy.types.Operator):
795     bl_idname = 'object.np_ps_run_resize'
796     bl_label = 'NP PS Run Resize'
797     bl_options = {'INTERNAL'}
798
799     def modal(self,context,event):
800         context.area.tag_redraw()
801         #flag = NP020PS.flag
802         #self.mode = NP020PS.mode
803         #self.flag_con = NP020PS.flag_con
804         #self.flag_cenpivot = NP020PS.flag_cenpivot
805         #self.flag_lev = NP020PS.flag_lev
806         #self.flag_force = NP020PS.flag_force
807         #axis = NP020PS.axis
808
809         if event.type in ('LEFTMOUSE', 'NUMPAD_ENTER') and event.value == 'RELEASE':
810             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
811             return{'FINISHED'}
812
813         elif event.type in ('ESC', 'RIGHTMOUSE'):
814         # this actually begins when user RELEASES esc or rightmouse, PRESS is
815         # taken by transform.resize operator
816             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
817             return{'FINISHED'}
818
819         return{'PASS_THROUGH'}
820
821     def invoke(self, context, event):
822         flag = NP020PS.flag
823
824         if context.area.type == 'VIEW_3D':
825             if flag == 'EXIT':
826                 np_print('RunResize_INVOKE_DECLINED_FINISHED',';','flag = ', flag)
827                 return {'FINISHED'}
828             elif flag == 'RUNRESIZE':
829                 axis = NP020PS.axis
830             args = (self, context)
831             self._handle = bpy.types.SpaceView3D.draw_handler_add(DRAW_RunResize, args, 'WINDOW', 'POST_PIXEL')
832             context.window_manager.modal_handler_add(self)
833             bpy.ops.transform.resize('INVOKE_DEFAULT', constraint_axis = axis, orient_type = 'GLOBAL')
834
835             np_print('RunResize_INVOKE_a_RUNNING_MODAL')
836             return {'RUNNING_MODAL'}
837         else:
838             self.report({'WARNING'}, "View3D not found, cannot run operator")
839             np_print('RunResize_INVOKE_DECLINED_FINISHED',';','flag = ', Storage.flag)
840             return {'FINISHED'}
841
842
843 # Defining the set of instructions that will draw the graphical OpenGL elements on the screen during the execution of RunResize operator:
844
845 def DRAW_RunResize(self, context):
846
847     # Restore opengl defaults
848     bgl.glDisable(bgl.GL_POINTS)
849     bgl.glLineWidth(1)
850     bgl.glDisable(bgl.GL_BLEND)
851     bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
852
853
854 # Restoring the object selection and system settings from before the operator activation:
855
856 class NPPSRestoreContext(bpy.types.Operator):
857     bl_idname = "object.np_ps_restore_context"
858     bl_label = "NP PS Restore Context"
859     bl_options = {'INTERNAL'}
860
861     def execute(self, context):
862         selob = NP020PS.selob
863         bpy.ops.object.select_all(action = 'DESELECT')
864         for ob in selob:
865             ob.select_set(True)
866             bpy.context.view_layer.objects.active = ob
867         bpy.context.tool_settings.use_snap = NP020PS.use_snap
868         bpy.context.tool_settings.snap_element = NP020PS.snap_element
869         bpy.context.tool_settings.snap_target = NP020PS.snap_target
870         bpy.context.space_data.pivot_point = NP020PS.pivot_point
871         bpy.context.space_data.transform_orientation = NP020PS.trans_orient
872         if NP020PS.trans_custom: bpy.ops.transform.delete_orientation()
873         bpy.context.scene.cursor.location = NP020PS.curloc
874         if NP020PS.acob is not None:
875             bpy.context.view_layer.objects.active = NP020PS.acob
876             bpy.ops.object.mode_set(mode = NP020PS.edit_mode)
877         NP020PS.flag = 'DISPLAY'
878         return {'FINISHED'}
879
880
881 # This is the actual addon process, the algorithm that defines the order of operator activation inside the main macro:
882
883 def register():
884
885     NP020PointScale.define("OBJECT_OT_np_ps_get_context")
886     NP020PointScale.define("OBJECT_OT_np_ps_get_selection")
887     NP020PointScale.define("OBJECT_OT_np_ps_prepare_context")
888     NP020PointScale.define("OBJECT_OT_np_ps_display_cage")
889     NP020PointScale.define("OBJECT_OT_np_ps_prepare_context")
890     NP020PointScale.define("OBJECT_OT_np_ps_run_resize")
891     NP020PointScale.define("OBJECT_OT_np_ps_restore_context")
892
893 def unregister():
894     pass