First commit GP addon
This commit is contained in:
parent
77b3f0e83e
commit
d7e2005d22
|
@ -0,0 +1,54 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
bl_info = {
|
||||
"name": "Grease Pencil Tools",
|
||||
"description": "Pack of tools for Grease pencil drawing",
|
||||
"author": "Samuel Bernou",
|
||||
"version": (0, 0, 5),
|
||||
"blender": (2, 83, 0),
|
||||
"location": "sidebar (N) > Grease pencil > Grease pencil",
|
||||
"warning": "",
|
||||
"doc_url": "https://github.com/Pullusb/greasepencil-addon",
|
||||
"tracker_url": "https://github.com/Pullusb/greasepencil-addon/issues",
|
||||
"category": "Object",
|
||||
# "support": "COMMUNITY",
|
||||
}
|
||||
|
||||
|
||||
from . import (prefs,
|
||||
box_deform,
|
||||
line_reshape,
|
||||
ui_panels,
|
||||
)
|
||||
|
||||
def register():
|
||||
prefs.register()
|
||||
box_deform.register()
|
||||
line_reshape.register()
|
||||
ui_panels.register()
|
||||
|
||||
def unregister():
|
||||
ui_panels.unregister()
|
||||
box_deform.unregister()
|
||||
line_reshape.unregister()
|
||||
prefs.unregister()
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -0,0 +1,591 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
'''Based on Box_deform standalone addon - Author: Samuel Bernou'''
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
def get_addon_prefs():
|
||||
import os
|
||||
addon_name = os.path.splitext(__name__)[0]
|
||||
preferences = bpy.context.preferences
|
||||
addon_prefs = preferences.addons[addon_name].preferences
|
||||
return (addon_prefs)
|
||||
|
||||
|
||||
def location_to_region(worldcoords):
|
||||
from bpy_extras import view3d_utils
|
||||
return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords)
|
||||
|
||||
def region_to_location(viewcoords, depthcoords):
|
||||
from bpy_extras import view3d_utils
|
||||
return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords)
|
||||
|
||||
def assign_vg(obj, vg_name):
|
||||
## create vertex group
|
||||
vg = obj.vertex_groups.get(vg_name)
|
||||
if vg:
|
||||
# remove to start clean
|
||||
obj.vertex_groups.remove(vg)
|
||||
vg = obj.vertex_groups.new(name=vg_name)
|
||||
bpy.ops.gpencil.vertex_group_assign()
|
||||
return vg
|
||||
|
||||
def view_cage(obj):
|
||||
prefs = get_addon_prefs()
|
||||
lattice_interp = prefs.default_deform_type
|
||||
|
||||
gp = obj.data
|
||||
gpl = gp.layers
|
||||
|
||||
coords = []
|
||||
initial_mode = bpy.context.mode
|
||||
|
||||
## get points
|
||||
if bpy.context.mode == 'EDIT_GPENCIL':
|
||||
for l in gpl:
|
||||
if l.lock or l.hide or not l.active_frame:#or len(l.frames)
|
||||
continue
|
||||
if gp.use_multiedit:
|
||||
target_frames = [f for f in l.frames if f.select]
|
||||
else:
|
||||
target_frames = [l.active_frame]
|
||||
|
||||
for f in target_frames:
|
||||
for s in f.strokes:
|
||||
if not s.select:
|
||||
continue
|
||||
for p in s.points:
|
||||
if p.select:
|
||||
# get real location
|
||||
coords.append(obj.matrix_world @ p.co)
|
||||
|
||||
elif bpy.context.mode == 'OBJECT':#object mode -> all points
|
||||
for l in gpl:# if l.hide:continue# only visible ? (might break things)
|
||||
if not len(l.frames):
|
||||
continue#skip frameless layer
|
||||
for s in l.active_frame.strokes:
|
||||
for p in s.points:
|
||||
coords.append(obj.matrix_world @ p.co)
|
||||
|
||||
elif bpy.context.mode == 'PAINT_GPENCIL':
|
||||
# get last stroke points coordinated
|
||||
if not gpl.active or not gpl.active.active_frame:
|
||||
return 'No frame to deform'
|
||||
|
||||
if not len(gpl.active.active_frame.strokes):
|
||||
return 'No stroke found to deform'
|
||||
|
||||
paint_id = -1
|
||||
if bpy.context.scene.tool_settings.use_gpencil_draw_onback:
|
||||
paint_id = 0
|
||||
coords = [obj.matrix_world @ p.co for p in gpl.active.active_frame.strokes[paint_id].points]
|
||||
|
||||
else:
|
||||
return 'Wrong mode!'
|
||||
|
||||
if not coords:
|
||||
## maybe silent return instead (need special str code to manage errorless return)
|
||||
return 'No points found!'
|
||||
|
||||
if bpy.context.mode in ('EDIT_GPENCIL', 'PAINT_GPENCIL') and len(coords) < 2:
|
||||
# Dont block object mod
|
||||
return 'Less than two point selected'
|
||||
|
||||
vg_name = 'lattice_cage_deform_group'
|
||||
|
||||
if bpy.context.mode == 'EDIT_GPENCIL':
|
||||
vg = assign_vg(obj, vg_name)
|
||||
|
||||
if bpy.context.mode == 'PAINT_GPENCIL':
|
||||
# points cannot be assign to API yet(ugly and slow workaround but only way)
|
||||
# -> https://developer.blender.org/T56280 so, hop'in'ops !
|
||||
|
||||
# store selection and deselect all
|
||||
plist = []
|
||||
for s in gpl.active.active_frame.strokes:
|
||||
for p in s.points:
|
||||
plist.append([p, p.select])
|
||||
p.select = False
|
||||
|
||||
# select
|
||||
## foreach_set does not update
|
||||
# gpl.active.active_frame.strokes[paint_id].points.foreach_set('select', [True]*len(gpl.active.active_frame.strokes[paint_id].points))
|
||||
for p in gpl.active.active_frame.strokes[paint_id].points:
|
||||
p.select = True
|
||||
|
||||
# assign
|
||||
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||
vg = assign_vg(obj, vg_name)
|
||||
|
||||
# restore
|
||||
for pl in plist:
|
||||
pl[0].select = pl[1]
|
||||
|
||||
|
||||
## View axis Mode ---
|
||||
|
||||
## get view coordinate of all points
|
||||
coords2D = [location_to_region(co) for co in coords]
|
||||
|
||||
# find centroid for depth (or more economic, use obj origin...)
|
||||
centroid = np.mean(coords, axis=0)
|
||||
|
||||
# not a mean ! a mean of extreme ! centroid2d = np.mean(coords2D, axis=0)
|
||||
all_x, all_y = np.array(coords2D)[:, 0], np.array(coords2D)[:, 1]
|
||||
min_x, min_y = np.min(all_x), np.min(all_y)
|
||||
max_x, max_y = np.max(all_x), np.max(all_y)
|
||||
|
||||
width = (max_x - min_x)
|
||||
height = (max_y - min_y)
|
||||
center_x = min_x + (width/2)
|
||||
center_y = min_y + (height/2)
|
||||
|
||||
centroid2d = (center_x,center_y)
|
||||
center = region_to_location(centroid2d, centroid)
|
||||
# bpy.context.scene.cursor.location = center#Dbg
|
||||
|
||||
|
||||
#corner Bottom-left to Bottom-right
|
||||
x0 = region_to_location((min_x, min_y), centroid)
|
||||
x1 = region_to_location((max_x, min_y), centroid)
|
||||
x_worldsize = (x0 - x1).length
|
||||
|
||||
#corner Bottom-left to top-left
|
||||
y0 = region_to_location((min_x, min_y), centroid)
|
||||
y1 = region_to_location((min_x, max_y), centroid)
|
||||
y_worldsize = (y0 - y1).length
|
||||
|
||||
## in case of 3
|
||||
|
||||
lattice_name = 'lattice_cage_deform'
|
||||
# cleaning
|
||||
cage = bpy.data.objects.get(lattice_name)
|
||||
if cage:
|
||||
bpy.data.objects.remove(cage)
|
||||
|
||||
lattice = bpy.data.lattices.get(lattice_name)
|
||||
if lattice:
|
||||
bpy.data.lattices.remove(lattice)
|
||||
|
||||
# create lattice object
|
||||
lattice = bpy.data.lattices.new(lattice_name)
|
||||
cage = bpy.data.objects.new(lattice_name, lattice)
|
||||
cage.show_in_front = True
|
||||
|
||||
## Master (root) collection
|
||||
bpy.context.scene.collection.objects.link(cage)
|
||||
|
||||
# spawn cage and align it to view (Again ! align something to a vector !!! argg)
|
||||
|
||||
r3d = bpy.context.space_data.region_3d
|
||||
viewmat = r3d.view_matrix
|
||||
|
||||
cage.matrix_world = viewmat.inverted()
|
||||
cage.scale = (x_worldsize, y_worldsize, 1)
|
||||
## Z aligned in view direction (need minus X 90 degree to be aligned FRONT)
|
||||
# cage.rotation_euler.x -= radians(90)
|
||||
# cage.scale = (x_worldsize, 1, y_worldsize)
|
||||
cage.location = center
|
||||
|
||||
lattice.points_u = 2
|
||||
lattice.points_v = 2
|
||||
lattice.points_w = 1
|
||||
|
||||
lattice.interpolation_type_u = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
|
||||
lattice.interpolation_type_v = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
|
||||
lattice.interpolation_type_w = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
|
||||
|
||||
mod = obj.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE')
|
||||
|
||||
# move to top if modifiers exists
|
||||
for _ in range(len(obj.grease_pencil_modifiers)):
|
||||
bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
|
||||
|
||||
mod.object = cage
|
||||
|
||||
if initial_mode == 'PAINT_GPENCIL':
|
||||
mod.layer = gpl.active.info
|
||||
|
||||
# note : if initial was Paint, changed to Edit
|
||||
# so vertex attribution is valid even for paint
|
||||
if bpy.context.mode == 'EDIT_GPENCIL':
|
||||
mod.vertex_group = vg.name
|
||||
|
||||
#Go in object mode if not already
|
||||
if bpy.context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Store name of deformed object in case of 'revive modal'
|
||||
cage.vertex_groups.new(name=obj.name)
|
||||
|
||||
## select and make cage active
|
||||
# cage.select_set(True)
|
||||
bpy.context.view_layer.objects.active = cage
|
||||
obj.select_set(False)#deselect GP object
|
||||
bpy.ops.object.mode_set(mode='EDIT')# go in lattice edit mode
|
||||
bpy.ops.lattice.select_all(action='SELECT')# select all points
|
||||
|
||||
if prefs.use_clic_drag:
|
||||
## Eventually change tool mode to tweak for direct point editing (reset after before leaving)
|
||||
bpy.ops.wm.tool_set_by_id(name="builtin.select")# Tweaktoolcode
|
||||
return cage
|
||||
|
||||
|
||||
def back_to_obj(obj, gp_mode, org_lattice_toolset, context):
|
||||
if context.mode == 'EDIT_LATTICE' and org_lattice_toolset:# Tweaktoolcode - restore the active tool used by lattice edit..
|
||||
bpy.ops.wm.tool_set_by_id(name = org_lattice_toolset)# Tweaktoolcode
|
||||
|
||||
# gp object active and selected
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
|
||||
def delete_cage(cage):
|
||||
lattice = cage.data
|
||||
bpy.data.objects.remove(cage)
|
||||
bpy.data.lattices.remove(lattice)
|
||||
|
||||
def apply_cage(gp_obj, cage):
|
||||
mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
|
||||
if mod:
|
||||
bpy.ops.object.gpencil_modifier_apply(apply_as='DATA', modifier=mod.name)
|
||||
else:
|
||||
print('tmp_lattice modifier not found to apply...')
|
||||
|
||||
delete_cage(cage)
|
||||
|
||||
def cancel_cage(gp_obj, cage):
|
||||
#remove modifier
|
||||
mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
|
||||
if mod:
|
||||
gp_obj.grease_pencil_modifiers.remove(mod)
|
||||
else:
|
||||
print('tmp_lattice modifier not found to remove...')
|
||||
|
||||
delete_cage(cage)
|
||||
|
||||
|
||||
class GP_OT_latticeGpDeform(bpy.types.Operator):
|
||||
"""Create a lattice to use as quad corner transform"""
|
||||
bl_idname = "gp.latticedeform"
|
||||
bl_label = "Box Deform"
|
||||
bl_description = "Use lattice for free box transforms on grease pencil points (Ctrl+T)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object is not None and context.object.type in ('GPENCIL','LATTICE')
|
||||
|
||||
# local variable
|
||||
tab_press_ct = 0
|
||||
|
||||
def modal(self, context, event):
|
||||
display_text = f"Deform Cage size: {self.lat.points_u}x{self.lat.points_v} (1-9 or ctrl + ←→↑↓) | \
|
||||
mode (M) : {'Linear' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'Spline'} | \
|
||||
valid:Spacebar/Enter/Tab, cancel:Del/Backspace/Ctrl+T"
|
||||
context.area.header_text_set(display_text)
|
||||
|
||||
|
||||
## Handle ctrl+Z
|
||||
if event.type in {'Z'} and event.value == 'PRESS' and event.ctrl:
|
||||
## Disable (capture key)
|
||||
return {"RUNNING_MODAL"}
|
||||
## Not found how possible to find modal start point in undo stack to
|
||||
# print('ops list', context.window_manager.operators.keys())
|
||||
# if context.window_manager.operators:#can be empty
|
||||
# print('\nlast name', context.window_manager.operators[-1].name)
|
||||
|
||||
# Auto interpo check
|
||||
if self.auto_interp:
|
||||
if event.type in {'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO',} and event.value == 'PRESS':
|
||||
self.set_lattice_interp('KEY_BSPLINE')
|
||||
if event.type in {'DOWN_ARROW', "UP_ARROW", "RIGHT_ARROW", "LEFT_ARROW"} and event.value == 'PRESS' and event.ctrl:
|
||||
self.set_lattice_interp('KEY_BSPLINE')
|
||||
if event.type in {'ONE'} and event.value == 'PRESS':
|
||||
self.set_lattice_interp('KEY_LINEAR')
|
||||
|
||||
# Single keys
|
||||
if event.type in {'H'} and event.value == 'PRESS':
|
||||
# self.report({'INFO'}, "Can't hide")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'ONE'} and event.value == 'PRESS':# , 'NUMPAD_1'
|
||||
self.lat.points_u = self.lat.points_v = 2
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'TWO'} and event.value == 'PRESS':# , 'NUMPAD_2'
|
||||
self.lat.points_u = self.lat.points_v = 3
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'THREE'} and event.value == 'PRESS':# , 'NUMPAD_3'
|
||||
self.lat.points_u = self.lat.points_v = 4
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'FOUR'} and event.value == 'PRESS':# , 'NUMPAD_4'
|
||||
self.lat.points_u = self.lat.points_v = 5
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'FIVE'} and event.value == 'PRESS':# , 'NUMPAD_5'
|
||||
self.lat.points_u = self.lat.points_v = 6
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'SIX'} and event.value == 'PRESS':# , 'NUMPAD_6'
|
||||
self.lat.points_u = self.lat.points_v = 7
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'SEVEN'} and event.value == 'PRESS':# , 'NUMPAD_7'
|
||||
self.lat.points_u = self.lat.points_v = 8
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'EIGHT'} and event.value == 'PRESS':# , 'NUMPAD_8'
|
||||
self.lat.points_u = self.lat.points_v = 9
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'NINE'} and event.value == 'PRESS':# , 'NUMPAD_9'
|
||||
self.lat.points_u = self.lat.points_v = 10
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'ZERO'} and event.value == 'PRESS':# , 'NUMPAD_0'
|
||||
self.lat.points_u = 2
|
||||
self.lat.points_v = 1
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'RIGHT_ARROW'} and event.value == 'PRESS' and event.ctrl:
|
||||
if self.lat.points_u < 20:
|
||||
self.lat.points_u += 1
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'LEFT_ARROW'} and event.value == 'PRESS' and event.ctrl:
|
||||
if self.lat.points_u > 1:
|
||||
self.lat.points_u -= 1
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'UP_ARROW'} and event.value == 'PRESS' and event.ctrl:
|
||||
if self.lat.points_v < 20:
|
||||
self.lat.points_v += 1
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'DOWN_ARROW'} and event.value == 'PRESS' and event.ctrl:
|
||||
if self.lat.points_v > 1:
|
||||
self.lat.points_v -= 1
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
# change modes
|
||||
if event.type in {'M'} and event.value == 'PRESS':
|
||||
self.auto_interp = False
|
||||
interp = 'KEY_BSPLINE' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'KEY_LINEAR'
|
||||
self.set_lattice_interp(interp)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
# Valid
|
||||
if event.type in {'RET', 'SPACE'}:
|
||||
if event.value == 'PRESS':
|
||||
context.window_manager.boxdeform_running = False
|
||||
self.restore_prefs(context)
|
||||
back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
|
||||
apply_cage(self.gp_obj, self.cage)#must be in object mode
|
||||
|
||||
# back to original mode
|
||||
if self.gp_mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode=self.gp_mode)
|
||||
|
||||
context.area.header_text_set(None)#reset header
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Abort ---
|
||||
# One Warning for Tab cancellation.
|
||||
if event.type == 'TAB' and event.value == 'PRESS':
|
||||
self.tab_press_ct += 1
|
||||
if self.tab_press_ct < 2:
|
||||
self.report({'WARNING'}, "Pressing TAB again will Cancel")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type in {'T'} and event.value == 'PRESS' and event.ctrl:# Retyped same shortcut
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type in {'DEL', 'BACK_SPACE'} or self.tab_press_ct >= 2:#'ESC',
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def set_lattice_interp(self, interp):
|
||||
self.lat.interpolation_type_u = self.lat.interpolation_type_v = self.lat.interpolation_type_w = interp
|
||||
|
||||
def cancel(self, context):
|
||||
context.window_manager.boxdeform_running = False
|
||||
self.restore_prefs(context)
|
||||
back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
|
||||
cancel_cage(self.gp_obj, self.cage)
|
||||
context.area.header_text_set(None)
|
||||
if self.gp_mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode=self.gp_mode)
|
||||
|
||||
def store_prefs(self, context):
|
||||
# store_valierables <-< preferences
|
||||
self.use_drag_immediately = context.preferences.inputs.use_drag_immediately
|
||||
self.drag_threshold_mouse = context.preferences.inputs.drag_threshold_mouse
|
||||
self.drag_threshold_tablet = context.preferences.inputs.drag_threshold_tablet
|
||||
self.use_overlays = context.space_data.overlay.show_overlays
|
||||
# maybe store in windows manager to keep around in case of modal revival ?
|
||||
|
||||
def restore_prefs(self, context):
|
||||
# preferences <-< store_valierables
|
||||
context.preferences.inputs.use_drag_immediately = self.use_drag_immediately
|
||||
context.preferences.inputs.drag_threshold_mouse = self.drag_threshold_mouse
|
||||
context.preferences.inputs.drag_threshold_tablet = self.drag_threshold_tablet
|
||||
context.space_data.overlay.show_overlays = self.use_overlays
|
||||
|
||||
def set_prefs(self, context):
|
||||
context.preferences.inputs.use_drag_immediately = True
|
||||
context.preferences.inputs.drag_threshold_mouse = 1
|
||||
context.preferences.inputs.drag_threshold_tablet = 3
|
||||
context.space_data.overlay.show_overlays = True
|
||||
|
||||
def invoke(self, context, event):
|
||||
## Restrict to 3D view
|
||||
if context.area.type != 'VIEW_3D':
|
||||
self.report({'WARNING'}, "View3D not found, cannot run operator")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not context.object:#do it in poll ?
|
||||
self.report({'ERROR'}, "No active objects found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if context.window_manager.boxdeform_running:
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.prefs = get_addon_prefs()#get_prefs
|
||||
self.auto_interp = self.prefs.auto_swap_deform_type
|
||||
self.org_lattice_toolset = None
|
||||
## usability toggles
|
||||
if self.prefs.use_clic_drag:#Store the active tool since we will change it
|
||||
self.org_lattice_toolset = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname# Tweaktoolcode
|
||||
|
||||
#store (scene properties needed in case of ctrlZ revival)
|
||||
self.store_prefs(context)
|
||||
self.gp_mode = 'EDIT_GPENCIL'
|
||||
|
||||
# --- special Case of lattice revive modal, just after ctrl+Z back into lattice with modal stopped
|
||||
if context.mode == 'EDIT_LATTICE' and context.object.name == 'lattice_cage_deform' and len(context.object.vertex_groups):
|
||||
self.gp_obj = context.scene.objects.get(context.object.vertex_groups[0].name)
|
||||
if not self.gp_obj:
|
||||
self.report({'ERROR'}, "/!\\ Box Deform : Cannot find object to target")
|
||||
return {'CANCELLED'}
|
||||
if not self.gp_obj.grease_pencil_modifiers.get('tmp_lattice'):
|
||||
self.report({'ERROR'}, "/!\\ No 'tmp_lattice' modifiers on GP object")
|
||||
return {'CANCELLED'}
|
||||
self.cage = context.object
|
||||
self.lat = self.cage.data
|
||||
self.set_prefs(context)
|
||||
|
||||
if self.prefs.use_clic_drag:
|
||||
bpy.ops.wm.tool_set_by_id(name="builtin.select")
|
||||
context.window_manager.boxdeform_running = True
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
if context.object.type != 'GPENCIL':
|
||||
# self.report({'ERROR'}, "Works only on gpencil objects")
|
||||
## silent return
|
||||
return {'CANCELLED'}
|
||||
|
||||
#paint need VG workaround. object need good shortcut
|
||||
if context.mode not in ('EDIT_GPENCIL', 'OBJECT', 'PAINT_GPENCIL'):
|
||||
# self.report({'WARNING'}, "Works only in following GPencil modes: edit")# ERROR
|
||||
## silent return
|
||||
return {'CANCELLED'}
|
||||
|
||||
# bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
|
||||
# https://developer.blender.org/D6147 <- undo forget
|
||||
|
||||
self.gp_obj = context.object
|
||||
# Clean potential failed previous job (delete tmp lattice)
|
||||
mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
|
||||
if mod:
|
||||
print('Deleted remaining lattice modifiers')
|
||||
self.gp_obj.grease_pencil_modifiers.remove(mod)
|
||||
|
||||
phantom_obj = context.scene.objects.get('lattice_cage_deform')
|
||||
if phantom_obj:
|
||||
print('Deleted remaining lattice object')
|
||||
delete_cage(phantom_obj)
|
||||
|
||||
if [m for m in self.gp_obj.grease_pencil_modifiers if m.type == 'GP_LATTICE']:
|
||||
self.report({'ERROR'}, "Grease pencil object already has a lattice modifier (can only have one)")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
self.gp_mode = context.mode#store mode for restore
|
||||
|
||||
# All good, create lattice and start modal
|
||||
|
||||
# Create lattice (and switch to lattice edit) ----
|
||||
self.cage = view_cage(self.gp_obj)
|
||||
if isinstance(self.cage, str):#error, cage not created, display error
|
||||
self.report({'ERROR'}, self.cage)
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.lat = self.cage.data
|
||||
|
||||
self.set_prefs(context)
|
||||
context.window_manager.boxdeform_running = True
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
## --- KEYMAP
|
||||
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
|
||||
km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')
|
||||
kmi = km.keymap_items.new("gp.latticedeform", type ='T', value = "PRESS", ctrl = True)
|
||||
kmi.repeat = False
|
||||
addon_keymaps.append(km)
|
||||
|
||||
def unregister_keymaps():
|
||||
for km in addon_keymaps:
|
||||
for kmi in km.keymap_items:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
### --- REGISTER ---
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
bpy.types.WindowManager.boxdeform_running = bpy.props.BoolProperty(default=False)
|
||||
bpy.utils.register_class(GP_OT_latticeGpDeform)
|
||||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
unregister_keymaps()
|
||||
bpy.utils.unregister_class(GP_OT_latticeGpDeform)
|
||||
wm = bpy.context.window_manager
|
||||
p = 'boxdeform_running'
|
||||
if p in wm:
|
||||
del wm[p]
|
|
@ -0,0 +1,192 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
'''Based on GP_refine_stroke 0.2.4 - Author: Samuel Bernou'''
|
||||
|
||||
import bpy
|
||||
|
||||
### --- Vector utils
|
||||
|
||||
def mean(*args):
|
||||
'''
|
||||
return mean of all passed value (multiple)
|
||||
If it's a list or tuple return mean of it (only on first list passed).
|
||||
'''
|
||||
if isinstance(args[0], list) or isinstance(args[0], tuple):
|
||||
return mean(*args[0])#send the first list UNPACKED (else infinite recursion as it always evaluate as list)
|
||||
return sum(args) / len(args)
|
||||
|
||||
def vector_len_from_coord(a, b):
|
||||
'''
|
||||
Get two points (that has coordinate 'co' attribute) or Vectors (2D or 3D)
|
||||
Return length as float
|
||||
'''
|
||||
from mathutils import Vector
|
||||
if type(a) is Vector:
|
||||
return (a - b).length
|
||||
else:
|
||||
return (a.co - b.co).length
|
||||
|
||||
def point_from_dist_in_segment_3d(a, b, ratio):
|
||||
'''return the tuple coords of a point on 3D segment ab according to given ratio (some distance divided by total segment lenght)'''
|
||||
## ref:https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point
|
||||
# ratio = dist / seglenght
|
||||
return ( ((1 - ratio) * a[0] + (ratio*b[0])), ((1 - ratio) * a[1] + (ratio*b[1])), ((1 - ratio) * a[2] + (ratio*b[2])) )
|
||||
|
||||
def get_stroke_length(s):
|
||||
'''return 3D total length of the stroke'''
|
||||
all_len = 0.0
|
||||
for i in range(0, len(s.points)-1):
|
||||
#print(vector_len_from_coord(s.points[i],s.points[i+1]))
|
||||
all_len += vector_len_from_coord(s.points[i],s.points[i+1])
|
||||
return (all_len)
|
||||
|
||||
### --- Functions
|
||||
|
||||
def to_straight_line(s, keep_points=True, influence=100, straight_pressure=True):
|
||||
'''
|
||||
keep points : if false only start and end point stay
|
||||
straight_pressure : (not available with keep point) take the mean pressure of all points and apply to stroke.
|
||||
'''
|
||||
|
||||
p_len = len(s.points)
|
||||
if p_len <= 2: # 1 or 2 points only, cancel
|
||||
return
|
||||
|
||||
if not keep_points:
|
||||
if straight_pressure: mean_pressure = mean([p.pressure for p in s.points])#can use a foreach_get but might not be faster.
|
||||
for i in range(p_len-2):
|
||||
s.points.pop(index=1)
|
||||
if straight_pressure:
|
||||
for p in s.points:
|
||||
p.pressure = mean_pressure
|
||||
|
||||
else:
|
||||
A = s.points[0].co
|
||||
B = s.points[-1].co
|
||||
# ab_dist = vector_len_from_coord(A,B)
|
||||
full_dist = get_stroke_length(s)
|
||||
dist_from_start = 0.0
|
||||
coord_list = []
|
||||
|
||||
for i in range(1, p_len-1):#all but first and last
|
||||
dist_from_start += vector_len_from_coord(s.points[i-1],s.points[i])
|
||||
ratio = dist_from_start / full_dist
|
||||
# dont apply directly (change line as we measure it in loop)
|
||||
coord_list.append( point_from_dist_in_segment_3d(A, B, ratio) )
|
||||
|
||||
# apply change
|
||||
for i in range(1, p_len-1):
|
||||
## Direct super straight 100%
|
||||
#s.points[i].co = coord_list[i-1]
|
||||
|
||||
## With influence
|
||||
s.points[i].co = point_from_dist_in_segment_3d(s.points[i].co, coord_list[i-1], influence / 100)
|
||||
|
||||
return
|
||||
|
||||
def get_last_index(context=None):
|
||||
if not context:
|
||||
context = bpy.context
|
||||
return 0 if context.tool_settings.use_gpencil_draw_onback else -1
|
||||
|
||||
### --- OPS
|
||||
|
||||
class GP_OT_straightStroke(bpy.types.Operator):
|
||||
bl_idname = "gp.straight_stroke"
|
||||
bl_label = "Straight Stroke"
|
||||
bl_description = "Make stroke a straight line between first and last point, tweak influence in the redo panel\
|
||||
\nshift+click to reset infuence to 100%"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.object.type == 'GPENCIL'
|
||||
#and context.mode in ('PAINT_GPENCIL', 'EDIT_GPENCIL')
|
||||
|
||||
influence_val : bpy.props.FloatProperty(name="Straight force", description="Straight interpolation percentage",
|
||||
default=100, min=0, max=100, step=2, precision=1, subtype='PERCENTAGE', unit='NONE')
|
||||
|
||||
def execute(self, context):
|
||||
gp = context.object.data
|
||||
gpl = gp.layers
|
||||
if not gpl:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if context.mode == 'PAINT_GPENCIL':
|
||||
if not gpl.active or not gpl.active.active_frame:
|
||||
self.report({'ERROR'}, 'No Grease pencil frame found')
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not len(gpl.active.active_frame.strokes):
|
||||
self.report({'ERROR'}, 'No strokes found.')
|
||||
return {"CANCELLED"}
|
||||
|
||||
s = gpl.active.active_frame.strokes[get_last_index(context)]
|
||||
to_straight_line(s, keep_points=True, influence=self.influence_val)
|
||||
|
||||
elif context.mode == 'EDIT_GPENCIL':
|
||||
ct = 0
|
||||
for l in gpl:
|
||||
if l.lock or l.hide or not l.active_frame:
|
||||
# avoid locked, hided, empty layers
|
||||
continue
|
||||
if gp.use_multiedit:
|
||||
target_frames = [f for f in l.frames if f.select]
|
||||
else:
|
||||
target_frames = [l.active_frame]
|
||||
|
||||
for f in target_frames:
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
ct += 1
|
||||
to_straight_line(s, keep_points=True, influence=self.influence_val)
|
||||
|
||||
if not ct:
|
||||
self.report({'ERROR'}, 'No selected stroke found.')
|
||||
return {"CANCELLED"}
|
||||
|
||||
## filter method
|
||||
# if context.mode == 'PAINT_GPENCIL':
|
||||
# L, F, S = 'ACTIVE', 'ACTIVE', 'LAST'
|
||||
# elif context.mode == 'EDIT_GPENCIL'
|
||||
# L, F, S = 'ALL', 'ACTIVE', 'SELECT'
|
||||
# if gp.use_multiedit: F = 'SELECT'
|
||||
# else : return {"CANCELLED"}
|
||||
# for s in strokelist(t_layer=L, t_frame=F, t_stroke=S):
|
||||
# to_straight_line(s, keep_points=True, influence = self.influence_val)#, straight_pressure=True
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "influence_val")
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.mode not in ('PAINT_GPENCIL', 'EDIT_GPENCIL'):
|
||||
return {"CANCELLED"}
|
||||
if event.shift:
|
||||
self.influence_val = 100
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GP_OT_straightStroke)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(GP_OT_straightStroke)
|
|
@ -0,0 +1,112 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
# IntProperty,
|
||||
)
|
||||
|
||||
|
||||
class GreasePencilAddonPrefs(bpy.types.AddonPreferences):
|
||||
bl_idname = os.path.splitext(__name__)[0]#'greasepencil-addon'#can be called 'master'
|
||||
# bl_idname = __name__
|
||||
|
||||
pref_tabs : EnumProperty(
|
||||
items=(('PREF', "Preferences", "Preferences properties of GP"),
|
||||
('TUTO', "Tutorial", "How to use the tool"),
|
||||
# ('KEYMAP', "Keymap", "customise the default keymap"),
|
||||
),
|
||||
default='PREF')
|
||||
|
||||
# --- props
|
||||
use_clic_drag : BoolProperty(
|
||||
name='Use click drag directly on points',
|
||||
description="Change the active tool to 'tweak' during modal, Allow to direct clic-drag points of the box",
|
||||
default=True)
|
||||
|
||||
default_deform_type : EnumProperty(
|
||||
items=(('KEY_LINEAR', "Linear (perspective mode)", "Linear interpolation, like corner deform / perspective tools of classic 2D", 'IPO_LINEAR',0),
|
||||
('KEY_BSPLINE', "Spline (smooth deform)", "Spline interpolation transformation\nBest when lattice is subdivided", 'IPO_CIRC',1),
|
||||
),
|
||||
name='Starting interpolation', default='KEY_LINEAR', description='Choose default interpolation when entering mode')
|
||||
|
||||
# About interpolation : https://docs.blender.org/manual/en/2.83/animation/shape_keys/shape_keys_panel.html#fig-interpolation-type
|
||||
|
||||
auto_swap_deform_type : BoolProperty(
|
||||
name='Auto swap interpolation mode',
|
||||
description="Automatically set interpolation to 'spline' when subdividing lattice\n Back to 'linear' when",
|
||||
default=True)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# layout.use_property_split = True
|
||||
row= layout.row(align=True)
|
||||
row.prop(self, "pref_tabs", expand=True)
|
||||
|
||||
if self.pref_tabs == 'PREF':
|
||||
layout.label(text='Box deform tool preferences')
|
||||
layout.prop(self, "use_clic_drag")
|
||||
# layout.separator()
|
||||
layout.prop(self, "default_deform_type")
|
||||
layout.label(text="Deformer type can be changed during modal with 'M' key, this is for default behavior", icon='INFO')
|
||||
|
||||
layout.prop(self, "auto_swap_deform_type")
|
||||
layout.label(text="Once 'M' is hit, auto swap is desactivated to stay in your chosen mode", icon='INFO')
|
||||
|
||||
if self.pref_tabs == 'TUTO':
|
||||
|
||||
#**Behavior from context mode**
|
||||
col = layout.column()
|
||||
col.label(text='Box deform tool')
|
||||
col.label(text="Usage:", icon='MOD_LATTICE')
|
||||
col.label(text="Use the shortcut 'Ctrl+T' in available modes (listed below)")
|
||||
col.label(text="The lattice box is generated facing your view (be sure to face canvas if you want to stay on it)")
|
||||
col.label(text="Use shortcuts below to deform (a help will be displayed in the topbar)")
|
||||
|
||||
col.separator()
|
||||
col.label(text="Shortcuts:", icon='HAND')
|
||||
col.label(text="Spacebar / Enter : Confirm")
|
||||
col.label(text="Delete / Backspace / Tab(twice) / Ctrl+T : Cancel")
|
||||
col.label(text="M : Toggle between Linear and Spline mode at any moment")
|
||||
col.label(text="1-9 top row number : Subdivide the box")
|
||||
col.label(text="Ctrl + arrows-keys : Subdivide the box incrementally in individual X/Y axis")
|
||||
|
||||
col.separator()
|
||||
col.label(text="Modes and deformation target:", icon='PIVOT_BOUNDBOX')
|
||||
col.label(text="- Object mode : The whole GP object is deformed (including all frames)")
|
||||
col.label(text="- GPencil Edit mode : Deform Selected points")
|
||||
col.label(text="- Gpencil Paint : Deform last Strokes")
|
||||
# col.label(text="- Lattice edit : Revive the modal after a ctrl+Z")
|
||||
|
||||
col.separator()
|
||||
col.label(text="Notes:", icon='TEXT')
|
||||
col.label(text="- If you return in box deform after applying (with a ctrl+Z), you need to hit 'Ctrl+T' again to revive the modal.")
|
||||
col.label(text="- A cancel warning will be displayed the first time you hit Tab")
|
||||
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GreasePencilAddonPrefs)
|
||||
# Force box deform running to false
|
||||
bpy.context.preferences.addons[os.path.splitext(__name__)[0]].preferences.boxdeform_running = False
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(GreasePencilAddonPrefs)
|
|
@ -0,0 +1,74 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import bpy
|
||||
|
||||
class GP_PT_sidebarPanel(bpy.types.Panel):
|
||||
bl_label = "Grease Pencil tools"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Grease pencil"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
# Box deform ops
|
||||
self.layout.operator_context = 'INVOKE_DEFAULT'
|
||||
layout.operator('gp.latticedeform', icon ="MOD_MESHDEFORM")# MOD_LATTICE, LATTICE_DATA
|
||||
|
||||
# Straight line ops
|
||||
layout.operator('gp.straight_stroke', icon ="CURVE_PATH")# IPO_LINEAR
|
||||
|
||||
|
||||
# Expose Native view operators
|
||||
# if context.scene.camera:
|
||||
row = layout.row(align=True)
|
||||
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS')# FULLSCREEN_EXIT?
|
||||
row.operator('view3d.view_center_camera', text = 'Zoom Fit', icon = 'FULLSCREEN_ENTER')
|
||||
|
||||
|
||||
def menu_boxdeform_entry(self, context):
|
||||
"""Transform shortcut to append in existing menu"""
|
||||
layout = self.layout
|
||||
obj = bpy.context.object
|
||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
||||
if obj and obj.type == 'GPENCIL' and context.mode in {'OBJECT', 'EDIT_GPENCIL', 'PAINT_GPENCIL'}:
|
||||
self.layout.operator_context = 'INVOKE_DEFAULT'
|
||||
layout.operator('gp.latticedeform', text='Box Deform')
|
||||
|
||||
def menu_stroke_entry(self, context):
|
||||
layout = self.layout
|
||||
# Gpencil modes : {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
||||
if context.mode in {'EDIT_GPENCIL', 'PAINT_GPENCIL'}:
|
||||
self.layout.operator_context = 'INVOKE_DEFAULT'
|
||||
layout.operator('gp.straight_stroke', text='Straight Stroke')
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GP_PT_sidebarPanel)
|
||||
## VIEW3D_MT_edit_gpencil.append# Grease pencil menu
|
||||
bpy.types.VIEW3D_MT_transform_object.append(menu_boxdeform_entry)
|
||||
bpy.types.VIEW3D_MT_edit_gpencil_transform.append(menu_boxdeform_entry)
|
||||
bpy.types.VIEW3D_MT_edit_gpencil_stroke.append(menu_stroke_entry)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_MT_transform_object.remove(menu_boxdeform_entry)
|
||||
bpy.types.VIEW3D_MT_edit_gpencil_transform.remove(menu_boxdeform_entry)
|
||||
bpy.types.VIEW3D_MT_edit_gpencil_stroke.remove(menu_stroke_entry)
|
||||
bpy.utils.unregister_class(GP_PT_sidebarPanel)
|
Loading…
Reference in New Issue