8 Commits
Author SHA1 Message Date
pullusb c02b890915 Add Copy material to layer
3.3.0

- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
2024-07-16 17:57:27 +02:00
pullusb 19e26f8cee Fix batch project bug and expose placemnt options
3.2.0

- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
2024-06-04 14:33:31 +02:00
pullusb 01ce06201e move material to layer feature
3.1.0

- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
2024-05-30 18:33:05 +02:00
pullusb 92e53f8368 update urls to gitea repository 2024-03-28 17:10:32 +01:00
pullusb 386be46251 update changelog 3.0.2 2024-03-27 12:10:15 +01:00
pullusb 47b9b68e9e Expose ops copy-move keys to layer in menus
3.0.2

- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
2024-03-27 12:09:57 +01:00
pullusb 810256f5cb fix crash after empty frame creation
3.0.1

- fixed: Crash when drawing directly after generating empty frames
2024-02-22 11:09:26 +01:00
pullusb cf2ba8448a replace bgl with gpu calls update for 4.0
3.0.0

- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2024-02-20 16:07:20 +01:00
13 changed files with 415 additions and 145 deletions
+27
View File
@@ -1,5 +1,32 @@
# Changelog # Changelog
3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
3.2.0
- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
3.1.0
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
3.0.2
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
3.0.1
- fixed: Crash after generating empty frames
3.0.0
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2.5.0 2.5.0
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet) - added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
@@ -166,6 +166,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
gpl.active.frames.new(num, active=False) gpl.active.frames.new(num, active=False)
fct += 1 fct += 1
gpl.update()
if fct: if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}") self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
else: else:
+3 -4
View File
@@ -1,6 +1,5 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
import gpu import gpu
@@ -190,7 +189,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self): def draw_callback_px(self):
bgl.glEnable(bgl.GL_BLEND) gpu.state.blend_set('ALPHA')
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA) #bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1) #bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
@@ -201,7 +200,7 @@ class GPTB_OT_eraser(Operator):
bg_color = area.spaces.active.shading.background_color bg_color = area.spaces.active.shading.background_color
#print(bg_color) #print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.bind() shader.bind()
shader.uniform_float("color", (1, 1, 1, 1)) shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path: for mouse, radius in self.mouse_path:
@@ -210,7 +209,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader) batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24) draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
bgl.glDisable(bgl.GL_BLEND) gpu.state.blend_set('NONE')
+26 -12
View File
@@ -17,20 +17,27 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) : class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer" bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate and send to layer' bl_label = 'Duplicate Send To Layer'
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
# important to have the updated enum here as bl_property # important to have the updated enum here as bl_property
bl_property = "layers_enum" bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty( layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers", name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer", description="Duplicate selected keys in active layer and send them to chosen layer",
items=get_layer_list, items=get_layer_list,
options={'HIDDEN'}, options={'HIDDEN'},
) )
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod
def description(cls, context, properties):
if properties.delete_source:
return f"Move selected keys in active layer to chosen layer"
else:
return f"Copy selected keys in active layer and send to chosen layer"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'\ return context.object and context.object.type == 'GPENCIL'\
@@ -56,16 +63,16 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace) replaced = len(to_replace)
## remove overlapping frames ## Remove overlapping frames
for f in reversed(to_replace): for f in reversed(to_replace):
target_layer.frames.remove(f) target_layer.frames.remove(f)
## copy original frames ## Copy original frames
for f in selected_frames: for f in selected_frames:
target_layer.frames.copy(f) target_layer.frames.copy(f)
sent = len(selected_frames) sent = len(selected_frames)
## delete original frames as an option ## Delete original frames as an option
if self.delete_source: if self.delete_source:
for f in reversed(selected_frames): for f in reversed(selected_frames):
act_layer.frames.remove(f) act_layer.frames.remove(f)
@@ -74,9 +81,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
if replaced: if replaced:
mess += f' ({replaced} replaced)' mess += f' ({replaced} replaced)'
# context.view_layer.update()
# bpy.ops.gpencil.editmode_toggle()
mod = context.mode mod = context.mode
bpy.ops.gpencil.editmode_toggle() bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod) bpy.ops.object.mode_set(mode=mod)
@@ -109,9 +113,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True) kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
@@ -129,6 +130,12 @@ def unregister_keymaps():
addon_keymaps.clear() addon_keymaps.clear()
def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN'
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = ( classes = (
GPTB_OT_duplicate_send_to_layer, GPTB_OT_duplicate_send_to_layer,
) )
@@ -139,12 +146,19 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
register_keymaps() register_keymaps()
bpy.types.DOPESHEET_MT_gpencil_key.append(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
def unregister(): def unregister():
if bpy.app.background: if bpy.app.background:
return return
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_gpencil_key.remove(menu_duplicate_and_send_to_layer)
unregister_keymaps() unregister_keymaps()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
+183
View File
@@ -0,0 +1,183 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from . import utils
# def get_layer_list(self, context):
# '''return (identifier, name, description) of enum content'''
# if not context:
# return [('None', 'None','None')]
# if not context.object:
# return [('None', 'None','None')]
# return [(l.info, l.info, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
## in Class
# bl_property = "layers_enum"
# layers_enum : bpy.props.EnumProperty(
# name="Send Material To Layer",
# description="Send active material to layer",
# items=get_layer_list,
# options={'HIDDEN'},
# )
class GPTB_OT_move_material_to_layer(Operator) :
bl_idname = "gp.move_material_to_layer"
bl_label = 'Move Material To Layer'
bl_description = 'Move active material to an existing or new layer'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
layer_name : bpy.props.StringProperty(
name='Layer Name', default='', options={'SKIP_SAVE'})
copy : bpy.props.BoolProperty(
name='Copy to layer', default=False,
description='Copy strokes to layer instead of moving',
options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
if self.layer_name:
return self.execute(context)
if not len(context.object.data.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
mat = context.object.data.materials[context.object.active_material_index]
self.mat_name = mat.name
# wm.invoke_search_popup(self)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# layout.operator_context = "INVOKE_DEFAULT"
layout.prop(self, 'copy', text='Copy Strokes')
action_label = 'Copy' if self.copy else 'Move'
layout.label(text=f'{action_label} material "{self.mat_name}" to layer:', icon='MATERIAL')
col = layout.column()
col.prop(self, 'layer_name', text='', icon='ADD')
# if self.layer_name:
# col.label(text='Ok/Enter to create new layer', icon='INFO')
col.separator()
for l in reversed(context.object.data.layers):
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
row = col.row()
row.alignment = 'LEFT'
op = col.operator('gp.move_material_to_layer', text=l.info, icon=icon, emboss=False)
op.layer_name = l.info
op.copy = self.copy
def execute(self, context):
if not self.layer_name:
print('Out')
return {'CANCELLED'}
## Active + selection
pool = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
if not context.object in pool:
pool.append(context.object)
mat = context.object.data.materials[context.object.active_material_index]
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
# import time
# t = time.time() # Dbg
total = 0
oct = 0
for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
if mat_index is None:
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
continue
gpl = ob.data.layers
if not (target_layer := gpl.get(self.layer_name)):
target_layer = gpl.new(self.layer_name)
## List existing frames
key_dict = {f.frame_number : f for f in target_layer.frames}
### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0
sct = 0
for l in gpl:
if l == target_layer:
## ! infinite loop if target layer is included
continue
for f in l.frames:
## skip if no stroke has active material
if not next((s for s in f.strokes if s.material_index == mat_index), None):
continue
## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)):
dest_key = target_layer.frames.new(f.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {f.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s in f.strokes:
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
# print('Removing frames') # Dbg
## Remove from source frame (f)
if not self.copy:
for s in reversed(stroke_to_delete):
f.strokes.remove(s)
## ? Remove frame if layer is empty ? -> probably not, will show previous frame
fct += 1
l.frames.update()
if fct:
oct += 1
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
total += fct
report_type = 'INFO' if total else 'WARNING'
if self.copy:
self.report({report_type}, f'Copied {total} frames accross {oct} object(s)')
else:
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
return {'FINISHED'}
# def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GPENCIL':
# self.layout.operator_context = 'INVOKE_REGION_WIN'
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_move_material_to_layer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+56 -34
View File
@@ -15,6 +15,35 @@ def get_scale_matrix(scale):
matscale = matscale_x @ matscale_y @ matscale_z matscale = matscale_x @ matscale_y @ matscale_z
return matscale return matscale
'''
## Old reproject method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT')
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode)
'''
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method '''Reproject - ops method
:all_stroke: affect hided, locked layers :all_stroke: affect hided, locked layers
@@ -72,40 +101,13 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel() # new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
s.points.foreach_set('co', new_local_coords) s.points.foreach_set('co', new_local_coords)
bpy.context.area.tag_redraw()
'''
## Old method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT')
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode)
'''
if restore_frame: if restore_frame:
bpy.context.scene.frame_current = oframe bpy.context.scene.frame_current = oframe
## Update the layer and redraw all viewports
obj.data.layers.update()
utils.refresh_areas()
def align_global(reproject=True, ref=None, all_strokes=True): def align_global(reproject=True, ref=None, all_strokes=True):
@@ -369,14 +371,14 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
name='All Strokes', default=True, name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected') description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type', type : bpy.props.EnumProperty(name='Type',
items=(('CURRENT', "Current", ""), items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""), ('FRONT', "Front", ""),
('SIDE', "Side", ""), ('SIDE', "Side", ""),
('TOP', "Top", ""), ('TOP', "Top", ""),
('VIEW', "View", ""), ('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""), ('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
), ),
default='CURRENT') default='CURRENT')
@@ -390,9 +392,29 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
layout = self.layout layout = self.layout
if not context.region_data.view_perspective == 'CAMERA': if not context.region_data.view_perspective == 'CAMERA':
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') # layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera, not current view', icon='ERROR') layout.label(text='Reprojection is made from camera', icon='ERROR')
layout.prop(self, "all_strokes") layout.prop(self, "all_strokes")
layout.prop(self, "type") layout.prop(self, "type", text='Project Axis')
## Hint show axis
if self.type == 'CURRENT':
## Show as prop
# row = layout.row()
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
# row.enabled = False
orient = {
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
}
box = layout.box()
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context): def execute(self, context):
+1 -1
View File
@@ -2,7 +2,7 @@
Blender addon - Various tool to help with grease pencil in animation productions. Blender addon - Various tool to help with grease pencil in animation productions.
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)** **[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+1 -1
View File
@@ -2,7 +2,7 @@
Blender addon - Boîte à outils de grease pencil pour la production d'animation. Blender addon - Boîte à outils de grease pencil pour la production d'animation.
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)** **[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+7
View File
@@ -58,6 +58,11 @@ class GPTB_PT_sidebar_panel(Panel):
## flip X cam ## flip X cam
# layout.label(text='! Flipped !') # layout.label(text='! Flipped !')
row = col.row(align=True)
row.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
row = col.row(align=True) row = col.row(align=True)
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
@@ -432,6 +437,8 @@ def palette_manager_menu(self, context):
layout.separator() layout.separator()
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
layout.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context): def expose_use_channel_color_pref(self, context):
+6 -4
View File
@@ -4,12 +4,12 @@ bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production", "description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux", "author": "Samuel Bernou, Christophe Seux",
"version": (2, 5, 0), "version": (3, 3, 0),
"blender": (3, 0, 0), "blender": (4, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues", "tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
"category": "3D View", "category": "3D View",
} }
@@ -46,6 +46,7 @@ from . import OP_git_update
from . import OP_layer_namespace from . import OP_layer_namespace
from . import OP_pseudo_tint from . import OP_pseudo_tint
from . import OP_follow_curve from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush # from . import OP_eraser_brush
# from . import TOOL_eraser_brush # from . import TOOL_eraser_brush
from . import handler_draw_cam from . import handler_draw_cam
@@ -803,6 +804,7 @@ addon_modules = (
OP_layer_picker, OP_layer_picker,
OP_layer_nav, OP_layer_nav,
OP_follow_curve, OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush, # OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush # TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam, handler_draw_cam,
-20
View File
@@ -165,7 +165,6 @@ def randomise_points(mat, points, attr, strength) :
setattr(point,attr,value+random*strength) setattr(point,attr,value+random*strength)
def zoom_to_object(cam, resolution, box, margin=0.01) : def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0] min_x= box[0]
max_x= box[1] max_x= box[1]
@@ -216,25 +215,6 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
#print(matrix,resolution) #print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,resolution return modelview_matrix,projection_matrix,frame,resolution
def set_viewport_matrix(width, height, mat):
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
glViewport(0,0,width,height)
#glLoadIdentity()
glMatrixMode(GL_PROJECTION)
projection = [mat[j][i] for i in range(4) for j in range(4)]
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
#glMatrixMode( GL_MODELVIEW )
#glLoadIdentity()
# get object info # get object info
def get_object_info(mesh_groups, order_list = []) : def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene scene = bpy.context.scene
+22 -9
View File
@@ -1,6 +1,5 @@
import bpy import bpy
import gpu import gpu
import bgl
# import blf # import blf
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -30,6 +29,20 @@ def view3d_camera_border_2d(context, cam):
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame] frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px return frame_px
def vertices_to_line_loop(v_list, closed=True) -> list:
'''Take a sequence of vertices
return a position lists of segments to create a line loop passing in all points
the result is usable with gpu_shader 'LINES'
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
'''
loop = []
for i in range(len(v_list) - 1):
loop += [v_list[i], v_list[i + 1]]
if closed:
# Add segment between last and first to close loop
loop += [v_list[-1], v_list[0]]
return loop
def draw_cam_frame_callback_2d(): def draw_cam_frame_callback_2d():
context = bpy.context context = bpy.context
if context.region_data.view_perspective != 'CAMERA': if context.region_data.view_perspective != 'CAMERA':
@@ -41,11 +54,12 @@ def draw_cam_frame_callback_2d():
if not main_cam: if not main_cam:
return return
bgl.glEnable(bgl.GL_BLEND) gpu.state.blend_set('ALPHA')
frame_point = view3d_camera_border_2d( frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent) context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR') shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout: if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT ### PASSEPARTOUT
@@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over ### Camera framing trace over
bgl.glLineWidth(1) gpu.state.line_width_set(1.0)
bgl.glEnable(bgl.GL_LINE_SMOOTH) # bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
""" """
## need to accurately detect viewport background color (difficult) ## need to accurately detect viewport background color (difficult)
@@ -135,15 +149,14 @@ def draw_cam_frame_callback_2d():
frame_color = (0.0, 0.0, 0.25, 1.0) frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader( screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": frame_point}) shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
shader_2d.bind() shader_2d.bind()
shader_2d.uniform_float("color", frame_color) shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d) screen_framing.draw(shader_2d)
# bgl.glLineWidth(1) # bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
bgl.glDisable(bgl.GL_LINE_SMOOTH) gpu.state.blend_set('NONE')
bgl.glDisable(bgl.GL_BLEND)
draw_handle = None draw_handle = None
+81 -59
View File
@@ -2,12 +2,13 @@ import bpy, os
import numpy as np import numpy as np
import bmesh import bmesh
import mathutils import mathutils
from mathutils import Vector
import math import math
from math import sqrt
from sys import platform
import subprocess import subprocess
from math import sqrt
from mathutils import Vector
from sys import platform
""" def get_gp_parent(layer) : """ def get_gp_parent(layer) :
@@ -263,55 +264,6 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### GP funcs ### GP funcs
# ----------------- # -----------------
""" V1
def get_gp_draw_plane(obj=None):
''' return tuple with plane coordinate and normal
of the curent drawing accordign to geometry'''
context = bpy.context
settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
if obj:
mat = obj.matrix_world
else:
mat = context.object.matrix_world if context.object else None
# -> placement
if loc == "CURSOR":
plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
if not context.object:
plane_co = None
else:
plane_co = context.object.matrix_world.to_translation()# context.object.location
# -> orientation
if orient == 'VIEW':
#only depth is important, no need to get view vector
plane_no = None
elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0))
plane_no.rotate(mat)
elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0))
plane_no.rotate(mat)
elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
elif orient == 'CURSOR':
plane_no = Vector((0,0,1))
plane_no.rotate(context.scene.cursor.matrix)
return plane_co, plane_no
"""
## V2
def get_gp_draw_plane(obj=None, orient=None): def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal ''' return tuple with plane coordinate and normal
of the curent drawing according to geometry''' of the curent drawing according to geometry'''
@@ -336,13 +288,13 @@ def get_gp_draw_plane(obj=None, orient=None):
plane_co = bpy.context.scene.cursor.location plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix mat = bpy.context.scene.cursor.matrix
elif orient == 'AXIS_Y':#front (X-Z) elif orient in ('AXIS_Y', 'FRONT'): # front (X-Z)
plane_no = Vector((0,1,0)) plane_no = Vector((0,1,0))
elif orient == 'AXIS_X':#side (Y-Z) elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z)
plane_no = Vector((1,0,0)) plane_no = Vector((1,0,0))
elif orient == 'AXIS_Z':#top (X-Y) elif orient in ('AXIS_Z', 'TOP'): # top (X-Y)
plane_no = Vector((0,0,1)) plane_no = Vector((0,0,1))
plane_no.rotate(mat) plane_no.rotate(mat)
@@ -459,14 +411,14 @@ def get_gp_datas(selection=True):
print('EOL. No active GP object') print('EOL. No active GP object')
return [] return []
def get_gp_layer(gp_data_block,name) : def get_gp_layer(gp_data_block, name) :
gp_layer = gp_data_block.layers.get(name) gp_layer = gp_data_block.layers.get(name)
if not gp_layer : if not gp_layer :
gp_layer = gp_data_block.layers.new(name) gp_layer = gp_data_block.layers.new(name)
return gp_layer return gp_layer
def get_gp_frame(layer,frame_nb = None) : def get_gp_frame(layer, frame_nb=None) :
scene = bpy.context.scene scene = bpy.context.scene
if not frame_nb : if not frame_nb :
frame_nb = scene.frame_current frame_nb = scene.frame_current
@@ -532,9 +484,72 @@ def selected_strokes(frame):
stlist.append(s) stlist.append(s)
return stlist return stlist
from math import sqrt ## Copy stroke to a frame
from mathutils import Vector
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
ns = frame.strokes.new()
## Set strokes attr
stroke_attr = [
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr))
## create points
point_count = len(s.points)
ns.points.add(len(s.points))
## Set points attr
# for p, np in zip(s.points, ns.points):
flat_list = [0.0] * point_count
flat_uv_fill_list = [0.0, 0.0] * point_count
flat_vector_list = [0.0, 0.0, 0.0] * point_count
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count
single_attr = [
'pressure',
'strength',
'uv_factor',
'uv_rotation',
]
for attr in single_attr:
# print(f'transfer point {attr}') # Dbg
s.points.foreach_get(attr, flat_list)
ns.points.foreach_set(attr, flat_list)
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns
# ----------------- # -----------------
### Vector utils 3d ### Vector utils 3d
@@ -838,6 +853,13 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
### UI utils ### UI utils
# ----------------- # -----------------
def refresh_areas():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
area.tag_redraw()
# for area in bpy.context.screen.areas:
# area.tag_redraw()
## kmi draw for addon without delete button ## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout): def draw_kmi(km, kmi, layout):
map_type = kmi.map_type map_type = kmi.map_type