GPencil Tools: Optimize Undo for Rotate Canvas
[blender-addons.git] / greasepencil_tools / box_deform.py
blob124caccce9be613ca7f6d0f4a1adc71696a1ae68
1 # SPDX-FileCopyrightText: 2020-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 '''Based on Box_deform standalone addon - Author: Samuel Bernou'''
7 from .prefs import get_addon_prefs
9 import bpy
10 import numpy as np
12 def location_to_region(worldcoords):
13 from bpy_extras import view3d_utils
14 return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords)
16 def region_to_location(viewcoords, depthcoords):
17 from bpy_extras import view3d_utils
18 return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords)
20 def store_cage(self, vg_name):
21 import time
22 unique_id = time.strftime(r'%y%m%d%H%M%S') # ex: 20210711111117
23 # name = f'gp_lattice_{unique_id}'
24 name = f'{self.gp_obj.name}_lat{unique_id}'
25 vg = self.gp_obj.vertex_groups.get(vg_name)
26 if vg:
27 vg.name = name
28 for o in self.other_gp:
29 vg = o.vertex_groups.get(vg_name)
30 if vg:
31 vg.name = name
33 self.cage.name = name
34 self.cage.data.name = name
35 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
36 if mod:
37 mod.name = name #f'Lattice_{unique_id}'
38 mod.vertex_group = name
39 for o in self.other_gp:
40 mod = o.grease_pencil_modifiers.get('tmp_lattice')
41 if mod:
42 mod.name = name
43 mod.vertex_group = name
45 def assign_vg(obj, vg_name, delete=False):
46 ## create vertex group
47 vg = obj.vertex_groups.get(vg_name)
48 if vg:
49 # remove to start clean
50 obj.vertex_groups.remove(vg)
51 if delete:
52 return
54 vg = obj.vertex_groups.new(name=vg_name)
55 bpy.ops.gpencil.vertex_group_assign()
56 return vg
58 def view_cage(obj):
59 prefs = get_addon_prefs()
60 lattice_interp = prefs.default_deform_type
62 gp = obj.data
63 gpl = gp.layers
65 from_obj = bpy.context.mode == 'OBJECT'
66 all_gps = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
67 other_gp = [o for o in all_gps if o is not obj]
69 coords = []
70 initial_mode = bpy.context.mode
72 ## get points
73 if bpy.context.mode == 'EDIT_GPENCIL':
74 for l in gpl:
75 if l.lock or l.hide or not l.active_frame:#or len(l.frames)
76 continue
77 if gp.use_multiedit:
78 target_frames = [f for f in l.frames if f.select]
79 else:
80 target_frames = [l.active_frame]
82 for f in target_frames:
83 for s in f.strokes:
84 if not s.select:
85 continue
86 for p in s.points:
87 if p.select:
88 # get real location
89 coords.append(obj.matrix_world @ p.co)
91 elif bpy.context.mode == 'OBJECT': # object mode -> all points of all selected gp objects
92 for gpo in all_gps:
93 for l in gpo.data.layers:# if l.hide:continue# only visible ? (might break things)
94 if not len(l.frames):
95 continue # skip frameless layer
96 for s in l.active_frame.strokes:
97 for p in s.points:
98 coords.append(gpo.matrix_world @ p.co)
100 elif bpy.context.mode == 'PAINT_GPENCIL':
101 # get last stroke points coordinated
102 if not gpl.active or not gpl.active.active_frame:
103 return 'No frame to deform'
105 if not len(gpl.active.active_frame.strokes):
106 return 'No stroke found to deform'
108 paint_id = -1
109 if bpy.context.scene.tool_settings.use_gpencil_draw_onback:
110 paint_id = 0
111 coords = [obj.matrix_world @ p.co for p in gpl.active.active_frame.strokes[paint_id].points]
113 else:
114 return 'Wrong mode!'
116 if not coords:
117 ## maybe silent return instead (need special str code to manage errorless return)
118 return 'No points found!'
120 if bpy.context.mode in ('EDIT_GPENCIL', 'PAINT_GPENCIL') and len(coords) < 2:
121 # Dont block object mod
122 return 'Less than two point selected'
124 vg_name = 'lattice_cage_deform_group'
126 if bpy.context.mode == 'EDIT_GPENCIL':
127 vg = assign_vg(obj, vg_name)
129 if bpy.context.mode == 'PAINT_GPENCIL':
130 # points cannot be assign to API yet(ugly and slow workaround but only way)
131 # -> https://developer.blender.org/T56280 so, hop'in'ops !
133 # store selection and deselect all
134 plist = []
135 for s in gpl.active.active_frame.strokes:
136 for p in s.points:
137 plist.append([p, p.select])
138 p.select = False
140 # select
141 ## foreach_set does not update
142 # gpl.active.active_frame.strokes[paint_id].points.foreach_set('select', [True]*len(gpl.active.active_frame.strokes[paint_id].points))
143 for p in gpl.active.active_frame.strokes[paint_id].points:
144 p.select = True
146 # assign
147 bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
148 vg = assign_vg(obj, vg_name)
150 # restore
151 for pl in plist:
152 pl[0].select = pl[1]
155 ## View axis Mode ---
157 ## get view coordinate of all points
158 coords2D = [location_to_region(co) for co in coords]
160 # find centroid for depth (or more economic, use obj origin...)
161 centroid = np.mean(coords, axis=0)
163 # not a mean ! a mean of extreme ! centroid2d = np.mean(coords2D, axis=0)
164 all_x, all_y = np.array(coords2D)[:, 0], np.array(coords2D)[:, 1]
165 min_x, min_y = np.min(all_x), np.min(all_y)
166 max_x, max_y = np.max(all_x), np.max(all_y)
168 width = (max_x - min_x)
169 height = (max_y - min_y)
170 center_x = min_x + (width/2)
171 center_y = min_y + (height/2)
173 centroid2d = (center_x,center_y)
174 center = region_to_location(centroid2d, centroid)
175 # bpy.context.scene.cursor.location = center#Dbg
178 #corner Bottom-left to Bottom-right
179 x0 = region_to_location((min_x, min_y), centroid)
180 x1 = region_to_location((max_x, min_y), centroid)
181 x_worldsize = (x0 - x1).length
183 #corner Bottom-left to top-left
184 y0 = region_to_location((min_x, min_y), centroid)
185 y1 = region_to_location((min_x, max_y), centroid)
186 y_worldsize = (y0 - y1).length
188 ## in case of 3
190 lattice_name = 'lattice_cage_deform'
191 # cleaning
192 cage = bpy.data.objects.get(lattice_name)
193 if cage:
194 bpy.data.objects.remove(cage)
196 lattice = bpy.data.lattices.get(lattice_name)
197 if lattice:
198 bpy.data.lattices.remove(lattice)
200 # create lattice object
201 lattice = bpy.data.lattices.new(lattice_name)
202 cage = bpy.data.objects.new(lattice_name, lattice)
203 cage.show_in_front = True
205 ## Master (root) collection
206 bpy.context.scene.collection.objects.link(cage)
208 # spawn cage and align it to view
210 r3d = bpy.context.space_data.region_3d
211 viewmat = r3d.view_matrix
213 cage.matrix_world = viewmat.inverted()
214 cage.scale = (x_worldsize, y_worldsize, 1)
215 ## Z aligned in view direction (need minus X 90 degree to be aligned FRONT)
216 # cage.rotation_euler.x -= radians(90)
217 # cage.scale = (x_worldsize, 1, y_worldsize)
218 cage.location = center
220 lattice.points_u = 2
221 lattice.points_v = 2
222 lattice.points_w = 1
224 lattice.interpolation_type_u = lattice_interp #'KEY_LINEAR'-'KEY_BSPLINE'
225 lattice.interpolation_type_v = lattice_interp
226 lattice.interpolation_type_w = lattice_interp
228 mod = obj.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE')
229 if from_obj:
230 mods = []
231 for o in other_gp:
232 mods.append( o.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE') )
234 # move to top if modifiers exists
235 for _ in range(len(obj.grease_pencil_modifiers)):
236 bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
237 if from_obj:
238 for o in other_gp:
239 for _ in range(len(o.grease_pencil_modifiers)):
240 context_override = {'object': o}
241 with bpy.context.temp_override(**context_override):
242 bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
244 mod.object = cage
245 if from_obj:
246 for m in mods:
247 m.object = cage
249 if initial_mode == 'PAINT_GPENCIL':
250 mod.layer = gpl.active.info
252 # note : if initial was Paint, changed to Edit
253 # so vertex attribution is valid even for paint
254 if bpy.context.mode == 'EDIT_GPENCIL':
255 mod.vertex_group = vg.name
257 # Go in object mode if not already
258 if bpy.context.mode != 'OBJECT':
259 bpy.ops.object.mode_set(mode='OBJECT')
261 # Store name of deformed object in case of 'revive modal'
262 cage.vertex_groups.new(name=obj.name)
263 if from_obj:
264 for o in other_gp:
265 cage.vertex_groups.new(name=o.name)
267 ## select and make cage active
268 # cage.select_set(True)
269 bpy.context.view_layer.objects.active = cage
270 obj.select_set(False) # deselect GP object
271 bpy.ops.object.mode_set(mode='EDIT') # go in lattice edit mode
272 bpy.ops.lattice.select_all(action='SELECT') # select all points
274 if prefs.use_clic_drag:
275 ## Eventually change tool mode to tweak for direct point editing (reset after before leaving)
276 bpy.ops.wm.tool_set_by_id(name="builtin.select") # Tweaktoolcode
277 return cage
280 def back_to_obj(obj, gp_mode, org_lattice_toolset, context):
281 if context.mode == 'EDIT_LATTICE' and org_lattice_toolset: # Tweaktoolcode - restore the active tool used by lattice edit..
282 bpy.ops.wm.tool_set_by_id(name = org_lattice_toolset) # Tweaktoolcode
284 # gp object active and selected
285 bpy.ops.object.mode_set(mode='OBJECT')
286 obj.select_set(True)
287 bpy.context.view_layer.objects.active = obj
290 def delete_cage(cage):
291 lattice = cage.data
292 bpy.data.objects.remove(cage)
293 bpy.data.lattices.remove(lattice)
295 def apply_cage(gp_obj, context):
296 mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
297 multi_user = None
298 if mod:
299 if gp_obj.data.users > 1:
300 old = gp_obj.data
301 multi_user = old.name
302 other_user = [o for o in bpy.data.objects if o is not gp_obj and o.data is old]
303 gp_obj.data = gp_obj.data.copy()
305 with context.temp_override(object=gp_obj):
306 bpy.ops.object.gpencil_modifier_apply(apply_as='DATA', modifier=mod.name)
308 if multi_user:
309 for o in other_user: # relink
310 o.data = gp_obj.data
311 bpy.data.grease_pencils.remove(old)
312 gp_obj.data.name = multi_user
314 else:
315 print('tmp_lattice modifier not found to apply...')
317 def cancel_cage(self):
318 #remove modifier
319 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
320 if mod:
321 self.gp_obj.grease_pencil_modifiers.remove(mod)
322 else:
323 print(f'tmp_lattice modifier not found to remove on {self.gp_obj.name}')
325 for ob in self.other_gp:
326 mod = ob.grease_pencil_modifiers.get('tmp_lattice')
327 if mod:
328 ob.grease_pencil_modifiers.remove(mod)
329 else:
330 print(f'tmp_lattice modifier not found to remove on {ob.name}')
332 delete_cage(self.cage)
335 class VIEW3D_OT_gp_box_deform(bpy.types.Operator):
336 bl_idname = "view3d.gp_box_deform"
337 bl_label = "Box Deform"
338 bl_description = "Use lattice for free box transforms on grease pencil points\
339 \n(Ctrl+T)"
340 bl_options = {"REGISTER", "UNDO"}
342 @classmethod
343 def poll(cls, context):
344 return context.object is not None and context.object.type in ('GPENCIL','LATTICE')
346 # local variable
347 tab_press_ct = 0
349 def modal(self, context, event):
350 display_text = f"Deform Cage size: {self.lat.points_u}x{self.lat.points_v} (1-9 or ctrl + ←→↑↓) | \
351 mode (M) : {'Linear' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'Spline'} | \
352 valid:Spacebar/Enter, cancel:Del/Backspace/Tab/{self.shortcut_ui}"
353 context.area.header_text_set(display_text)
356 ## Handle ctrl+Z
357 if event.type in {'Z'} and event.value == 'PRESS' and event.ctrl:
358 ## Disable (capture key)
359 return {"RUNNING_MODAL"}
360 ## Not found how possible to find modal start point in undo stack to
361 # print('ops list', context.window_manager.operators.keys())
362 # if context.window_manager.operators:#can be empty
363 # print('\nlast name', context.window_manager.operators[-1].name)
365 # Auto interpo check
366 if self.auto_interp:
367 if event.type in {'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO',} and event.value == 'PRESS':
368 self.set_lattice_interp('KEY_BSPLINE')
369 if event.type in {'DOWN_ARROW', "UP_ARROW", "RIGHT_ARROW", "LEFT_ARROW"} and event.value == 'PRESS' and event.ctrl:
370 self.set_lattice_interp('KEY_BSPLINE')
371 if event.type in {'ONE'} and event.value == 'PRESS':
372 self.set_lattice_interp('KEY_LINEAR')
374 # Single keys
375 if event.type in {'H'} and event.value == 'PRESS':
376 # self.report({'INFO'}, "Can't hide")
377 return {"RUNNING_MODAL"}
379 if event.type in {'ONE'} and event.value == 'PRESS':# , 'NUMPAD_1'
380 self.lat.points_u = self.lat.points_v = 2
381 return {"RUNNING_MODAL"}
383 if event.type in {'TWO'} and event.value == 'PRESS':# , 'NUMPAD_2'
384 self.lat.points_u = self.lat.points_v = 3
385 return {"RUNNING_MODAL"}
387 if event.type in {'THREE'} and event.value == 'PRESS':# , 'NUMPAD_3'
388 self.lat.points_u = self.lat.points_v = 4
389 return {"RUNNING_MODAL"}
391 if event.type in {'FOUR'} and event.value == 'PRESS':# , 'NUMPAD_4'
392 self.lat.points_u = self.lat.points_v = 5
393 return {"RUNNING_MODAL"}
395 if event.type in {'FIVE'} and event.value == 'PRESS':# , 'NUMPAD_5'
396 self.lat.points_u = self.lat.points_v = 6
397 return {"RUNNING_MODAL"}
399 if event.type in {'SIX'} and event.value == 'PRESS':# , 'NUMPAD_6'
400 self.lat.points_u = self.lat.points_v = 7
401 return {"RUNNING_MODAL"}
403 if event.type in {'SEVEN'} and event.value == 'PRESS':# , 'NUMPAD_7'
404 self.lat.points_u = self.lat.points_v = 8
405 return {"RUNNING_MODAL"}
407 if event.type in {'EIGHT'} and event.value == 'PRESS':# , 'NUMPAD_8'
408 self.lat.points_u = self.lat.points_v = 9
409 return {"RUNNING_MODAL"}
411 if event.type in {'NINE'} and event.value == 'PRESS':# , 'NUMPAD_9'
412 self.lat.points_u = self.lat.points_v = 10
413 return {"RUNNING_MODAL"}
415 if event.type in {'ZERO'} and event.value == 'PRESS':# , 'NUMPAD_0'
416 self.lat.points_u = 2
417 self.lat.points_v = 1
418 return {"RUNNING_MODAL"}
420 if event.type in {'RIGHT_ARROW'} and event.value == 'PRESS' and event.ctrl:
421 if self.lat.points_u < 20:
422 self.lat.points_u += 1
423 return {"RUNNING_MODAL"}
425 if event.type in {'LEFT_ARROW'} and event.value == 'PRESS' and event.ctrl:
426 if self.lat.points_u > 1:
427 self.lat.points_u -= 1
428 return {"RUNNING_MODAL"}
430 if event.type in {'UP_ARROW'} and event.value == 'PRESS' and event.ctrl:
431 if self.lat.points_v < 20:
432 self.lat.points_v += 1
433 return {"RUNNING_MODAL"}
435 if event.type in {'DOWN_ARROW'} and event.value == 'PRESS' and event.ctrl:
436 if self.lat.points_v > 1:
437 self.lat.points_v -= 1
438 return {"RUNNING_MODAL"}
441 # Change modes
442 if event.type in {'M'} and event.value == 'PRESS':
443 self.auto_interp = False
444 interp = 'KEY_BSPLINE' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'KEY_LINEAR'
445 self.set_lattice_interp(interp)
446 return {"RUNNING_MODAL"}
448 # Valid
449 if event.type in {'RET', 'SPACE', 'NUMPAD_ENTER'}:
450 if event.value == 'PRESS':
451 context.window_manager.boxdeform_running = False
452 self.restore_prefs(context)
453 back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
454 if event.shift:
455 # Let the cage as is with a unique ID
456 store_cage(self, 'lattice_cage_deform_group')
457 else:
458 apply_cage(self.gp_obj, context) # must be in object mode
459 assign_vg(self.gp_obj, 'lattice_cage_deform_group', delete=True)
460 for o in self.other_gp:
461 apply_cage(o, context)
462 assign_vg(o, 'lattice_cage_deform_group', delete=True)
463 delete_cage(self.cage)
465 # back to original mode
466 if self.gp_mode != 'OBJECT':
467 bpy.ops.object.mode_set(mode=self.gp_mode)
468 context.area.header_text_set(None) # Reset header
470 return {'FINISHED'}
472 # Abort ---
473 # One Warning for Tab cancellation.
474 if event.type == 'TAB' and event.value == 'PRESS':
475 self.tab_press_ct += 1
476 if self.tab_press_ct < 2:
477 self.report({'WARNING'}, "Pressing TAB again will Cancel")
478 return {"RUNNING_MODAL"}
481 if all(getattr(event, k) == v for k,v in self.shortcut_d.items()):
482 # Cancel when retyped same shortcut
483 self.cancel(context)
484 return {'CANCELLED'}
486 if event.type in {'DEL', 'BACK_SPACE'} or self.tab_press_ct >= 2:#'ESC',
487 self.cancel(context)
488 return {'CANCELLED'}
490 return {'PASS_THROUGH'}
492 def set_lattice_interp(self, interp):
493 self.lat.interpolation_type_u = self.lat.interpolation_type_v = self.lat.interpolation_type_w = interp
495 def cancel(self, context):
496 context.window_manager.boxdeform_running = False
497 self.restore_prefs(context)
498 back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
499 cancel_cage(self)
500 assign_vg(self.gp_obj, 'lattice_cage_deform_group', delete=True)
501 context.area.header_text_set(None)
502 if self.gp_mode != 'OBJECT':
503 bpy.ops.object.mode_set(mode=self.gp_mode)
505 def store_prefs(self, context):
506 # store_valierables <-< preferences
507 self.use_drag_immediately = context.preferences.inputs.use_drag_immediately
508 self.drag_threshold_mouse = context.preferences.inputs.drag_threshold_mouse
509 self.drag_threshold_tablet = context.preferences.inputs.drag_threshold_tablet
510 self.use_overlays = context.space_data.overlay.show_overlays
511 # maybe store in windows manager to keep around in case of modal revival ?
513 def restore_prefs(self, context):
514 # preferences <-< store_valierables
515 context.preferences.inputs.use_drag_immediately = self.use_drag_immediately
516 context.preferences.inputs.drag_threshold_mouse = self.drag_threshold_mouse
517 context.preferences.inputs.drag_threshold_tablet = self.drag_threshold_tablet
518 context.space_data.overlay.show_overlays = self.use_overlays
520 def set_prefs(self, context):
521 context.preferences.inputs.use_drag_immediately = True
522 context.preferences.inputs.drag_threshold_mouse = 1
523 context.preferences.inputs.drag_threshold_tablet = 3
524 context.space_data.overlay.show_overlays = True
526 def invoke(self, context, event):
527 ## Store cancel shortcut
528 if event.type not in ('LEFTMOUSE', 'RIGHTMOUSE') and event.value == 'PRESS':
529 self.shortcut_d = {'type': event.type, 'value': event.value, 'ctrl': event.ctrl,
530 'shift': event.shift, 'alt': event.alt, 'oskey': event.oskey}
531 else:
532 self.shortcut_d = {'type': 'T', 'value': 'PRESS', 'ctrl': True,
533 'shift': False, 'alt': False, 'oskey': False}
534 self.shortcut_ui = '+'.join([k.title() for k,v in self.shortcut_d.items() if v is True] + [self.shortcut_d['type']])
536 ## Restrict to 3D view
537 if context.area.type != 'VIEW_3D':
538 self.report({'WARNING'}, "View3D not found, cannot run operator")
539 return {'CANCELLED'}
541 if not context.object:#do it in poll ?
542 self.report({'ERROR'}, "No active objects found")
543 return {'CANCELLED'}
545 if context.window_manager.boxdeform_running:
546 return {'CANCELLED'}
548 self.prefs = get_addon_prefs()#get_prefs
549 self.auto_interp = self.prefs.auto_swap_deform_type
550 self.org_lattice_toolset = None
551 ## usability toggles
552 if self.prefs.use_clic_drag:#Store the active tool since we will change it
553 self.org_lattice_toolset = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname# Tweaktoolcode
555 #store (scene properties needed in case of ctrlZ revival)
556 self.store_prefs(context)
557 self.gp_mode = 'EDIT_GPENCIL'
559 # --- special Case of lattice revive modal, just after ctrl+Z back into lattice with modal stopped
560 if context.mode == 'EDIT_LATTICE' and context.object.name == 'lattice_cage_deform' and len(context.object.vertex_groups):
561 self.gp_obj = context.scene.objects.get(context.object.vertex_groups[0].name)
562 if not self.gp_obj:
563 self.report({'ERROR'}, "/!\\ Box Deform : Cannot find object to target")
564 return {'CANCELLED'}
565 if not self.gp_obj.grease_pencil_modifiers.get('tmp_lattice'):
566 self.report({'ERROR'}, "/!\\ No 'tmp_lattice' modifiers on GP object")
567 return {'CANCELLED'}
568 self.cage = context.object
569 self.lat = self.cage.data
570 self.set_prefs(context)
572 if self.prefs.use_clic_drag:
573 bpy.ops.wm.tool_set_by_id(name="builtin.select")
574 context.window_manager.boxdeform_running = True
575 context.window_manager.modal_handler_add(self)
576 return {'RUNNING_MODAL'}
578 if context.object.type != 'GPENCIL':
579 # self.report({'ERROR'}, "Works only on gpencil objects")
580 ## silent return
581 return {'CANCELLED'}
583 if context.mode not in ('EDIT_GPENCIL', 'OBJECT', 'PAINT_GPENCIL'):
584 # self.report({'WARNING'}, "Works only in following GPencil modes: object / edit/ paint")# ERROR
585 ## silent return
586 return {'CANCELLED'}
589 # bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
590 # https://developer.blender.org/D6147 <- undo forget
592 self.gp_obj = context.object
594 self.from_object = context.mode == 'OBJECT'
595 self.all_gps = self.other_gp = []
596 if self.from_object:
597 self.all_gps = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
598 self.other_gp = [o for o in self.all_gps if o is not self.gp_obj]
600 # Clean potential failed previous job (delete tmp lattice)
601 mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
602 if mod:
603 print('Deleted remaining lattice modifiers')
604 self.gp_obj.grease_pencil_modifiers.remove(mod)
606 phantom_obj = context.scene.objects.get('lattice_cage_deform')
607 if phantom_obj:
608 print('Deleted remaining lattice object')
609 delete_cage(phantom_obj)
611 if bpy.app.version < (2,93,0):
612 if [m for m in self.gp_obj.grease_pencil_modifiers if m.type == 'GP_LATTICE']:
613 self.report({'ERROR'}, "Grease pencil object already has a lattice modifier (multi-lattices are enabled in blender 2.93+)")
614 return {'CANCELLED'}
616 self.gp_mode = context.mode # store mode for restore
618 # All good, create lattice and start modal
620 # Create lattice (and switch to lattice edit) ----
621 self.cage = view_cage(self.gp_obj)
622 if isinstance(self.cage, str):#error, cage not created, display error
623 self.report({'ERROR'}, self.cage)
624 return {'CANCELLED'}
626 self.lat = self.cage.data
628 self.set_prefs(context)
629 context.window_manager.boxdeform_running = True
630 context.window_manager.modal_handler_add(self)
631 return {'RUNNING_MODAL'}
633 ## --- KEYMAP
635 addon_keymaps = []
636 def register_keymaps():
637 kc = bpy.context.window_manager.keyconfigs.addon
638 if kc is None:
639 return
641 km = kc.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')
642 kmi = km.keymap_items.new("view3d.gp_box_deform", type ='T', value = "PRESS", ctrl = True)
643 kmi.repeat = False
644 addon_keymaps.append((km, kmi))
646 def unregister_keymaps():
647 for km, kmi in addon_keymaps:
648 km.keymap_items.remove(kmi)
649 addon_keymaps.clear()
651 ### --- REGISTER ---
653 def register():
654 if bpy.app.background:
655 return
656 bpy.types.WindowManager.boxdeform_running = bpy.props.BoolProperty(default=False)
657 bpy.utils.register_class(VIEW3D_OT_gp_box_deform)
658 register_keymaps()
660 def unregister():
661 if bpy.app.background:
662 return
663 unregister_keymaps()
664 bpy.utils.unregister_class(VIEW3D_OT_gp_box_deform)
665 wm = bpy.context.window_manager
666 p = 'boxdeform_running'
667 if p in wm:
668 del wm[p]