Cleanup: strip trailing space, remove BOM
[blender-addons.git] / greasepencil_tools / box_deform.py
blob6fa866ecedf0d7ca3a8d1ed54fbde7bb55be1aaf
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 '''Based on Box_deform standalone addon - Author: Samuel Bernou'''
21 from .prefs import get_addon_prefs
23 import bpy
24 import numpy as np
26 def location_to_region(worldcoords):
27 from bpy_extras import view3d_utils
28 return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords)
30 def region_to_location(viewcoords, depthcoords):
31 from bpy_extras import view3d_utils
32 return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords)
34 def store_cage(self, vg_name):
35 import time
36 unique_id = time.strftime(r'%y%m%d%H%M%S') # ex: 20210711111117
37 # name = f'gp_lattice_{unique_id}'
38 name = f'{self.gp_obj.name}_lat{unique_id}'
39 vg = self.gp_obj.vertex_groups.get(vg_name)
40 if vg:
41 vg.name = name
42 for o in self.other_gp:
43 vg = o.vertex_groups.get(vg_name)
44 if vg:
45 vg.name = name
47 self.cage.name = name
48 self.cage.data.name = name
49 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
50 if mod:
51 mod.name = name #f'Lattice_{unique_id}'
52 mod.vertex_group = name
53 for o in self.other_gp:
54 mod = o.grease_pencil_modifiers.get('tmp_lattice')
55 if mod:
56 mod.name = name
57 mod.vertex_group = name
59 def assign_vg(obj, vg_name, delete=False):
60 ## create vertex group
61 vg = obj.vertex_groups.get(vg_name)
62 if vg:
63 # remove to start clean
64 obj.vertex_groups.remove(vg)
65 if delete:
66 return
68 vg = obj.vertex_groups.new(name=vg_name)
69 bpy.ops.gpencil.vertex_group_assign()
70 return vg
72 def view_cage(obj):
73 prefs = get_addon_prefs()
74 lattice_interp = prefs.default_deform_type
76 gp = obj.data
77 gpl = gp.layers
79 from_obj = bpy.context.mode == 'OBJECT'
80 all_gps = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
81 other_gp = [o for o in all_gps if o is not obj]
83 coords = []
84 initial_mode = bpy.context.mode
86 ## get points
87 if bpy.context.mode == 'EDIT_GPENCIL':
88 for l in gpl:
89 if l.lock or l.hide or not l.active_frame:#or len(l.frames)
90 continue
91 if gp.use_multiedit:
92 target_frames = [f for f in l.frames if f.select]
93 else:
94 target_frames = [l.active_frame]
96 for f in target_frames:
97 for s in f.strokes:
98 if not s.select:
99 continue
100 for p in s.points:
101 if p.select:
102 # get real location
103 coords.append(obj.matrix_world @ p.co)
105 elif bpy.context.mode == 'OBJECT': # object mode -> all points of all selected gp objects
106 for gpo in all_gps:
107 for l in gpo.data.layers:# if l.hide:continue# only visible ? (might break things)
108 if not len(l.frames):
109 continue # skip frameless layer
110 for s in l.active_frame.strokes:
111 for p in s.points:
112 coords.append(gpo.matrix_world @ p.co)
114 elif bpy.context.mode == 'PAINT_GPENCIL':
115 # get last stroke points coordinated
116 if not gpl.active or not gpl.active.active_frame:
117 return 'No frame to deform'
119 if not len(gpl.active.active_frame.strokes):
120 return 'No stroke found to deform'
122 paint_id = -1
123 if bpy.context.scene.tool_settings.use_gpencil_draw_onback:
124 paint_id = 0
125 coords = [obj.matrix_world @ p.co for p in gpl.active.active_frame.strokes[paint_id].points]
127 else:
128 return 'Wrong mode!'
130 if not coords:
131 ## maybe silent return instead (need special str code to manage errorless return)
132 return 'No points found!'
134 if bpy.context.mode in ('EDIT_GPENCIL', 'PAINT_GPENCIL') and len(coords) < 2:
135 # Dont block object mod
136 return 'Less than two point selected'
138 vg_name = 'lattice_cage_deform_group'
140 if bpy.context.mode == 'EDIT_GPENCIL':
141 vg = assign_vg(obj, vg_name)
143 if bpy.context.mode == 'PAINT_GPENCIL':
144 # points cannot be assign to API yet(ugly and slow workaround but only way)
145 # -> https://developer.blender.org/T56280 so, hop'in'ops !
147 # store selection and deselect all
148 plist = []
149 for s in gpl.active.active_frame.strokes:
150 for p in s.points:
151 plist.append([p, p.select])
152 p.select = False
154 # select
155 ## foreach_set does not update
156 # gpl.active.active_frame.strokes[paint_id].points.foreach_set('select', [True]*len(gpl.active.active_frame.strokes[paint_id].points))
157 for p in gpl.active.active_frame.strokes[paint_id].points:
158 p.select = True
160 # assign
161 bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
162 vg = assign_vg(obj, vg_name)
164 # restore
165 for pl in plist:
166 pl[0].select = pl[1]
169 ## View axis Mode ---
171 ## get view coordinate of all points
172 coords2D = [location_to_region(co) for co in coords]
174 # find centroid for depth (or more economic, use obj origin...)
175 centroid = np.mean(coords, axis=0)
177 # not a mean ! a mean of extreme ! centroid2d = np.mean(coords2D, axis=0)
178 all_x, all_y = np.array(coords2D)[:, 0], np.array(coords2D)[:, 1]
179 min_x, min_y = np.min(all_x), np.min(all_y)
180 max_x, max_y = np.max(all_x), np.max(all_y)
182 width = (max_x - min_x)
183 height = (max_y - min_y)
184 center_x = min_x + (width/2)
185 center_y = min_y + (height/2)
187 centroid2d = (center_x,center_y)
188 center = region_to_location(centroid2d, centroid)
189 # bpy.context.scene.cursor.location = center#Dbg
192 #corner Bottom-left to Bottom-right
193 x0 = region_to_location((min_x, min_y), centroid)
194 x1 = region_to_location((max_x, min_y), centroid)
195 x_worldsize = (x0 - x1).length
197 #corner Bottom-left to top-left
198 y0 = region_to_location((min_x, min_y), centroid)
199 y1 = region_to_location((min_x, max_y), centroid)
200 y_worldsize = (y0 - y1).length
202 ## in case of 3
204 lattice_name = 'lattice_cage_deform'
205 # cleaning
206 cage = bpy.data.objects.get(lattice_name)
207 if cage:
208 bpy.data.objects.remove(cage)
210 lattice = bpy.data.lattices.get(lattice_name)
211 if lattice:
212 bpy.data.lattices.remove(lattice)
214 # create lattice object
215 lattice = bpy.data.lattices.new(lattice_name)
216 cage = bpy.data.objects.new(lattice_name, lattice)
217 cage.show_in_front = True
219 ## Master (root) collection
220 bpy.context.scene.collection.objects.link(cage)
222 # spawn cage and align it to view
224 r3d = bpy.context.space_data.region_3d
225 viewmat = r3d.view_matrix
227 cage.matrix_world = viewmat.inverted()
228 cage.scale = (x_worldsize, y_worldsize, 1)
229 ## Z aligned in view direction (need minus X 90 degree to be aligned FRONT)
230 # cage.rotation_euler.x -= radians(90)
231 # cage.scale = (x_worldsize, 1, y_worldsize)
232 cage.location = center
234 lattice.points_u = 2
235 lattice.points_v = 2
236 lattice.points_w = 1
238 lattice.interpolation_type_u = lattice_interp #'KEY_LINEAR'-'KEY_BSPLINE'
239 lattice.interpolation_type_v = lattice_interp
240 lattice.interpolation_type_w = lattice_interp
242 mod = obj.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE')
243 if from_obj:
244 mods = []
245 for o in other_gp:
246 mods.append( o.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE') )
248 # move to top if modifiers exists
249 for _ in range(len(obj.grease_pencil_modifiers)):
250 bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
251 if from_obj:
252 for o in other_gp:
253 for _ in range(len(o.grease_pencil_modifiers)):
254 bpy.ops.object.gpencil_modifier_move_up({'object':o}, modifier='tmp_lattice')
256 mod.object = cage
257 if from_obj:
258 for m in mods:
259 m.object = cage
261 if initial_mode == 'PAINT_GPENCIL':
262 mod.layer = gpl.active.info
264 # note : if initial was Paint, changed to Edit
265 # so vertex attribution is valid even for paint
266 if bpy.context.mode == 'EDIT_GPENCIL':
267 mod.vertex_group = vg.name
269 # Go in object mode if not already
270 if bpy.context.mode != 'OBJECT':
271 bpy.ops.object.mode_set(mode='OBJECT')
273 # Store name of deformed object in case of 'revive modal'
274 cage.vertex_groups.new(name=obj.name)
275 if from_obj:
276 for o in other_gp:
277 cage.vertex_groups.new(name=o.name)
279 ## select and make cage active
280 # cage.select_set(True)
281 bpy.context.view_layer.objects.active = cage
282 obj.select_set(False) # deselect GP object
283 bpy.ops.object.mode_set(mode='EDIT') # go in lattice edit mode
284 bpy.ops.lattice.select_all(action='SELECT') # select all points
286 if prefs.use_clic_drag:
287 ## Eventually change tool mode to tweak for direct point editing (reset after before leaving)
288 bpy.ops.wm.tool_set_by_id(name="builtin.select") # Tweaktoolcode
289 return cage
292 def back_to_obj(obj, gp_mode, org_lattice_toolset, context):
293 if context.mode == 'EDIT_LATTICE' and org_lattice_toolset: # Tweaktoolcode - restore the active tool used by lattice edit..
294 bpy.ops.wm.tool_set_by_id(name = org_lattice_toolset) # Tweaktoolcode
296 # gp object active and selected
297 bpy.ops.object.mode_set(mode='OBJECT')
298 obj.select_set(True)
299 bpy.context.view_layer.objects.active = obj
302 def delete_cage(cage):
303 lattice = cage.data
304 bpy.data.objects.remove(cage)
305 bpy.data.lattices.remove(lattice)
307 def apply_cage(gp_obj):
308 mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
309 multi_user = None
310 if mod:
311 if gp_obj.data.users > 1:
312 old = gp_obj.data
313 multi_user = old.name
314 other_user = [o for o in bpy.data.objects if o is not gp_obj and o.data is old]
315 gp_obj.data = gp_obj.data.copy()
317 # bpy.ops.object.gpencil_modifier_apply(apply_as='DATA', modifier=mod.name)
318 bpy.ops.object.gpencil_modifier_apply({'object': gp_obj}, apply_as='DATA', modifier=mod.name)
320 if multi_user:
321 for o in other_user: # relink
322 o.data = gp_obj.data
323 bpy.data.grease_pencils.remove(old)
324 gp_obj.data.name = multi_user
326 else:
327 print('tmp_lattice modifier not found to apply...')
329 def cancel_cage(self):
330 #remove modifier
331 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
332 if mod:
333 self.gp_obj.grease_pencil_modifiers.remove(mod)
334 else:
335 print(f'tmp_lattice modifier not found to remove on {self.gp_obj.name}')
337 for ob in self.other_gp:
338 mod = ob.grease_pencil_modifiers.get('tmp_lattice')
339 if mod:
340 ob.grease_pencil_modifiers.remove(mod)
341 else:
342 print(f'tmp_lattice modifier not found to remove on {ob.name}')
344 delete_cage(self.cage)
347 class GP_OT_latticeGpDeform(bpy.types.Operator):
348 """Create a lattice to use as quad corner transform"""
349 bl_idname = "gp.latticedeform"
350 bl_label = "Box Deform"
351 bl_description = "Use lattice for free box transforms on grease pencil points (Ctrl+T)"
352 bl_options = {"REGISTER", "UNDO"}
354 @classmethod
355 def poll(cls, context):
356 return context.object is not None and context.object.type in ('GPENCIL','LATTICE')
358 # local variable
359 tab_press_ct = 0
361 def modal(self, context, event):
362 display_text = f"Deform Cage size: {self.lat.points_u}x{self.lat.points_v} (1-9 or ctrl + ←→↑↓) | \
363 mode (M) : {'Linear' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'Spline'} | \
364 valid:Spacebar/Enter, cancel:Del/Backspace/Tab/Ctrl+T"
365 context.area.header_text_set(display_text)
368 ## Handle ctrl+Z
369 if event.type in {'Z'} and event.value == 'PRESS' and event.ctrl:
370 ## Disable (capture key)
371 return {"RUNNING_MODAL"}
372 ## Not found how possible to find modal start point in undo stack to
373 # print('ops list', context.window_manager.operators.keys())
374 # if context.window_manager.operators:#can be empty
375 # print('\nlast name', context.window_manager.operators[-1].name)
377 # Auto interpo check
378 if self.auto_interp:
379 if event.type in {'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO',} and event.value == 'PRESS':
380 self.set_lattice_interp('KEY_BSPLINE')
381 if event.type in {'DOWN_ARROW', "UP_ARROW", "RIGHT_ARROW", "LEFT_ARROW"} and event.value == 'PRESS' and event.ctrl:
382 self.set_lattice_interp('KEY_BSPLINE')
383 if event.type in {'ONE'} and event.value == 'PRESS':
384 self.set_lattice_interp('KEY_LINEAR')
386 # Single keys
387 if event.type in {'H'} and event.value == 'PRESS':
388 # self.report({'INFO'}, "Can't hide")
389 return {"RUNNING_MODAL"}
391 if event.type in {'ONE'} and event.value == 'PRESS':# , 'NUMPAD_1'
392 self.lat.points_u = self.lat.points_v = 2
393 return {"RUNNING_MODAL"}
395 if event.type in {'TWO'} and event.value == 'PRESS':# , 'NUMPAD_2'
396 self.lat.points_u = self.lat.points_v = 3
397 return {"RUNNING_MODAL"}
399 if event.type in {'THREE'} and event.value == 'PRESS':# , 'NUMPAD_3'
400 self.lat.points_u = self.lat.points_v = 4
401 return {"RUNNING_MODAL"}
403 if event.type in {'FOUR'} and event.value == 'PRESS':# , 'NUMPAD_4'
404 self.lat.points_u = self.lat.points_v = 5
405 return {"RUNNING_MODAL"}
407 if event.type in {'FIVE'} and event.value == 'PRESS':# , 'NUMPAD_5'
408 self.lat.points_u = self.lat.points_v = 6
409 return {"RUNNING_MODAL"}
411 if event.type in {'SIX'} and event.value == 'PRESS':# , 'NUMPAD_6'
412 self.lat.points_u = self.lat.points_v = 7
413 return {"RUNNING_MODAL"}
415 if event.type in {'SEVEN'} and event.value == 'PRESS':# , 'NUMPAD_7'
416 self.lat.points_u = self.lat.points_v = 8
417 return {"RUNNING_MODAL"}
419 if event.type in {'EIGHT'} and event.value == 'PRESS':# , 'NUMPAD_8'
420 self.lat.points_u = self.lat.points_v = 9
421 return {"RUNNING_MODAL"}
423 if event.type in {'NINE'} and event.value == 'PRESS':# , 'NUMPAD_9'
424 self.lat.points_u = self.lat.points_v = 10
425 return {"RUNNING_MODAL"}
427 if event.type in {'ZERO'} and event.value == 'PRESS':# , 'NUMPAD_0'
428 self.lat.points_u = 2
429 self.lat.points_v = 1
430 return {"RUNNING_MODAL"}
432 if event.type in {'RIGHT_ARROW'} and event.value == 'PRESS' and event.ctrl:
433 if self.lat.points_u < 20:
434 self.lat.points_u += 1
435 return {"RUNNING_MODAL"}
437 if event.type in {'LEFT_ARROW'} and event.value == 'PRESS' and event.ctrl:
438 if self.lat.points_u > 1:
439 self.lat.points_u -= 1
440 return {"RUNNING_MODAL"}
442 if event.type in {'UP_ARROW'} and event.value == 'PRESS' and event.ctrl:
443 if self.lat.points_v < 20:
444 self.lat.points_v += 1
445 return {"RUNNING_MODAL"}
447 if event.type in {'DOWN_ARROW'} and event.value == 'PRESS' and event.ctrl:
448 if self.lat.points_v > 1:
449 self.lat.points_v -= 1
450 return {"RUNNING_MODAL"}
453 # change modes
454 if event.type in {'M'} and event.value == 'PRESS':
455 self.auto_interp = False
456 interp = 'KEY_BSPLINE' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'KEY_LINEAR'
457 self.set_lattice_interp(interp)
458 return {"RUNNING_MODAL"}
460 # Valid
461 if event.type in {'RET', 'SPACE'}:
462 if event.value == 'PRESS':
463 context.window_manager.boxdeform_running = False
464 self.restore_prefs(context)
465 back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
466 if event.shift:
467 # Let the cage as is with a unique ID
468 store_cage(self, 'lattice_cage_deform_group')
469 else:
470 apply_cage(self.gp_obj) # must be in object mode
471 assign_vg(self.gp_obj, 'lattice_cage_deform_group', delete=True)
472 for o in self.other_gp:
473 apply_cage(o)
474 assign_vg(o, 'lattice_cage_deform_group', delete=True)
475 delete_cage(self.cage)
477 # back to original mode
478 if self.gp_mode != 'OBJECT':
479 bpy.ops.object.mode_set(mode=self.gp_mode)
480 context.area.header_text_set(None)#reset header
482 return {'FINISHED'}
484 # Abort ---
485 # One Warning for Tab cancellation.
486 if event.type == 'TAB' and event.value == 'PRESS':
487 self.tab_press_ct += 1
488 if self.tab_press_ct < 2:
489 self.report({'WARNING'}, "Pressing TAB again will Cancel")
490 return {"RUNNING_MODAL"}
492 if event.type in {'T'} and event.value == 'PRESS' and event.ctrl:# Retyped same shortcut
493 self.cancel(context)
494 return {'CANCELLED'}
496 if event.type in {'DEL', 'BACK_SPACE'} or self.tab_press_ct >= 2:#'ESC',
497 self.cancel(context)
498 return {'CANCELLED'}
500 return {'PASS_THROUGH'}
502 def set_lattice_interp(self, interp):
503 self.lat.interpolation_type_u = self.lat.interpolation_type_v = self.lat.interpolation_type_w = interp
505 def cancel(self, context):
506 context.window_manager.boxdeform_running = False
507 self.restore_prefs(context)
508 back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
509 cancel_cage(self)
510 assign_vg(self.gp_obj, 'lattice_cage_deform_group', delete=True)
511 context.area.header_text_set(None)
512 if self.gp_mode != 'OBJECT':
513 bpy.ops.object.mode_set(mode=self.gp_mode)
515 def store_prefs(self, context):
516 # store_valierables <-< preferences
517 self.use_drag_immediately = context.preferences.inputs.use_drag_immediately
518 self.drag_threshold_mouse = context.preferences.inputs.drag_threshold_mouse
519 self.drag_threshold_tablet = context.preferences.inputs.drag_threshold_tablet
520 self.use_overlays = context.space_data.overlay.show_overlays
521 # maybe store in windows manager to keep around in case of modal revival ?
523 def restore_prefs(self, context):
524 # preferences <-< store_valierables
525 context.preferences.inputs.use_drag_immediately = self.use_drag_immediately
526 context.preferences.inputs.drag_threshold_mouse = self.drag_threshold_mouse
527 context.preferences.inputs.drag_threshold_tablet = self.drag_threshold_tablet
528 context.space_data.overlay.show_overlays = self.use_overlays
530 def set_prefs(self, context):
531 context.preferences.inputs.use_drag_immediately = True
532 context.preferences.inputs.drag_threshold_mouse = 1
533 context.preferences.inputs.drag_threshold_tablet = 3
534 context.space_data.overlay.show_overlays = True
536 def invoke(self, context, event):
537 ## Restrict to 3D view
538 if context.area.type != 'VIEW_3D':
539 self.report({'WARNING'}, "View3D not found, cannot run operator")
540 return {'CANCELLED'}
542 if not context.object:#do it in poll ?
543 self.report({'ERROR'}, "No active objects found")
544 return {'CANCELLED'}
546 if context.window_manager.boxdeform_running:
547 return {'CANCELLED'}
549 self.prefs = get_addon_prefs()#get_prefs
550 self.auto_interp = self.prefs.auto_swap_deform_type
551 self.org_lattice_toolset = None
552 ## usability toggles
553 if self.prefs.use_clic_drag:#Store the active tool since we will change it
554 self.org_lattice_toolset = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname# Tweaktoolcode
556 #store (scene properties needed in case of ctrlZ revival)
557 self.store_prefs(context)
558 self.gp_mode = 'EDIT_GPENCIL'
560 # --- special Case of lattice revive modal, just after ctrl+Z back into lattice with modal stopped
561 if context.mode == 'EDIT_LATTICE' and context.object.name == 'lattice_cage_deform' and len(context.object.vertex_groups):
562 self.gp_obj = context.scene.objects.get(context.object.vertex_groups[0].name)
563 if not self.gp_obj:
564 self.report({'ERROR'}, "/!\\ Box Deform : Cannot find object to target")
565 return {'CANCELLED'}
566 if not self.gp_obj.grease_pencil_modifiers.get('tmp_lattice'):
567 self.report({'ERROR'}, "/!\\ No 'tmp_lattice' modifiers on GP object")
568 return {'CANCELLED'}
569 self.cage = context.object
570 self.lat = self.cage.data
571 self.set_prefs(context)
573 if self.prefs.use_clic_drag:
574 bpy.ops.wm.tool_set_by_id(name="builtin.select")
575 context.window_manager.boxdeform_running = True
576 context.window_manager.modal_handler_add(self)
577 return {'RUNNING_MODAL'}
579 if context.object.type != 'GPENCIL':
580 # self.report({'ERROR'}, "Works only on gpencil objects")
581 ## silent return
582 return {'CANCELLED'}
584 if context.mode not in ('EDIT_GPENCIL', 'OBJECT', 'PAINT_GPENCIL'):
585 # self.report({'WARNING'}, "Works only in following GPencil modes: object / edit/ paint")# ERROR
586 ## silent return
587 return {'CANCELLED'}
590 # bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
591 # https://developer.blender.org/D6147 <- undo forget
593 self.gp_obj = context.object
595 self.from_object = context.mode == 'OBJECT'
596 self.all_gps = self.other_gp = []
597 if self.from_object:
598 self.all_gps = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
599 self.other_gp = [o for o in self.all_gps if o is not self.gp_obj]
601 # Clean potential failed previous job (delete tmp lattice)
602 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
603 if mod:
604 print('Deleted remaining lattice modifiers')
605 self.gp_obj.grease_pencil_modifiers.remove(mod)
607 phantom_obj = context.scene.objects.get('lattice_cage_deform')
608 if phantom_obj:
609 print('Deleted remaining lattice object')
610 delete_cage(phantom_obj)
612 if bpy.app.version < (2,93,0):
613 if [m for m in self.gp_obj.grease_pencil_modifiers if m.type == 'GP_LATTICE']:
614 self.report({'ERROR'}, "Grease pencil object already has a lattice modifier (multi-lattices are enabled in blender 2.93+)")
615 return {'CANCELLED'}
617 self.gp_mode = context.mode # store mode for restore
619 # All good, create lattice and start modal
621 # Create lattice (and switch to lattice edit) ----
622 self.cage = view_cage(self.gp_obj)
623 if isinstance(self.cage, str):#error, cage not created, display error
624 self.report({'ERROR'}, self.cage)
625 return {'CANCELLED'}
627 self.lat = self.cage.data
629 self.set_prefs(context)
630 context.window_manager.boxdeform_running = True
631 context.window_manager.modal_handler_add(self)
632 return {'RUNNING_MODAL'}
634 ## --- KEYMAP
636 addon_keymaps = []
637 def register_keymaps():
638 addon = bpy.context.window_manager.keyconfigs.addon
640 km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')
641 kmi = km.keymap_items.new("gp.latticedeform", type ='T', value = "PRESS", ctrl = True)
642 kmi.repeat = False
643 addon_keymaps.append((km, kmi))
645 def unregister_keymaps():
646 for km, kmi in addon_keymaps:
647 km.keymap_items.remove(kmi)
648 addon_keymaps.clear()
650 ### --- REGISTER ---
652 def register():
653 if bpy.app.background:
654 return
655 bpy.types.WindowManager.boxdeform_running = bpy.props.BoolProperty(default=False)
656 bpy.utils.register_class(GP_OT_latticeGpDeform)
657 register_keymaps()
659 def unregister():
660 if bpy.app.background:
661 return
662 unregister_keymaps()
663 bpy.utils.unregister_class(GP_OT_latticeGpDeform)
664 wm = bpy.context.window_manager
665 p = 'boxdeform_running'
666 if p in wm:
667 del wm[p]