31 Commits
Author SHA1 Message Date
Christophe SEUX 2b4c797e7e gpencil_selectmode_edit restore 2021-06-17 18:22:12 +02:00
Christophe SEUX beba17fe2f Fix brush radius 2021-06-17 16:22:44 +02:00
Pullusb 2efbb36ddf check list for file
1.5.1

- fix: eraser brush
- change: check file has custom check list in addon prefs
2021-06-17 16:21:27 +02:00
Christophe SEUX 060fa8ff92 correct pressure 2021-06-17 15:57:39 +02:00
Christophe SEUX 81e21bb428 Remove last erase 2021-06-17 15:44:18 +02:00
Christophe SEUX 9c7184836b Correct some bugs 2021-06-17 15:36:28 +02:00
Christophe SEUX bfcac1615d update chage log 2021-06-16 17:35:35 +02:00
Christophe SEUX 6eabc02ce8 Add Eraser Tool
1.5.0
- feat: Eraser Brush Tool (Need to be enable in the preferences)
2021-06-16 17:35:17 +02:00
Christophe SEUX 3d7a208a50 change comparaison 2021-06-16 11:37:51 +02:00
ChristopheSeux adfd887924 change is with '==' 2021-06-16 11:34:22 +02:00
Pullusb 86139a20fb fix typo 2021-06-14 18:53:15 +02:00
Pullusb 682048af63 brush palette loading feature
1.4.3

- feat: load brushes from blend (behave like a Brush palette)
- ui: add load brushes within tool brush dropdown panel and in the top bar in drawmode
- pref: Set project brushes folder in addon preferences
2021-06-14 17:41:41 +02:00
Pullusb 94e3b7a7ad New material cleaner
1.4.2

- feat: new material cleaner in GP layer menu with 3 options
  - clean material duplication (with sub option to not clear if color settings are different)
  - fuse material slots that have the same materials
  - remove empty slots
2021-06-07 19:07:37 +02:00
Pullusb f10f572bdc little fix in custom passepartout
1.4.1

- fix: custom passepartout size limit when dezooming in camera
2021-06-06 21:30:46 +02:00
Pullusb d8f71e3356 Draw cam passepartout and UI changes 2021-06-05 01:20:35 +02:00
Pullusb 2754ebf89b show main cam frame
1.3.3

- feat: show main cam frame when in draw_cam
2021-05-31 15:36:39 +02:00
Pullusb 4b9cd20891 disable name for cams
1.3.2:

- change: disable manip cam name drawing
- code: add initial support for main cam frame draw within camera view
2021-05-30 23:51:18 +02:00
Pullusb e22b91265b fix kf jump rare refresh problem
1.3.1:

- fix: native refresh error that rarely happen that doesn't completely refresh the scene on keyframe jump.
  - Use a double frame change to ensure refresh.
2021-05-25 11:32:47 +02:00
Pullusb 6cf22b81e8 feat: duplicate send keys
1.3.0:

- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
2021-05-24 17:06:10 +02:00
Pullusb 517eceab76 fix realign return error
1.2.2:

- fix: realign anim return error
2021-05-11 16:00:18 +02:00
Pullusb 9b06aee9af Fix breakdowner error handling
1.1.2:

- fix: Breakdowner initial error check
2021-05-10 18:10:34 +02:00
Pullusb 3594c4812d depth move - UI cahnge and changelog file
1.2.0:

- feat: New depth move operator that handle both perspective and orthographic cam
- feat: Realign, added drawing plane checkbox to autoset to Front after realign
- UI: Reorganised realign panel
- UI: Switched part of the sidebar items to columns intead of basic layout to gain space
- doc: Added changelog file (moved list from readme)
- doc: relative link to changelog and FR_readme in main readme
2021-05-09 01:26:37 +02:00
Pullusb bf85582f28 remap relative off, realign obj
1.1.0:

- Important change : Remap relative is now disabled by default in addon preferences
- feat: Add realign operator in sidebar with reproject as true by default
- UI: Batch reproject all frames is now in menus. Same places as native reproject
2021-05-05 18:33:34 +02:00
Pullusb 492095d333 batch reproject ops
1.0.9:

- feat: Reproject all frames operator
2021-05-04 23:17:19 +02:00
Pullusb 07a44190c9 initial setup for GP realign 2021-05-04 18:18:42 +02:00
Pullusb b193c67358 keyframe jump global UI filter
1.0.8:

- feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified
2021-05-02 23:14:38 +02:00
Pullusb 02ed04cd3d fix: keyframe jump better behavior (skip_save) 2021-05-02 16:00:32 +02:00
Pullusb 801235c760 feat : keyframe jump type filter
1.0.6:

- feat: Keyframe jump filter by type. User can now choose if the shortcut should jump on a specific keyframe type (All by default)
2021-05-02 15:25:37 +02:00
Pullusb 6d43064f0b Select Pasted stroke
1.0.5

- GP copy-paste : Pasted stroke are now selected
2021-04-01 18:20:24 +02:00
Pullusb b9dd1196ca better cam ref UI and clear keyframe ops
1.0.4:

- UI: Better cam ref exposition in Toolbox panel
  - Access to opacity
  - merge activation bool with source type icon
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps)
2021-03-31 01:38:17 +02:00
Pullusb 5e876f360b Add video demo link 2021-03-20 15:30:04 +01:00
19 changed files with 2470 additions and 411 deletions
+341
View File
@@ -0,0 +1,341 @@
# Changelog
1.5.1
- fix: eraser brush
- change: check file has custom check list in addon prefs
1.5.0
- feat: Eraser Brush Tool (Need to be enable in the preferences)
1.4.3
- feat: load brushes from blend
- ui: add load brushes within tool brush dropdown panel and in the top bar in drawmode
- pref: Set project brushes folder in addon preferences
1.4.2
- feat: new material cleaner in GP layer menu with 3 options
- clean material duplication (with sub option to not clear if color settings are different)
- fuse material slots that have the same materials
- remove empty slots
1.4.1
- fix: custom passepartout size limit when dezooming in camera
1.4.0
- feat: Passepartout displayed in main cam (permanent draw handler)
- UI: add Straight stroke operator button in Toolbox if GP tools official addon is on
- UI: placed Toolbox Playblast in a subpanel
- UI: removed Onion skin and Autolock layer checkbox (not often used)
- UI: sent rarely `cursor follow` to bottom of the panel
1.3.3
- feat: show main cam frame when in draw_cam
1.3.2:
- change: disable manip cam name drawing
- code: add initial support for main cam frame draw within camera view
1.3.1:
- fix: native refresh error that rarely happen that doesn't completely refresh the scene on keyframe jump.
- Use a double frame change to ensure refresh.
1.3.0:
- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
1.2.2:
- fix: realign anim return error
1.2.1:
- fix: Breakdowner initial error check
1.2.0:
- feat: New depth move operator that handle both perspective and orthographic cam
- feat: Realign, added drawing plane checkbox to autoset to Front after realign
- UI: Reorganised realign panel
- UI: Switched part of the sidebar items to columns intead of basic layout to gain space
- doc: Added changelog file (moved list from readme)
- doc: relative link to changelog and FR_readme in main readme
1.1.0:
- Important change : Remap relative is now disabled by default in addon preferences
- feat: Add realign operator in sidebar with reproject as true by default
- UI: Batch reproject all frames is now in menus. Same places as native reproject
1.0.9:
- feat: Reproject all frames operator
1.0.8:
- feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified
1.0.7:
- feat: Keyframe jump filter by type. User can now choose if the shortcut should jump on a specific keyframe type (All by default)
1.0.5:
- GP copy-paste : Pasted stroke are now selected (allow to use it to quickly rip/split strokes with cut/paste on the same layer)
1.0.4:
- UI: Better cam ref exposition in Toolbox panel
- Access to opacity
- merge activation bool with source type icon
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps)
1.0.3:
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
1.0.2:
- pref: Added option to disable always remap relative on save in addon-preference
1.0.1:
- fix: copy paste problems
- Get points uv_properties (used for brushed points)
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
1.0.0:
- Compatible with official grease pencil tools
- removed box deform and rotate canvas that existed in other
0.9.3:
- feat: keyframe jump keys are now auto-binded
- UI: added keyframe jump customisation in addon pref
- code: split keyframe jump in a separate file with his new key updater
0.9.2:
- doc: Correct download link (important, bugged the addon install) + update
- code: added tracker url
- updater: remove updater temp file, reset minimum version, turn off verbose mode
0.9.1:
- Public release
- prefs: added fps as part of project settings
- check file use pref fps value (previously used harcoded 24fps value)
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
- update: apply changes in integrated copy-paste from the last version of standalone addon
- doc: Added fully-detailed french readme
0.8.0:
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
- stripped associated properties from properties.py and passed as wm props.
0.7.2:
- fix: Palette importer bug
0.7.0:
- feat: auto create empty frame on color layer
0.6.3:
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
0.6.2:
- feat: colorisation, Option to change stop lines length
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
- Minor refactor for submodule register
0.6.1:
- feat: render objects grouped, one anim render with all ticked object using manual output name
0.6.0:
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
0.5.9:
- feat: render exporter
- Render a selection of GP object isolated from the rest
- added exclusions names for GP object listing
- setup settings and output according to a name
- open render folder
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
- check file: set scene resolution to settings in prefs (default 2048x1080)
0.5.8:
- feat: GP material append on active object from single blend file
0.5.7:
- Added warning message for cursor snapping
0.5.5 - 0.5.6:
- check file: added check for placement an projection mode for Gpencil.
- add a slider to change edit_lines_opacity globally for all GP data at once
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
0.5.4:
- feat: anim manager in his own GP_toolbox submenu:
- button to list disabled anim (allow to quickly check state of the scene)
- disable/enable all fcurve in for GP object or other object separately to paint
- shift clic to target selection only
- check file: added disabled fcurved counter alert with detail in console
0.5.3:
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
- check file option: change select active tool (choice added in addon preferences)
0.5.2:
- Revert back obj_cam operator for following object (native lock view follow only translation)
- Changed method for canvas rotation to more robust rotate axis.
- Add operators on link checker to open containing folder/file of link
- Refactor: file checkers in their own file
0.5.1:
- fix: error when empty material slot on GP object.
- fix: cursor snap on GP canvas when GP is parented
- change: Deleted obj cam (and related set view) operator
- change: blacker note background for playblast (stamp_background)
- feat: Always playblast from main camera (if in draw_cam)
- feat: Handler added to Remap relative on save (pre)
- ops: Check for broken links with porposition to find missing files
- ops: Added basic hardcoded file checker
- Lock main cam
- set scene percentage at 100
- set show slider and sync range
- set fps to 24
0.4.6:
- feat: basic Palette manager with base material check and warning
0.4.5:
- open blender config folder from addon preference
- fix: obj cam parent on selected object
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
- freeview : bug when rotating free viewfrom cardianl views
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
0.4.4:
- feat: added cursor follow handlers and UI toggle
0.4.3:
- change playblast out to 'images' and add playblast as name prefix
0.4.2:
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
- fix: canvas rotate works with parented camera !
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
0.4.1:
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
0.4.0:
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
0.3.8:
- UI: expose onion skin in interface
- UI: expose autolock in interface
- UI : putted tint layers in a submenu
- code: refactor, pushed most of class register in their owner file
- tool: tool to rename current or all grease pencil datablock with different name than container object
0.3.7:
- UI: new interface with tabs for addon preferences
- UI: possible to disable color panel from preference (might be deleted if unusable)
- docs: change readme changelog format and correct doc
0.3.6:
- UI: Stoplines : add a button for quickly set stoplines visibility.
0.3.5:
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
- Fix: Enter key added to valid object-breakdown modal.
0.3.3:
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
0.3.3:
- version 1 beta of gmic colorize
- variant of `screen.gp_keyframe_jump` through keymap seetings
0.3.0:
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
0.2.3:
- add operator to `screen.gp_keyframe_jump`
- add shortcut to rotate canvas
- fix duplicate class
0.2.2:
- separated props resolution_percentage parameter
- playblast options for launching folder and opening folder
0.2.1:
- playblast feature
- Button to go zoom 100% or fit screen
- display scene resolution with res indicator
- Fix reference panel : works with video and display in a box layout.
- close pseudo-color panel by default (plan to move it to Gpencil tab)
0.2.0:
- UI: Toggle camera background images from Toolbox panel
- UI: quick access to passepartout
- Feature: option to use namespace for pseudo color
0.1.5:
- added CGC-auto-updater
0.1.3:
- flip cam x
- inital stage of overlay toggle (need pref/multiple pref)
0.1.2:
- subpanel of GP data (instead of direct append)
- initial commit with GP pseudo color
+16 -9
View File
@@ -211,16 +211,23 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
## cursors ## cursors
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER' ## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
## start checks ## start checks
message = None if context.area.type != 'VIEW_3D':
if context.area.type != 'VIEW_3D':message = 'View3D not found, cannot run operator' self.report({'WARNING'}, 'View3D not found, cannot run operator')
return {'CANCELLED'}
obj = bpy.context.object# better use self.context obj = bpy.context.object# better use self.context
if not obj:message = 'no active object' if not obj:
self.report({'WARNING'}, 'No active object')
return {'CANCELLED'}
anim_data = obj.animation_data anim_data = obj.animation_data
if not anim_data:message = f'no animation data on obj: {obj.name}' if not anim_data:
self.report({'WARNING'}, f'No animation data on obj: {obj.name}')
return {'CANCELLED'}
action = anim_data.action action = anim_data.action
if not action:message = f'no action on animation data of obj: {obj.name}' if not action:
if message: self.report({'WARNING'}, f'No action on animation data of obj: {obj.name}')
self.report({'WARNING'}, message)# ERROR
return {'CANCELLED'} return {'CANCELLED'}
## initiate variable to use ## initiate variable to use
@@ -325,12 +332,12 @@ def unregister_keymaps():
### --- REGISTER --- ### --- REGISTER ---
def register(): def register():
if not bpy.app.background: # if not bpy.app.background:
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim) bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
register_keymaps() register_keymaps()
def unregister(): def unregister():
if not bpy.app.background: # if not bpy.app.background:
unregister_keymaps() unregister_keymaps()
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim) bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)
+77
View File
@@ -0,0 +1,77 @@
import bpy
from bpy_extras.io_utils import ImportHelper
from pathlib import Path
from .utils import get_addon_prefs
def get_brushes(blend_fp):
'''Get all brush from passed blend that aren't already in there (can take Path object)'''
cur_brushes = [b.name for b in bpy.data.brushes]
with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
# load brushes if not already there
data_to.brushes = [b for b in data_from.brushes if not b in cur_brushes]
## force fake user for appended the brushes
for b in data_to.brushes:
print(f'Append Brush: {b.name}')
b.use_fake_user = True
return len(data_to.brushes)
class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
bl_idname = "gp.load_brushes"
bl_label = "Load Brushes"
bl_description = "Load all brushes from chosen blend file in current if brushes aren't already there\nIf a replacement is needed, delete the previous brush before"
#bl_options = {"REGISTER", "INTERNAL"}
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
filename_ext = '.blend'
filter_glob: bpy.props.StringProperty(default='*.blend', options={'HIDDEN'} )
filepath : bpy.props.StringProperty(
name="File Path",
description="File path used for import",
maxlen= 1024)
def execute(self, context):
print(f'Appending brushes from file : {self.filepath}')
bct = get_brushes(self.filepath)
if bct:
self.report({'INFO'}, f'{bct} brushes appended')
else:
self.report({'WARNING'}, 'Brushes are already there (if need to re-import, delete first)')
return {"FINISHED"}
### -- MENU ENTRY --
def load_brush_ui(self, context):
if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
def load_brush_top_bar_ui(self, context):
if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes')
classes = (
GPTB_OT_load_brushes,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(load_brush_ui)
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
def unregister():
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(load_brush_ui)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+10 -4
View File
@@ -316,7 +316,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
#print(stroke_list) #print(stroke_list)
return stroke_list return stroke_list
def add_stroke(s, frame, layer, obj): def add_stroke(s, frame, layer, obj, select=False):
'''add stroke on a given frame, (layer is for parentage setting)''' '''add stroke on a given frame, (layer is for parentage setting)'''
# print(3*'-',s) # print(3*'-',s)
ns = frame.strokes.new() ns = frame.strokes.new()
@@ -343,6 +343,9 @@ def add_stroke(s, frame, layer, obj):
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
else: else:
setattr(ns.points[i], k, v) setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
else: else:
for i, pt in enumerate(s['points']): for i, pt in enumerate(s['points']):
for k, v in pt.items(): for k, v in pt.items():
@@ -351,6 +354,8 @@ def add_stroke(s, frame, layer, obj):
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
else: else:
setattr(ns.points[i], k, v) setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs) ## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
ns.points.update() ns.points.update()
@@ -358,11 +363,12 @@ def add_stroke(s, frame, layer, obj):
## patch pressure 2 ## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list) # ns.points.foreach_set('pressure', pressure_flat_list)
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True): def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
''' '''
add a list of strokes to active frame of given layer add a list of strokes to active frame of given layer
if no layer specified, active layer is used if no layer specified, active layer is used
if use_current_frame is True, a new frame will be created only if needed if use_current_frame is True, a new frame will be created only if needed
if select is True, newly added strokes are selected
''' '''
scene = bpy.context.scene scene = bpy.context.scene
obj = bpy.context.object obj = bpy.context.object
@@ -388,7 +394,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
#or active exists but not aligned scene.current with use_current_frame disabled #or active exists but not aligned scene.current with use_current_frame disabled
target_frame = layer.frames.new(fnum) target_frame = layer.frames.new(fnum)
add_stroke(s, target_frame, layer, obj) add_stroke(s, target_frame, layer, obj, select=select)
''' '''
for s in stroke_data: for s in stroke_data:
add_stroke(s, target_frame) add_stroke(s, target_frame)
@@ -478,7 +484,7 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
print('data loaded', time() - t0) print('data loaded', time() - t0)
add_multiple_strokes(data, use_current_frame=True) add_multiple_strokes(data, use_current_frame=True, select=True)
print('total_time', time() - t0) print('total_time', time() - t0)
return {"FINISHED"} return {"FINISHED"}
+120
View File
@@ -0,0 +1,120 @@
import bpy
from mathutils import Vector
class ODM_OT_depth_move(bpy.types.Operator):
bl_idname = "object.depth_proportional_move"
bl_label = "Depth move"
bl_description = "Move object in the depth from camera POV while retaining same size in framing"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type != 'CAMERA' # and context.scene.camera
def invoke(self, context, event):
self.init_mouse_x = event.mouse_x
self.cam = bpy.context.scene.camera
if not self.cam:
self.report({'ERROR'}, 'No active camera')
return {"CANCELLED"}
self.cam_pos = self.cam.matrix_world.translation
self.mode = 'distance'
self.objects = [o for o in context.selected_objects if o.type != 'CAMERA']
self.init_mats = [o.matrix_world.copy() for o in self.objects]
if self.cam.data.type == 'ORTHO':
context.area.header_text_set(f'Move factor: 0.00')
# distance is view vector based
self.view_vector = Vector((0,0,-1))
self.view_vector.rotate(self.cam.matrix_world)
else:
self.init_vecs = [o.matrix_world.translation - self.cam_pos for o in self.objects]
self.init_dists = [v.length for v in self.init_vecs]
context.area.header_text_set(f'Move factor: 0.00 | Mode: {self.mode} (M to switch)')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if self.mode == 'distance':
factor = 0.1
if event.shift:
factor = 0.01
else:
# Smaller factor for proportional dist
factor = 0.01
if event.shift:
factor = 0.001
if event.type in {'MOUSEMOVE'}:
diff = (event.mouse_x - self.init_mouse_x) * factor
if self.cam.data.type == 'ORTHO':
# just push in view vector direction
context.area.header_text_set(f'Move factor: {diff:.2f}')
for i, obj in enumerate(self.objects):
new_vec = self.init_mats[i].translation + (self.view_vector * diff)
obj.matrix_world.translation = new_vec
else:
# Push from camera point and scale accordingly
context.area.header_text_set(f'Move factor: {diff:.2f} | Mode: {self.mode} (M to switch)')
for i, obj in enumerate(self.objects):
if self.mode == 'distance':
## move with the same length for everyone
new_vec = self.init_vecs[i] + (self.init_vecs[i].normalized() * diff)
else:
## move with proportional factor from individual distance vector to camera
new_vec = self.init_vecs[i] + (self.init_vecs[i] * diff)
obj.matrix_world.translation = self.cam_pos + new_vec
dist_percentage = new_vec.length / self.init_dists[i]
obj.scale = self.init_mats[i].to_scale() * dist_percentage
if event.type in {'M'} and event.value == 'PRESS':
# Switch mode
self.mode = 'distance' if self.mode == 'proportional' else 'proportional'
if event.type in {'LEFTMOUSE'} and event.value == 'PRESS':
context.area.header_text_set(None)
return {"FINISHED"}
if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS':
for i, obj in enumerate(self.objects):
obj.matrix_world = self.init_mats[i]
context.area.header_text_set(None)
return {"CANCELLED"}
return {"RUNNING_MODAL"}
""" # Own standalone panel
class ODM_PT_sudden_depth_panel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_label = "Depth move"
def draw(self, context):
layout = self.layout
row = layout.row()
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
"""
### --- REGISTER ---
classes=(
ODM_OT_depth_move,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+517
View File
@@ -0,0 +1,517 @@
import bpy
from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader
import gpu
from time import time
from mathutils import Vector, Matrix, Euler
from mathutils.kdtree import KDTree
from mathutils.geometry import intersect_line_plane, intersect_line_sphere_2d, intersect_line_line
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d, \
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
from time import time
from math import pi, cos, sin
def get_gp_mat(gp, name, set_active=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
bpy.data.materials.create_gpencil_data(mat)
if mat not in gp.data.materials[:]:
gp.data.materials.append(mat)
mat_index = gp.data.materials[:].index(mat)
if set_active:
gp.active_material_index = mat_index
return mat
def get_gp_frame(gp_layer, frame=None):
if frame is None:
frame = bpy.context.scene.frame_current
gp_frame = next((f for f in gp_layer.frames if f.frame_number==frame), None)
if not gp_frame:
gp_frame = gp_layer.frames.new(frame)
return gp_frame
def get_gp_layer(gp, name=None):
if not name:
return gp.data.layers.active
layer = gp.data.layers.get(name)
if not layer:
layer = gp.data.layers.new(name)
gp.data.layers.active = layer
return layer
def co_2d_to_3d(co, depth=0.1):
area = bpy.context.area
region = bpy.context.region
rv3d = area.spaces.active.region_3d
view_mat = rv3d.view_matrix.inverted()
org = view_mat.to_translation()
depth_3d = view_mat @ Vector((0, 0, -depth))
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
return region_2d_to_location_3d(region, rv3d, co, depth_3d)
#vec = (region_2d_to_origin_3d(region, rv3d, co) - org).normalized()
return org + vec
return org + region_2d_to_vector_3d(region, rv3d , co)
def get_cuts_data(strokes, mouse, radius):
gp = bpy.context.object
area = bpy.context.area
region = bpy.context.region
rv3d = area.spaces.active.region_3d
view_mat = rv3d.view_matrix.inverted()
org = view_mat.to_translation()
mat = gp.matrix_world
cuts_data = []
for s in strokes:
is_polyline = 2<=len(s.points)<=5
if not is_polyline and not s.select:
continue
print('Cut Stroke', s)
for i, p in enumerate(s.points):
if not p.select and not is_polyline:
continue
# Test if the next or previous is unselected
edges = []
if i > 0:
prev_p = s.points[i-1]
if not prev_p.select and not is_polyline:
edges.append((i-1, i))
if i < len(s.points)-1:
next_p = s.points[i+1]
if not next_p.select or is_polyline:
edges.append((i, i+1))
for p1_index, p2_index in edges:
p1 = s.points[p1_index]
p2 = s.points[p2_index]
length_3d = (p2.co-p1.co).length
p1_3d = mat @ p1.co
p2_3d = mat @ p2.co
p1_2d = location_3d_to_region_2d(region, rv3d, p1_3d)
p2_2d = location_3d_to_region_2d(region, rv3d, p2_3d)
if p1_2d is None or p2_2d is None:
continue
length_2d = (p2_2d-p1_2d).length
if length_2d <= 4:
continue
intersects = intersect_line_sphere_2d(p1_2d, p2_2d, mouse, radius+2)
intersects = [i for i in intersects if i is not None]
if not intersects:
continue
close_points = [(p1_2d-i).length < 1 or (p2_2d-i).length < 1 for i in intersects]
if any(close_points):
#print('close_points', close_points)
continue
print('intersects', intersects)
line_intersects = []
for i_2d in intersects:
#factor = ((i_2d-p1_2d).length) / length_2d
#factor_3d = factor_2d * length_3d
#vec = region_2d_to_vector_3d(region, rv3d, i_2d)
#p3_3d = region_2d_to_location_3d(region, rv3d, i_2d, org)
#p4_3d = region_2d_to_origin_3d(region, rv3d, i_2d)
p3_3d = co_2d_to_3d(i_2d, 0.1)
p4_3d = co_2d_to_3d(i_2d, 1000)
#bpy.context.scene.cursor.location = p4_3d
line_intersect = intersect_line_line(p1_3d, p2_3d, p3_3d, p4_3d)
if not line_intersect:
continue
i1_3d, _ = line_intersect
line_intersects += [i1_3d]
#context.scene.cursor.location = i1_3d
print('line_intersects', line_intersects)
if line_intersects:
line_intersects.sort(key=lambda x : (x-p1_3d).length)
#cut_data[-1].sort(key=lambda x : (x-p1_3d).length)
cut_data = [p1_index, p2_index, s, line_intersects]
cuts_data.append(cut_data)
return cuts_data
def circle(x, y, radius, segments):
coords = []
m = (1.0 / (segments - 1)) * (pi * 2)
for p in range(segments):
p1 = x + cos(m * p) * radius
p2 = y + sin(m * p) * radius
coords.append((p1, p2))
return coords
class GPTB_OT_eraser(Operator):
"""Draw a line with the mouse"""
bl_idname = "gp.eraser"
bl_label = "Eraser Brush"
bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self):
bgl.glEnable(bgl.GL_BLEND)
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
area = bpy.context.area
#region = bpy.context.region
#rv3d = area.spaces.active.region_3d
bg_color = area.spaces.active.shading.background_color
#print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader.bind()
shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path:
circle_co = circle(*mouse, radius, 24)
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": circle_co})
batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
bgl.glDisable(bgl.GL_BLEND)
'''
def draw_holdout(self, context, event):
gp = context.object
mat_inv = gp.matrix_world.inverted()
mouse_3d = co_2d_to_3d(self.mouse)
radius_3d = co_2d_to_3d(self.mouse + Vector((self.radius, 0)))
search_radius = (radius_3d-mouse_3d).length
#print('search_radius', search_radius)
#print('radius', self.radius)
#bpy.context.scene.cursor.location = mouse_3d
for gp_frame, hld_stroke in self.hld_strokes:
#print('Add Point')
hld_stroke.points.add(count=1)
p = hld_stroke.points[-1]
p.co = mat_inv @ mouse_3d
p.pressure = search_radius * 2000
#context.scene.cursor.location = mouse_3d
'''
def get_radius(self, context, event):
pressure = event.pressure or 1
return context.scene.gptoolprops.eraser_radius * pressure
def erase(self, context, event):
gp = context.object
mat_inv = gp.matrix_world.inverted()
new_points = []
#print(self.cuts_data)
# for f in self.gp_frames:
# for s in [s for s in f.strokes if s.material_index==self.hld_index]:
# f.strokes.remove(s)
#gp.data.materials.pop(index=self.hld_index)
#bpy.data.materials.remove(self.hld_mat)
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
#bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
#for cut_data in self.cuts_data:
# print(cut_data, len(cut_data))
t0 = time()
print()
print('Number of cuts', len(self.mouse_path))
for mouse, radius in self.mouse_path:
t1 = time()
print()
x, y = mouse
bpy.ops.gpencil.select_all(action='DESELECT')
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
strokes = [s for f in self.gp_frames for s in f.strokes]
#print('select_circle', time()-t1)
t2 = time()
cut_data = get_cuts_data(strokes, mouse, radius)
#print('get_cuts_data', time()-t2)
#print([s for s in strokes if s.select])
print('cut_data', cut_data)
t3 = time()
for p1_index, p2_index, stroke, intersects in cut_data[::-1]:
bpy.ops.gpencil.select_all(action='DESELECT')
#print('p1_index', p1_index)
#print('p2_index', p2_index)
p1 = stroke.points[p1_index]
p2 = stroke.points[p2_index]
p1.select = True
p2.select = True
number_cuts = len(intersects)
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
new_p1 = stroke.points[p1_index+1]
new_p1.co = mat_inv@intersects[0]
new_points += [(stroke, p1_index+1)]
#print('number_cuts', number_cuts)
if number_cuts == 2:
new_p2 = stroke.points[p1_index+2]
new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
#new_points += [new_p2]
new_p3 = stroke.points[p1_index+3]
new_p3.co = mat_inv@intersects[1]
new_points += [(stroke, p1_index+3)]
#print('subdivide', time() - t3)
bpy.ops.gpencil.select_all(action='DESELECT')
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
'''
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
bpy.ops.gpencil.select_less()
for p in tip_points:
p.select = True
for stroke, index in new_points:
stroke.points[index].select = False
'''
t4 = time()
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
if selected_strokes:
bpy.ops.gpencil.delete(type='POINTS')
print('remove points', time()- t4)
#print('Total one cut', time()-t1)
#print('Total all cuts', time()-t0)
#bpy.ops.gpencil.select_less()
#for stroke, index in new_points:
# stroke.points[index].select = False
#bpy.ops.object.mode_set(mode='OBJECT')
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
#selected_strokes = [s for s in self.gp_frame.strokes if s.select]
#bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
def modal(self, context, event):
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
self.radius = self.get_radius(context, event)
context.area.tag_redraw()
if event.type == 'LEFTMOUSE':
#self.mouse = mouse
#self.mouse_path.append((self.mouse, self.radius))
self.erase(context, event)
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'FINISHED'}
if (self.mouse-self.mouse_prev).length < max(self.radius/1.33, 2):
return {'RUNNING_MODAL'}
self.mouse_prev = self.mouse
if event.type in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:
#self.draw_holdout(context, event)
self.mouse_path.append((self.mouse, self.radius))
#self.update_cuts_data(context, event)
#self.erase(context, event)
return {'RUNNING_MODAL'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
gp = context.object
matrix = gp.matrix_world
self.gpencil_selectmode_edit = context.scene.tool_settings.gpencil_selectmode_edit
self.radius = self.get_radius(context, event)
self.mouse_prev = self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
self.mouse_path = [(self.mouse_prev, self.radius)]
area = context.area
region = context.region
w, h = region.width, region.height
rv3d = area.spaces.active.region_3d
view_mat = rv3d.view_matrix.inverted()
org = self.org = view_mat.to_translation()
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
#print('ORG', org)
#print('view_mat', view_mat)
self.cuts_data = []
#org = self.view_mat @ Vector((0, 0, -10))
#self.plane_no = self.plane_co-self.org
#bottom_left = region_2d_to_location_3d(region, rv3d , (0, 0), self.plane_co)
#bottom_right = region_2d_to_location_3d(region, rv3d , (0, w), self.plane_co)
#bottom_left = intersect_line_plane(self.org, bottom_left, self.plane_co, self.plane_no)
#bottom_right = intersect_line_plane(self.org, bottom_right, self.plane_co, self.plane_no)
#self.scale_fac = (bottom_right-bottom_left).length / w
#print('scale_fac', self.scale_fac)
#depth_location = view_mat @ Vector((0, 0, -1))
#context.scene.cursor.location = depth_location
#plane_2d = [(0, 0), (0, h), (w, h), (w, h)]
#plane_3d = [region_2d_to_location_3d(p)]
t0 = time()
gp_mats = gp.data.materials
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
self.gp_frames = [l.active_frame for l in gp_layers]
'''
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
print('get_gp_points', time()-t0)
t0 = time()
#points_data = [(s, f, m, p, get_screen_co(p.co, matrix)) for s, f, m in points_data for p in reversed(s.points)]
points_data = [(s, f, m, p, org + ((matrix @ p.co)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
print('points_to_2d', time()-t0)
#print(points_data)
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
#for s, f, m, p, co in self.points_data:
# p.co = co
t0 = time()
self.kd_tree = KDTree(len(self.points_data))
for i, point_data in enumerate(self.points_data):
s, f, m, p, co = point_data
self.kd_tree.insert(co, i)
self.kd_tree.balance()
print('create kdtree', time()-t0)
'''
'''
# Create holdout mat
self.hld_mat = get_gp_mat(gp, name='Eraser Holdout Stroke')
self.hld_mat.grease_pencil.use_stroke_holdout = True
self.hld_mat.grease_pencil.show_stroke = True
self.hld_mat.grease_pencil.show_fill = False
self.hld_mat.grease_pencil.use_overlap_strokes = True
self.hld_index = gp_mats[:].index(self.hld_mat)
self.hld_strokes = []
for f in self.gp_frames:
hld_stroke = f.strokes.new()
hld_stroke.start_cap_mode = 'ROUND'
hld_stroke.end_cap_mode = 'ROUND'
hld_stroke.material_index = self.hld_index
#hld_stroke.line_width = self.radius
self.hld_strokes.append((f, hld_stroke))
self.draw_holdout(context, event)
'''
context.area.tag_redraw()
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
### --- REGISTER ---
classes=(
GPTB_OT_eraser,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
Regular → Executable
+48 -19
View File
@@ -27,10 +27,12 @@ class GPTB_OT_file_checker(bpy.types.Operator):
def execute(self, context): def execute(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
fix = prefs.fixprops
problems = [] problems = []
## Lock main cam: ## Lock main cam:
if not 'layout' in Path(bpy.data.filepath).stem:#dont touch layout cameras if fix.lock_main_cam:
if not 'layout' in Path(bpy.data.filepath).stem.lower():#dont touch layout cameras
if context.scene.camera: if context.scene.camera:
cam = context.scene.camera cam = context.scene.camera
if cam.name == 'draw_cam' and cam.parent: if cam.name == 'draw_cam' and cam.parent:
@@ -45,6 +47,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
cam.lock_location = cam.lock_rotation = triple cam.lock_location = cam.lock_rotation = triple
## set scene res at pref res according to addon pref ## set scene res at pref res according to addon pref
if fix.set_scene_res:
rx, ry = prefs.render_res_x, prefs.render_res_y rx, ry = prefs.render_res_x, prefs.render_res_y
# TODO set (rx, ry) to camera resolution if specified in camera name # TODO set (rx, ry) to camera resolution if specified in camera name
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry: if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
@@ -52,11 +55,19 @@ class GPTB_OT_file_checker(bpy.types.Operator):
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
## set scene percentage at 100: ## set scene percentage at 100:
if fix.set_res_percentage:
if context.scene.render.resolution_percentage != 100: if context.scene.render.resolution_percentage != 100:
problems.append('Resolution output to 100%') problems.append('Resolution output to 100%')
context.scene.render.resolution_percentage = 100 context.scene.render.resolution_percentage = 100
## set fps according to preferences settings
if fix.set_fps:
if context.scene.render.fps != prefs.fps:
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
context.scene.render.fps = prefs.fps
## set show slider and sync range ## set show slider and sync range
if fix.set_slider_n_sync:
for window in bpy.context.window_manager.windows: for window in bpy.context.window_manager.windows:
screen = window.screen screen = window.screen
for area in screen.areas: for area in screen.areas:
@@ -67,12 +78,8 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if hasattr(area.spaces[0], 'show_locked_time'): if hasattr(area.spaces[0], 'show_locked_time'):
setattr(area.spaces[0], 'show_locked_time', True) setattr(area.spaces[0], 'show_locked_time', True)
## set fps according to preferences settings
if context.scene.render.fps != prefs.fps:
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
context.scene.render.fps = prefs.fps
## set cursor type (according to prefs ?) ## set cursor type (according to prefs ?)
if fix.set_cursor_type:
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"): if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = prefs.select_active_tool tool = prefs.select_active_tool
if tool != 'none': if tool != 'none':
@@ -80,19 +87,24 @@ class GPTB_OT_file_checker(bpy.types.Operator):
bpy.ops.wm.tool_set_by_id(name=tool)# Tweaktoolcode bpy.ops.wm.tool_set_by_id(name=tool)# Tweaktoolcode
problems.append(f'tool changed to {tool.split(".")[1]}') problems.append(f'tool changed to {tool.split(".")[1]}')
## GP use additive drawing (else creating a frame in dopesheet makes it blank...) # ## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
if not context.scene.tool_settings.use_gpencil_draw_additive: # if not context.scene.tool_settings.use_gpencil_draw_additive:
problems.append(f'Activated Gp additive drawing mode (snowflake)') # problems.append(f'Activated Gp additive drawing mode (snowflake)')
context.scene.tool_settings.use_gpencil_draw_additive = True # context.scene.tool_settings.use_gpencil_draw_additive = True
## GP stroke placement/projection check ## GP stroke placement/projection check
if fix.check_front_axis:
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y': if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)') problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
if fix.check_placement:
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN': if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)') problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
## Disabled animation ## Disabled animation
if fix.list_disabled_anim:
fcu_ct = 0 fcu_ct = 0
for act in bpy.data.actions: for act in bpy.data.actions:
if not act.users: if not act.users:
@@ -104,15 +116,32 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fcu_ct: if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details -> console)') problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
## Set onion skin filter to 'All type' ## Use median point
fix_kf_type = 0 if fix.set_pivot_median_point:
for gp in bpy.data.grease_pencils:#from data if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
if not gp.is_annotation: problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
if gp.onion_keyframe_type != 'ALL': context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
gp.onion_keyframe_type = 'ALL'
fix_kf_type += 1 if fix.disable_guide:
if fix_kf_type: if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'") problems.append(f"Disabled Draw Guide")
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
if fix.autokey_add_n_replace:
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
problems.append(f"Autokey mode reset to 'Add & Replace'")
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
# ## Set onion skin filter to 'All type'
# fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data
# if not gp.is_annotation:
# if gp.onion_keyframe_type != 'ALL':
# gp.onion_keyframe_type = 'ALL'
# fix_kf_type += 1
# if fix_kf_type:
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object # for ob in context.scene.objects:#from object
# if ob.type == 'GPENCIL': # if ob.type == 'GPENCIL':
# ob.data.onion_keyframe_type = 'ALL' # ob.data.onion_keyframe_type = 'ALL'
+41 -4
View File
@@ -153,10 +153,11 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
# get main cam and error if not available # get main cam and error if not available
if drawcam.name == 'draw_cam': if drawcam.name == 'draw_cam':
maincam = drawcam.parent maincam = drawcam.parent
maincam.data.show_passepartout = context.scene.gptoolprops.drawcam_passepartout
else: else:
maincam = None maincam = None
main_name = drawcam.get('maincam_name')# Custom prop with previous avtive cam. main_name = drawcam.get('maincam_name')# Custom prop with previous active cam.
if main_name: if main_name:
maincam = context.scene.objects.get(main_name) maincam = context.scene.objects.get(main_name)
@@ -187,14 +188,17 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
if not drawcam: if not drawcam:
created=True created=True
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data) drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
drawcam.show_name = True
set_collection(drawcam, 'manip_cams') set_collection(drawcam, 'manip_cams')
if dcam_name == 'draw_cam': if dcam_name == 'draw_cam':
drawcam.parent = maincam drawcam.parent = maincam
if created:#set to main at creation time if created: # set to main at creation time
drawcam.matrix_world = maincam.matrix_world drawcam.matrix_world = maincam.matrix_world
drawcam.lock_location = (True,True,True) drawcam.lock_location = (True,True,True)
# drawcam.hide_viewport = True
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout
maincam.data.show_passepartout = False
else: else:
if created: if created:
@@ -226,7 +230,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
drawcam.hide_viewport = False drawcam.hide_viewport = False
maincam.hide_viewport = True maincam.hide_viewport = True
if created and drawcam.name == 'obj_cam':#Go in camera view if created and drawcam.name == 'obj_cam': # Go in camera view
context.region_data.view_perspective = 'CAMERA' context.region_data.view_perspective = 'CAMERA'
# ## make active # ## make active
# bpy.context.view_layer.objects.active = ob # bpy.context.view_layer.objects.active = ob
@@ -449,6 +453,38 @@ class GPTB_OT_overlay_presets(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class GPTB_OT_clear_active_frame(bpy.types.Operator):
bl_idname = "gp.clear_active_frame"
bl_label = "Clear Active Frame"
bl_description = "Delete all strokes in active frames"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
obj = context.object
l = obj.data.layers.active
if not l:
self.report({'ERROR'}, 'No layers')
return {'CANCELLED'}
f = l.active_frame
if not f:
self.report({'ERROR'}, 'No active frame')
return {'CANCELLED'}
ct = len(f.strokes)
if not ct:
self.report({'ERROR'}, 'Active frame already empty')
return {'CANCELLED'}
for s in reversed(f.strokes):
f.strokes.remove(s)
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
return {'FINISHED'}
classes = ( classes = (
GPTB_OT_copy_text, GPTB_OT_copy_text,
GPTB_OT_flipx_view, GPTB_OT_flipx_view,
@@ -458,6 +494,7 @@ GPTB_OT_set_view_as_cam,
GPTB_OT_reset_cam_rot, GPTB_OT_reset_cam_rot,
GPTB_OT_toggle_mute_animation, GPTB_OT_toggle_mute_animation,
GPTB_OT_list_disabled_anims, GPTB_OT_list_disabled_anims,
GPTB_OT_clear_active_frame,
) )
def register(): def register():
+139
View File
@@ -0,0 +1,139 @@
import bpy
from bpy.types import Operator
def get_layer_list(self, context):
'''return (identifier, name, description) of enum content'''
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
# try:
# except:
# return [("", "", "")]
# return [(i, basename(i), "") for i in blends]
# return [(i.path, basename(i.path), "") for i in self.blends]
class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate and send to layer'
# important to have the updated enum here as bl_property
bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer",
items=get_layer_list
)
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' and context.space_data.ui_mode == 'GPENCIL'
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
def execute(self, context):
target_layer = self.layers_enum
if not target_layer:
self.report({'WARNING'}, 'Target layer not specified')
return {'CANCELLED'}
gpl = context.object.data.layers
target_layer = gpl.get(target_layer)
act_layer = gpl.active
selected_frames = [f for f in act_layer.frames if f.select]
act_frame_num = [f.frame_number for f in act_layer.frames if f.select]
to_replace = [f for f in target_layer.frames if f.frame_number in act_frame_num]
replaced = len(to_replace)
## remove overlapping frames
for f in reversed(to_replace):
target_layer.frames.remove(f)
## copy original frames
for f in selected_frames:
target_layer.frames.copy(f)
sent = len(selected_frames)
## delete original frames as an option
if self.delete_source:
for f in reversed(selected_frames):
act_layer.frames.remove(f)
mess = f'{sent} keys copied'
if replaced:
mess += f' ({replaced} replaced)'
# context.view_layer.update()
# bpy.ops.gpencil.editmode_toggle()
mod = context.mode
bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod)
self.report({'INFO'}, mess)
return {'FINISHED'}
def invoke(self, context, event):
gp = context.object.data
if not len(gp.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
active = gp.layers.active
if not active:
self.report({'WARNING'}, 'No active layer to take keys from')
return {'CANCELLED'}
self.selected_frames = [f for f in active.frames if f.select]
if not self.selected_frames:
self.report({'WARNING'}, 'No selected keys in active layer')
return {'CANCELLED'}
wm = context.window_manager
wm.invoke_search_popup(self) # can't specify size... width=500, height=600
return {'FINISHED'}
addon_keymaps = []
def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "Screen", space_type = "EMPTY")
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", alt=False, ctrl=True, shift=True, any=False)
addon_keymaps.append((km,kmi))
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", alt=False, ctrl=True, shift=True, any=False)
kmi.properties.delete_source = True
addon_keymaps.append((km,kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
classes = (
GPTB_OT_duplicate_send_to_layer,
)
def register():
if not bpy.app.background:
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
def unregister():
if not bpy.app.background:
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+36 -9
View File
@@ -1,5 +1,6 @@
import bpy import bpy
from .utils import get_addon_prefs from .utils import get_addon_prefs
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
bl_idname = "screen.gp_keyframe_jump" bl_idname = "screen.gp_keyframe_jump"
@@ -11,17 +12,31 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
next : bpy.props.BoolProperty( next : BoolProperty(
name="Next GP keyframe", description="Go to next active GP keyframe", default=True) name="Next GP keyframe", description="Go to next active GP keyframe",
default=True, options={'HIDDEN', 'SKIP_SAVE'})
target : bpy.props.EnumProperty( target : EnumProperty(
name="Target layer", description="Choose wich layer to evaluate for keyframe change", default='ACTIVE',# options={'ANIMATABLE'}, update=None, get=None, set=None, name="Target layer", description="Choose wich layer to evaluate for keyframe change",
default='ACTIVE', options={'HIDDEN', 'SKIP_SAVE'},
items=( items=(
('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0), ('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0),
('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1), ('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1),
('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2), ('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2),
)) ))
#(key, label, descr, id[, icon])
keyframe_type : EnumProperty(
name="Keyframe Filter", description="Jump to choosen keyframe type, else use the UI jump filter",
default='NONE', options={'HIDDEN', 'SKIP_SAVE'},
items=(
('NONE', 'Use UI Filter', '', 0), # 'KEYFRAME'
('ALL', 'All', '', 1),
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 2),
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 3),
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
))
def execute(self, context): def execute(self, context):
if not context.object.data.layers.active: if not context.object.data.layers.active:
@@ -39,6 +54,11 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
elif self.target == 'ACCESSIBLE': elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock] gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
if self.keyframe_type != 'NONE':
# use shortcut choice override
kftype = self.keyframe_type
else:
kftype = context.scene.gptoolprops.keyframe_type
current = context.scene.frame_current current = context.scene.frame_current
p = n = None p = n = None
@@ -47,6 +67,10 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
maxs = [] maxs = []
for l in gpl: for l in gpl:
for f in l.frames: for f in l.frames:
# keyframe type filter
if kftype != 'ALL' and f.keyframe_type != kftype:
continue
if f.frame_number < current: if f.frame_number < current:
p = f.frame_number p = f.frame_number
if f.frame_number > current: if f.frame_number > current:
@@ -64,10 +88,13 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
if maxs: if maxs:
n = min(maxs) n = min(maxs)
## Double the frame set to avoid refresh problem (had one in 2.91.2)
if self.next and n is not None: if self.next and n is not None:
context.scene.frame_set(n) context.scene.frame_set(n)
context.scene.frame_current = n
elif not self.next and p is not None: elif not self.next and p is not None:
context.scene.frame_set(p) context.scene.frame_set(p)
context.scene.frame_current = p
else: else:
self.report({'INFO'}, 'No keyframe in this direction') self.report({'INFO'}, 'No keyframe in this direction')
return {"CANCELLED"} return {"CANCELLED"}
@@ -80,10 +107,10 @@ class KFJ_OT_rebinder(bpy.types.Operator):
bl_label = "rebind keyframe jump shortcut" bl_label = "rebind keyframe jump shortcut"
bl_options = {'REGISTER', 'INTERNAL'} bl_options = {'REGISTER', 'INTERNAL'}
s_keycode: bpy.props.StringProperty() s_keycode: StringProperty()
s_ctrl: bpy.props.StringProperty() s_ctrl: StringProperty()
s_shift: bpy.props.StringProperty() s_shift: StringProperty()
s_alt: bpy.props.StringProperty() s_alt: StringProperty()
def invoke(self, context, event): def invoke(self, context, event):
+180
View File
@@ -282,12 +282,192 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class GPTB_OT_clean_material_stack(bpy.types.Operator):
bl_idname = "gp.clean_material_stack"
bl_label = "Clean Material Stack"
bl_description = "Clean materials duplication in active GP object stack"
bl_options = {"REGISTER", "UNDO"} # , "INTERNAL"
use_clean_mats : bpy.props.BoolProperty(name="Remove Duplication",
description="All duplicated material (with suffix .001, .002 ...) will be replaced by the material with clean name (if found in scene)" ,
default=True)
skip_different_materials : bpy.props.BoolProperty(name="Skip Different Material",
description="Will not touch duplication if color settings are different (and show infos about skipped materials)",
default=True)
use_fuses_mats : bpy.props.BoolProperty(name="Fuse Materials Slots",
description="Fuse materials slots when multiple uses same materials",
default=True)
remove_empty_slots : bpy.props.BoolProperty(name="Remove Empty Slots",
description="Remove slots that haven't any material attached ",
default=True)
# skip_binded_empty_slots : bpy.props.BoolProperty(name="Skip Binded Empty slots",
# description="Remove only empty slots that haven't any material attached",
# default=False)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
self.ob = context.object
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
box = layout.box()
box.prop(self, 'use_clean_mats')
if self.use_clean_mats:
box.prop(self, 'skip_different_materials')
# layout.separator()
box = layout.box()
box.prop(self, 'use_fuses_mats')
box = layout.box()
box.prop(self, 'remove_empty_slots')
# if self.remove_empty_slots:
# box.prop(self, 'skip_binded_empty_slots')
def different_gp_mat(self, mata, matb):
a = mata.grease_pencil
b = matb.grease_pencil
if a.color[:] != b.color[:]:
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke color is different'
if a.fill_color[:] != b.fill_color[:]:
return f'! {self.ob.name}: {mata.name} and {matb.name} fill_color color is different'
if a.show_stroke != b.show_stroke:
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke has different state'
if a.show_fill != b.show_fill:
return f'! {self.ob.name}: {mata.name} and {matb.name} fill has different state'
## Clean dups
def clean_mats_duplication(self, ob):
import re
diff_ct = 0
todel = []
if ob.type != 'GPENCIL':
return
if not hasattr(ob, 'material_slots'):
return
for i, ms in enumerate(ob.material_slots):
mat = ms.material
if not mat:
continue
match = re.search(r'(.*)\.\d{3}$', mat.name)
if not match:
continue
basemat = bpy.data.materials.get(match.group(1))
if not basemat:
continue
diff = self.different_gp_mat(mat, basemat)
if diff:
print(diff)
diff_ct += 1
if self.skip_different_materials:
continue
if mat not in todel:
todel.append(mat)
ms.material = basemat
print(f'{ob.name} : slot {i} >> replaced {mat.name}')
mat.use_fake_user = False
### delete (only when using on all objects loop, else can delete another objects mat...)
## for m in reversed(todel):
## bpy.data.materials.remove(m)
if diff_ct:
return('INFO', f'{diff_ct} mat skipped >> same name but different color settings!')
## fuse
def fuse_object_mats(self, ob):
for i in range(len(ob.material_slots))[::-1]:
ms = ob.material_slots[i]
mat = ms.material
# if not mat:
# # remove empty slots
# if self.remove_empty_slots:
# ob.active_material_index = i
# bpy.ops.object.material_slot_remove()
# continue
# update mat list
mlist = [ms.material for ms in ob.material_slots if ms.material]
if mlist.count(mat) > 1:
# get first material in list
new_mat_id = mlist.index(mat)
# iterate in all strokes and replace with new_mat_id
for l in ob.data.layers:
for f in l.frames:
for s in f.strokes:
if s.material_index == i:
s.material_index = new_mat_id
# delete slot (or add to the remove_slot list
ob.active_material_index = i
bpy.ops.object.material_slot_remove()
def delete_empty_material_slots(self, ob):
for i in range(len(ob.material_slots))[::-1]:
ms = ob.material_slots[i]
mat = ms.material
if not mat:
# is_binded=False
# if self.skip_binded_empty_slots:
# for l in ob.data.layers:
# for f in l.frames:
# for s in f.strokes:
# if s.material_index == i:
# is_binded = True
# break
# if is_binded:
# continue
ob.active_material_index = i
bpy.ops.object.material_slot_remove()
def execute(self, context):
ob = context.object
info = None
if not self.use_clean_mats and not self.use_fuses_mats and not self.remove_empty_slots:
self.report({'ERROR'}, 'At least one operation should be selected')
return {"CANCELLED"}
if self.use_clean_mats:
info = self.clean_mats_duplication(ob)
if self.use_fuses_mats:
self.fuse_object_mats(ob)
if self.remove_empty_slots:
self.delete_empty_material_slots(ob)
if info:
self.report({info[0]}, info[1])
# else:
# self.report({'WARNING'}, '')
return {"FINISHED"}
classes = ( classes = (
GPTB_OT_load_palette, GPTB_OT_load_palette,
GPTB_OT_save_palette, GPTB_OT_save_palette,
GPTB_OT_load_default_palette, GPTB_OT_load_default_palette,
GPTB_OT_load_blend_palette, GPTB_OT_load_blend_palette,
GPTB_OT_copy_active_to_selected_palette, GPTB_OT_copy_active_to_selected_palette,
GPTB_OT_clean_material_stack,
) )
def register(): def register():
+382
View File
@@ -0,0 +1,382 @@
import bpy
import mathutils
from mathutils import Matrix, Vector
from math import pi
import numpy as np
from time import time
def get_scale_matrix(scale):
# recreate a neutral mat scale
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
matscale = matscale_x @ matscale_y @ matscale_z
return matscale
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method
:all_stroke: affect hided, locked layers
'''
if restore_frame:
oframe = bpy.context.scene.frame_current
omode = bpy.context.mode
# FIXME : if all_stroke is False, might be better to still store>set>restore "lock_frame"
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 l in obj.data.layers:
for f in l.frames:
if not len(f.strokes):
continue
bpy.context.scene.frame_set(f.frame_number)
# switch to edit to reproject through ops
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW
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:
bpy.context.scene.frame_current = oframe
def align_global(reproject=True, ref=None, all_strokes=True):
if not ref:
ref = bpy.context.scene.camera
o = bpy.context.object
# if o.matrix_basis != o.matrix_world and not o.parent:
ref = bpy.context.scene.camera
ref_mat = ref.matrix_world
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
if o.parent:
mat = o.matrix_world
else:
mat = o.matrix_basis
o_loc, o_rot, o_scale = mat.decompose()
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
loc_mat = Matrix.Translation(o_loc)
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat
# world_coords = []
for l in o.data.layers:
for f in l.frames:
for s in f.strokes:
## foreach
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords)
# print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v])
# s.points.update() # seem to works # but adding/deleting a point is "safer"
## force update
s.points.add(1)
s.points.pop()
# for p in s.points:
## GOOD :
# world_co = mat @ p.co
# p.co = new_mat.inverted() @ world_co
## GOOD :
# p.co = p.co @ mat.inverted() @ new_mat
if o.parent:
o.matrix_world = new_mat
else:
o.matrix_basis = new_mat
if reproject:
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
def align_all_frames(reproject=True, ref=None, all_strokes=True):
print('aligning all frames...')
o = bpy.context.object
if not ref:
ref = bpy.context.scene.camera
# get all rot
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
## double list keys
rot_keys = [k.co.x for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
## normal iter
# for fcu in o.animation_data.action.fcurves:
# if fcu.data_path != chanel :
# continue
# for k in fcu.keyframe_points():
# rot_keys.append(k.co.x)
rot_keys = list(set(rot_keys))
# TODO # TOTHINK
# for now the rotation of the object is adjusted at every frame....
# might be better to check camera rotation of the current frame only (stored as copy).
# else the object rotate following the cameraview vector (not constant)...
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
for l in o.data.layers:
for f in l.frames:
# set the frame to dedicated
bpy.context.scene.frame_set(f.frame_number)
ref_mat = ref.matrix_world
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
if o.parent:
mat = o.matrix_world
else:
mat = o.matrix_basis
o_loc, o_rot, o_scale = mat.decompose()
loc_mat = Matrix.Translation(o_loc)
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat
for s in f.strokes:
## foreach
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords)
# print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v])
# s.points.update() # seem to works
## force update
s.points.add(1)
s.points.pop()
for fnum in rot_keys:
bpy.context.scene.frame_set(fnum)
#/update calculation block
ref_mat = ref.matrix_world
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
if o.parent:
mat = o.matrix_world
else:
mat = o.matrix_basis
o_loc, o_rot, o_scale = mat.decompose()
loc_mat = Matrix.Translation(o_loc)
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat
# update calculation block/
if o.parent:
o.matrix_world = new_mat
else:
o.matrix_basis = new_mat
o.keyframe_insert(chanel, index=-1, frame=bpy.context.scene.frame_current, options={'INSERTKEY_AVAILABLE'})
if reproject:
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
return
class GPTB_OT_realign(bpy.types.Operator):
bl_idname = "gp.realign"
bl_label = "Realign GP"
bl_description = "Realign the grease pencil front axis with active camera"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
reproject : bpy.props.BoolProperty(
name='Reproject', default=True,
description='Reproject stroke on the new alignment')
all_strokes : bpy.props.BoolProperty(
name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected')
set_draw_axis : bpy.props.BoolProperty(
name='Set draw axis to Front', default=True,
description='Set the gpencil draw plane axis to Front')
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
def invoke(self, context, event):
if context.object.data.use_multiedit:
self.report({'ERROR'}, 'Does not work in Multi-edit')
return {"CANCELLED"}
self.alert = ''
o = context.object
if o.animation_data and o.animation_data.action:
act = o.animation_data.action
for chan in ('rotation_euler', 'rotation_quaternion'):
if act.fcurves.find(chan):
self.alert = 'Animated Rotation (CONSTANT interpolation)'
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
if interpos:
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
break
return context.window_manager.invoke_props_dialog(self, width=450)
def draw(self, context):
layout = self.layout
layout.label(text='Realign the GP object : Front axis facing active camera')
if self.alert:
layout.label(text=self.alert, icon='ERROR')
layout.label(text='(rotations key will be overwritten to face camera)')
# layout.separator()
box = layout.box()
box.prop(self, "reproject")
if self.reproject:
box.label(text='After Realigning, reproject each frames on front axis')
if not context.region_data.view_perspective == 'CAMERA':
box.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
box.prop(self, "all_strokes")
if not self.all_strokes:
box.label(text='Only visible and unlocked layers will be reprojected', icon='INFO')
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
if axis != 'AXIS_Y':
orient = {
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
# 'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], #
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'],
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'],
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
}
box = layout.box()
box.label(text=f'Current drawing plane : {orient[axis][0]}', icon=orient[axis][1])
box.prop(self, "set_draw_axis")
def exit(self, context, frame):
context.scene.frame_current = frame
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y' and self.set_draw_axis:
context.scene.tool_settings.gpencil_sculpt.lock_axis = 'AXIS_Y'
def execute(self, context):
t0 = time()
oframe = context.scene.frame_current
o = bpy.context.object
if o.animation_data and o.animation_data.action:
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'):
align_all_frames(reproject=self.reproject)
print(f'\nAnim realign ({time()-t0:.2f}s)')
self.exit(context, oframe)
return {"FINISHED"}
align_global(reproject=self.reproject)
print(f'\nGlobal Realign ({time()-t0:.2f}s)')
self.exit(context, oframe)
return {"FINISHED"}
class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
bl_idname = "gp.batch_reproject_all_frames"
bl_label = "Reproject All Frames"
bl_description = "Reproject all frames of active object."
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
all_strokes : bpy.props.BoolProperty(
name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type',
items=(('FRONT', "Front", ""),
('SIDE', "Side", ""),
('TOP', "Top", ""),
('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""),
),
default='FRONT')
def invoke(self, context, event):
if context.object.data.use_multiedit:
self.report({'ERROR'}, 'Does not work in Multi-edit')
return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
if not context.region_data.view_perspective == 'CAMERA':
layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.prop(self, "all_strokes")
layout.prop(self, "type")
def execute(self, context):
t0 = time()
batch_reproject(context.object, proj_type=self.type, all_strokes=self.all_strokes, restore_frame=True)
self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
return {"FINISHED"}
### -- MENU ENTRY --
def reproject_clean_menu(self, context):
if context.mode == 'EDIT_GPENCIL':
self.layout.operator('gp.batch_reproject_all_frames', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
def reproject_context_menu(self, context):
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator('gp.batch_reproject_all_frames', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
classes = (
GPTB_OT_realign,
GPTB_OT_batch_reproject_all_frames,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(reproject_context_menu)
bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu)
def unregister():
bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu)
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+6 -248
View File
@@ -4,9 +4,9 @@ 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://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
<!-- ### [Demo Youtube]() --> **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
**[Readme Doc in French (Documentation en Français et plus détaillée)](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/blob/master/README_FR.md)** **[Readme Doc in French (Documentation en Français et plus détaillée)](README_FR.md)**
--- ---
@@ -31,7 +31,7 @@ Note about palette : For now the importer is not working with linked palette as
### Passive action ### Passive action
Add an "on save" Handler that trigger relative remap of all path. Add an "on save" Handler that trigger relative remap of all path (can be disabled in addon prefs).
### function ### function
@@ -88,7 +88,7 @@ In material submenu you have mutliple new entry:
Panel in sidebar : 3D view > sidebar 'N' > Gpencil Panel in sidebar : 3D view > sidebar 'N' > Gpencil
<!--
## Todo: ## Todo:
- Allow to render resolution from cam name - Allow to render resolution from cam name
@@ -105,251 +105,9 @@ Panel in sidebar : 3D view > sidebar 'N' > Gpencil
- Move automatically view to match GP Front (depending on Gpencil view settings) - Move automatically view to match GP Front (depending on Gpencil view settings)
- move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures) - move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures) -->
--- ---
## Changelog: Consult [Changelog here](CHANGELOG.md)
1.0.3:
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
1.0.2:
- pref: Added option to disable always remap relative on save in addon-preference
1.0.1:
- fix: copy paste problems
- Get points uv_properties (used for brushed points)
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
1.0.0:
- Compatible with official grease pencil tools
- removed box deform and rotate canvas that existed in other
0.9.3:
- feat: keyframe jump keys are now auto-binded
- UI: added keyframe jump customisation in addon pref
- code: split keyframe jump in a separate file with his new key updater
0.9.2:
- doc: Correct download link (important, bugged the addon install) + update
- code: added tracker url
- updater: remove updater temp file, reset minimum version, turn off verbose mode
0.9.1:
- Public release
- prefs: added fps as part of project settings
- check file use pref fps value (previously used harcoded 24fps value)
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
- update: apply changes in integrated copy-paste from the last version of standalone addon
- doc: Added fully-detailed french readme
0.8.0:
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
- stripped associated properties from properties.py and passed as wm props.
0.7.2:
- fix: Palette importer bug
0.7.0:
- feat: auto create empty frame on color layer
0.6.3:
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
0.6.2:
- feat: colorisation, Option to change stop lines length
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
- Minor refactor for submodule register
0.6.1:
- feat: render objects grouped, one anim render with all ticked object using manual output name
0.6.0:
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
0.5.9:
- feat: render exporter
- Render a selection of GP object isolated from the rest
- added exclusions names for GP object listing
- setup settings and output according to a name
- open render folder
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
- check file: set scene resolution to settings in prefs (default 2048x1080)
0.5.8:
- feat: GP material append on active object from single blend file
0.5.7:
- Added warning message for cursor snapping
0.5.5 - 0.5.6:
- check file: added check for placement an projection mode for Gpencil.
- add a slider to change edit_lines_opacity globally for all GP data at once
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
0.5.4:
- feat: anim manager in his own GP_toolbox submenu:
- button to list disabled anim (allow to quickly check state of the scene)
- disable/enable all fcurve in for GP object or other object separately to paint
- shift clic to target selection only
- check file: added disabled fcurved counter alert with detail in console
0.5.3:
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
- check file option: change select active tool (choice added in addon preferences)
0.5.2:
- Revert back obj_cam operator for following object (native lock view follow only translation)
- Changed method for canvas rotation to more robust rotate axis.
- Add operators on link checker to open containing folder/file of link
- Refactor: file checkers in their own file
0.5.1:
- fix: error when empty material slot on GP object.
- fix: cursor snap on GP canvas when GP is parented
- change: Deleted obj cam (and related set view) operator
- change: blacker note background for playblast (stamp_background)
- feat: Always playblast from main camera (if in draw_cam)
- feat: Handler added to Remap relative on save (pre)
- ops: Check for broken links with porposition to find missing files
- ops: Added basic hardcoded file checker
- Lock main cam
- set scene percentage at 100
- set show slider and sync range
- set fps to 24
0.4.6:
- feat: basic Palette manager with base material check and warning
0.4.5:
- open blender config folder from addon preference
- fix: obj cam parent on selected object
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
- freeview : bug when rotating free viewfrom cardianl views
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
0.4.4:
- feat: added cursor follow handlers and UI toggle
0.4.3:
- change playblast out to 'images' and add playblast as name prefix
0.4.2:
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
- fix: canvas rotate works with parented camera !
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
0.4.1:
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
0.4.0:
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
0.3.8:
- UI: expose onion skin in interface
- UI: expose autolock in interface
- UI : putted tint layers in a submenu
- code: refactor, pushed most of class register in their owner file
- tool: tool to rename current or all grease pencil datablock with different name than container object
0.3.7:
- UI: new interface with tabs for addon preferences
- UI: possible to disable color panel from preference (might be deleted if unusable)
- docs: change readme changelog format and correct doc
0.3.6:
- UI: Stoplines : add a button for quickly set stoplines visibility.
0.3.5:
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
- Fix: Enter key added to valid object-breakdown modal.
0.3.3:
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
0.3.3:
- version 1 beta of gmic colorize
- variant of `screen.gp_keyframe_jump` through keymap seetings
0.3.0:
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
0.2.3:
- add operator to `screen.gp_keyframe_jump`
- add shortcut to rotate canvas
- fix duplicate class
0.2.2:
- separated props resolution_percentage parameter
- playblast options for launching folder and opening folder
0.2.1:
- playblast feature
- Button to go zoom 100% or fit screen
- display scene resolution with res indicator
- Fix reference panel : works with video and display in a box layout.
- close pseudo-color panel by default (plan to move it to Gpencil tab)
0.2.0:
- UI: Toggle camera background images from Toolbox panel
- UI: quick access to passepartout
- Feature: option to use namespace for pseudo color
0.1.5:
- added CGC-auto-updater
0.1.3:
- flip cam x
- inital stage of overlay toggle (need pref/multiple pref)
0.1.2:
- subpanel of GP data (instead of direct append)
- initial commit with GP pseudo color
+6
View File
@@ -4,6 +4,8 @@ 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://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
> Une fois en place un système de mise a jour facilité est accessible dans les préférence (La vérification automatique de nouvelle mise a jour peut y être activé) > Une fois en place un système de mise a jour facilité est accessible dans les préférence (La vérification automatique de nouvelle mise a jour peut y être activé)
Il est recommandé de désactiver l'addon natif "Grease pencil tools" car ces outils sont déjà intégré dans la toolbox et risque de créer des conflit. Il est recommandé de désactiver l'addon natif "Grease pencil tools" car ces outils sont déjà intégré dans la toolbox et risque de créer des conflit.
@@ -147,3 +149,7 @@ pour set : Résolution du film, dossier palette, render settings
- Déplacer les clés de dopesheet en même temps que les clés de GP (Déjà Créer par Tom Viguier sur [Andarta](https://gitlab.com/andarta-pictures) - Déplacer les clés de dopesheet en même temps que les clés de GP (Déjà Créer par Tom Viguier sur [Andarta](https://gitlab.com/andarta-pictures)
- Meilleure table lumineuse (grosse réflexion et travail en perspective) - Meilleure table lumineuse (grosse réflexion et travail en perspective)
---
[Liste des changements ici](CHANGELOG.md)
+68
View File
@@ -0,0 +1,68 @@
import bpy
from bpy.types import WorkSpaceTool
from gpu_extras.presets import draw_circle_2d
from time import time
from .utils import get_addon_prefs
class GPTB_WT_eraser(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'PAINT_GPENCIL'
# The prefix of the idname should be your add-on name.
bl_idname = "gp.eraser_tool"
bl_label = "Eraser"
bl_description = (
"This is a tooltip\n"
"with multiple lines"
)
bl_icon = "brush.paint_vertex.draw"
bl_widget = None
bl_keymap = (
("gp.eraser", {"type": 'LEFTMOUSE', "value": 'PRESS'},
{"properties": []}),
("wm.radial_control", {"type": 'F', "value": 'PRESS'},
{"properties": [("data_path_primary", 'scene.gptoolprops.eraser_radius')]}),
)
bl_cursor = 'DOT'
'''
def draw_cursor(context, tool, xy):
from gpu_extras.presets import draw_circle_2d
radius = context.scene.gptoolprops.eraser_radius
draw_circle_2d(xy, (0.75, 0.25, 0.35, 0.85), radius, 32)
'''
def draw_settings(context, layout, tool):
layout.prop(context.scene.gptoolprops, "eraser_radius")
### --- REGISTER ---
## --- KEYMAP
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name="Grease Pencil Stroke Paint (Draw brush)", space_type="EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new("gp.eraser", type='LEFTMOUSE', value="PRESS", ctrl=True)
prefs = get_addon_prefs()
kmi.active = prefs.use_precise_eraser
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
def register():
bpy.utils.register_tool(GPTB_WT_eraser, after={"builtin.cursor"})
#bpy.context.window_manager.keyconfigs.default.keymaps['Grease Pencil Stroke Paint (Draw brush)'].keymap_items[3].idname = 'gp.eraser'
register_keymaps()
def unregister():
bpy.utils.unregister_tool(GPTB_WT_eraser)
unregister_keymaps()
+73 -41
View File
@@ -81,10 +81,13 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
if context.scene.camera: if context.scene.camera:
row = layout.row(align=True)# .split(factor=0.5) row = layout.row(align=True)# .split(factor=0.5)
row.label(text='Passepartout') row.label(text='Passepartout')
row.prop(context.scene.camera.data, 'show_passepartout',text='', icon ='OBJECT_HIDDEN' ) if context.scene.camera.name == 'draw_cam':
row.prop(context.scene.gptoolprops, 'drawcam_passepartout', text='', icon ='OBJECT_HIDDEN')
else:
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
row.prop(context.scene.camera.data, 'passepartout_alpha', text='') row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
row = layout.row(align=True) 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.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') row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER')
## background images/videos ## background images/videos
@@ -99,48 +102,45 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
for bg_img in context.scene.camera.data.background_images: for bg_img in context.scene.camera.data.background_images:
if bg_img.source == 'IMAGE' and bg_img.image: if bg_img.source == 'IMAGE' and bg_img.image:
row = box.row(align=True) row = box.row(align=True)
row.label(text=bg_img.image.name, icon='IMAGE_RGB')# FILE_IMAGE row.prop(bg_img, 'show_background_image', text='', icon='IMAGE_RGB')
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'} row.prop(bg_img, 'alpha', text=bg_img.image.name) # options={'HIDDEN'}
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'} # row.label(icon='IMAGE_RGB')
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
# row.prop(bg_img, 'show_background_image', text='', icon=icon)
if bg_img.source == 'MOVIE_CLIP' and bg_img.clip: if bg_img.source == 'MOVIE_CLIP' and bg_img.clip:
row = box.row(align=True) row = box.row(align=True)
row.label(text=bg_img.clip.name, icon='FILE_MOVIE') row.prop(bg_img, 'show_background_image', text='', icon='FILE_MOVIE')
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'} row.prop(bg_img, 'alpha', text=bg_img.clip.name) # options={'HIDDEN'}
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'} # row.label(icon='FILE_MOVIE')
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
## playblast params # row.prop(bg_img, 'show_background_image', text='', icon=icon)
layout.separator()
layout.label(text = 'Playblast:')
row = layout.row(align=False)#split(factor=0.6)
row.label(text = f'{rd.resolution_x * context.scene.gptoolprops.resolution_percentage // 100} x {rd.resolution_y * context.scene.gptoolprops.resolution_percentage // 100}')
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
# row.prop(rd, 'resolution_percentage', text='')#real percent scene percentage
row = layout.row(align=True)
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
else: else:
layout.label(text='No camera !', icon = 'ERROR') layout.label(text='No camera !', icon = 'ERROR')
## Straight line ops from official greasepencil_tools addon if enabled.
# if any(x in context.preferences.addons.keys() for x in ('greasepencil_tools', 'greasepencil-addon')): # check enabled addons
if hasattr(bpy.types, 'GP_OT_straight_stroke'): # check if operator exists : bpy.ops.gp.straight_stroke.idname()
layout.operator('gp.straight_stroke', icon ="CURVE_PATH")
## Options ## Options
layout.separator() layout.separator()
layout.label(text = 'Options:') layout.label(text = 'Options:')
# row = layout.row(align=False)
## maybe remove cursor_follow icon that look like
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
layout.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
layout.prop(context.space_data.overlay, 'use_gpencil_onion_skin')
## Kf Jump filter
layout.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
col = layout.column()
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
if context.object and context.object.type == 'GPENCIL': if context.object and context.object.type == 'GPENCIL':
layout.prop(context.object.data, 'use_autolock_layers') # col.prop(context.object.data, 'use_autolock_layers') # not often used
layout.prop(context.object, 'show_in_front', text='X-ray')#default text "In Front" col.prop(context.object, 'show_in_front', text='X-ray') # default text "In Front"
## rename datablock temporary layout ## rename datablock temporary layout
if context.object.name != context.object.data.name: if context.object.name != context.object.data.name:
box = layout.box() box = col.box()
box.label(text='different name for object and data:', icon='INFO') box.label(text='different name for object and data:', icon='INFO')
row = box.row(align=False) row = box.row(align=False)
row.operator('gp.rename_data_from_obj').rename_all = False row.operator('gp.rename_data_from_obj').rename_all = False
@@ -148,24 +148,34 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
## Check base palette ## Check base palette
if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")): if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
box = layout.box() box = col.box()
box.label(text='Missing base material setup', icon='INFO') box.label(text='Missing base material setup', icon='INFO')
box.operator('gp.load_default_palette') box.operator('gp.load_default_palette')
else: else:
layout.label(text='No GP object selected') col.label(text='No GP object selected')
layout.prop(context.scene.gptoolprops, 'edit_lines_opacity') col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
row = col.row(align=True)
## realign / reproject
row.operator('gp.realign', icon='AXIS_FRONT')
## move in depth
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel ) ## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
layout.operator('gp.create_empty_frames', icon = 'DECORATE_KEYFRAME') col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
## File checker ## File checker
row = layout.row(align=True) row = col.row(align=True)
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA') row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED') row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
# Mention update as notice # Mention update as notice
addon_updater_ops.update_notice_box_ui(self, context) addon_updater_ops.update_notice_box_ui(self, context)
@@ -217,6 +227,28 @@ class GPTB_PT_anim_manager(bpy.types.Panel):
ops.skip_obj = True ops.skip_obj = True
ops.mute = True ops.mute = True
class GPTB_PT_toolbox_playblast(bpy.types.Panel):
bl_label = "Playblast"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
rd = context.scene.render
row = layout.row(align=False) # split(factor=0.6)
percent = context.scene.gptoolprops.resolution_percentage
row.label(text = f'{rd.resolution_x * percent // 100} x {rd.resolution_y * percent // 100}')
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
# row.prop(rd, 'resolution_percentage', text='') # real percent scene percentage
row = layout.row(align=True)
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
class GPTB_PT_tint_layers(bpy.types.Panel): class GPTB_PT_tint_layers(bpy.types.Panel):
bl_label = "Tint layers" bl_label = "Tint layers"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
@@ -272,13 +304,14 @@ class GPTB_PT_render(bpy.types.Panel):
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS
blend = Path(bpy.data.filepath) blend = bpy.data.filepath
if blend:
blend = Path(blend)
out = blend.parents[1] / "compo" / "base" out = blend.parents[1] / "compo" / "base"
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out) layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
""" """
## unused -- (integrated in sidebar_panel) ## unused -- (integrated in sidebar_panel)
class GPTB_PT_cam_ref_panel(bpy.types.Panel): class GPTB_PT_cam_ref_panel(bpy.types.Panel):
@@ -320,11 +353,13 @@ def palette_manager_menu(self, context):
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.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
classes = ( classes = (
GPTB_PT_sidebar_panel, GPTB_PT_sidebar_panel,
GPTB_PT_anim_manager, GPTB_PT_anim_manager,
GPTB_PT_toolbox_playblast,
GPTB_PT_tint_layers, GPTB_PT_tint_layers,
GPTB_PT_render, GPTB_PT_render,
## GPTB_PT_cam_ref_panel, ## GPTB_PT_cam_ref_panel,
@@ -385,8 +420,5 @@ def GPdata_toolbox_panel(self, context):
row.label("Extra tools") row.label("Extra tools")
if settings.extra_tools : if settings.extra_tools :
layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction") """ layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction")
"""
Regular → Executable
+92 -11
View File
@@ -14,10 +14,10 @@
bl_info = { bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Set of tools for Grease Pencil in animation production", "description": "Set of tools for Grease Pencil in animation production",
"author": "Samuel Bernou", "author": "Samuel Bernou, Christophe Seux",
"version": (1, 0, 3), "version": (1, 5, 1),
"blender": (2, 91, 0), "blender": (2, 91, 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://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues", "tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
@@ -41,16 +41,23 @@ from . import OP_helpers
from . import OP_keyframe_jump from . import OP_keyframe_jump
from . import OP_cursor_snap_canvas from . import OP_cursor_snap_canvas
from . import OP_palettes from . import OP_palettes
from . import OP_brushes
from . import OP_file_checker from . import OP_file_checker
from . import OP_render from . import OP_render
from . import OP_copy_paste from . import OP_copy_paste
from . import OP_realign
from . import OP_depth_move
from . import OP_key_duplicate_send
from . import OP_eraser_brush
from . import TOOL_eraser_brush
from . import handler_draw_cam
from . import keymaps from . import keymaps
from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers
from . import UI_tools from . import UI_tools
from .properties import GP_PG_ToolsSettings from .properties import GP_PG_ToolsSettings, GP_PG_FixSettings
from bpy.props import (FloatProperty, from bpy.props import (FloatProperty,
BoolProperty, BoolProperty,
@@ -73,11 +80,14 @@ from pathlib import Path
@persistent @persistent
def remap_relative(dummy): def remap_relative(dummy):
# try:
all_path = [lib for lib in bpy.utils.blend_paths(local=True)] all_path = [lib for lib in bpy.utils.blend_paths(local=True)]
bpy.ops.file.make_paths_relative() bpy.ops.file.make_paths_relative()
for i, lib in enumerate(bpy.utils.blend_paths(local=True)): for i, lib in enumerate(bpy.utils.blend_paths(local=True)):
if all_path[i] != lib: if all_path[i] != lib:
print('Remapped:', all_path[i], '\n>> ', lib) print('Remapped:', all_path[i], '\n>> ', lib)
# except Exception as e:
# print(e)
def remap_on_save_update(self, context): def remap_on_save_update(self, context):
pref = get_addon_prefs() pref = get_addon_prefs()
@@ -89,15 +99,30 @@ def remap_on_save_update(self, context):
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]: if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.remove(remap_relative) bpy.app.handlers.save_pre.remove(remap_relative)
def update_use_precise_eraser(self, context):
km, kmi = TOOL_eraser_brush.addon_keymaps[0]
kmi.active = self.use_precise_eraser
class GPTB_prefs(bpy.types.AddonPreferences): class GPTB_prefs(bpy.types.AddonPreferences):
bl_idname = __name__ bl_idname = __name__
use_precise_eraser : BoolProperty(
name='Precise Eraser',
default=False,
update=update_use_precise_eraser
)
## tabs ## tabs
pref_tabs : EnumProperty( pref_tabs : EnumProperty(
items=(('PREF', "Preferences", "Change some preferences of the modal"), items=(('PREF', "Preferences", "Change some preferences of the modal"),
('MAN_OPS', "Operator", "Operator to add Manually"), ('MAN_OPS', "Operator", "Operator to add Manually"),
# ('TUTO', "Tutorial", "How to use the tool"), # ('TUTO', "Tutorial", "How to use the tool"),
('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"),
('UPDATE', "Update", "Check and apply updates"), ('UPDATE', "Update", "Check and apply updates"),
# ('KEYMAP', "Keymap", "customise the default keymap"), # ('KEYMAP', "Keymap", "customise the default keymap"),
), ),
@@ -149,14 +174,14 @@ class GPTB_prefs(bpy.types.AddonPreferences):
use_relative_remap_on_save : BoolProperty( use_relative_remap_on_save : BoolProperty(
name="Relative Remap On Save", name="Relative Remap On Save",
description="Always remap all external path to relative when saving\nNeed blender restart if changed", description="Always remap all external path to relative when saving\nNeed blender restart if changed",
default=True, default=False,
update=remap_on_save_update update=remap_on_save_update
) )
fps : IntProperty( fps : IntProperty(
name='Frame Rate', name='Frame Rate',
description="Fps of the project, Used to conform the file when you use Check file operator", description="Fps of the project, Used to conform the file when you use Check file operator",
default=25, default=24,
min=1, min=1,
max=10000 max=10000
) )
@@ -185,6 +210,11 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Path to palette containing palette.json files to save and load", description="Path to palette containing palette.json files to save and load",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
brush_path : StringProperty(
name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
## Playblast prefs ## Playblast prefs
playblast_auto_play : BoolProperty( playblast_auto_play : BoolProperty(
name="Playblast auto play", name="Playblast auto play",
@@ -235,7 +265,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
## KF jumper ## KF jumper
kfj_use_shortcut: BoolProperty( kfj_use_shortcut: BoolProperty(
name = "Use Keyframe Jump Shortcut", name = "Use Keyframe Jump Shortcut",
description = "Auto bind shotcut for keyframe jump (else you can bien manually using 'screen.gp_keyframe_jump' id_name)", description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
default = True) default = True)
kfj_prev_keycode : StringProperty( kfj_prev_keycode : StringProperty(
@@ -278,6 +308,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description = "add ctrl", description = "add ctrl",
default = False) default = False)
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings)
## Temp cutter ## Temp cutter
# temp_cutter_use_shortcut: BoolProperty( # temp_cutter_use_shortcut: BoolProperty(
# name = "Use temp cutter Shortcut", # name = "Use temp cutter Shortcut",
@@ -304,8 +337,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
row.prop(self, 'render_res_x', text='Width') row.prop(self, 'render_res_x', text='Width')
row.prop(self, 'render_res_y', text='Height') row.prop(self, 'render_res_y', text='Height')
## Palette ## Palette
box.label(text='Palette library folder:') box.label(text='Project folders:')
box.prop(self, 'palette_path') box.prop(self, 'palette_path')
box.prop(self, 'brush_path')
## render output ## render output
box.prop(self, 'output_path') box.prop(self, 'output_path')
@@ -321,7 +355,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
# box.separator()## Keyframe jumper # box.separator()## Keyframe jumper
box = layout.box() box = layout.box()
box.label(text='Keyframe Jump option:') box.label(text='Keyframe Jump options:')
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts') box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
if self.kfj_use_shortcut: if self.kfj_use_shortcut:
@@ -367,11 +401,14 @@ class GPTB_prefs(bpy.types.AddonPreferences):
box.label(text='Random color options:') box.label(text='Random color options:')
box.prop(self, 'separator') box.prop(self, 'separator')
box = layout.box()
box.label(text='Tools options:')
box.prop(self, 'use_precise_eraser')
if self.pref_tabs == 'MAN_OPS': if self.pref_tabs == 'MAN_OPS':
# layout.separator()## notes # layout.separator()## notes
# layout.label(text='Notes:') # layout.label(text='Notes:')
layout.label(text='Following operators ID have to be set manually :') layout.label(text='Following operators ID have to be set manually in keymap if needed :')
## keyframe jump ## keyframe jump
box = layout.box() box = layout.box()
@@ -392,11 +429,38 @@ class GPTB_prefs(bpy.types.AddonPreferences):
row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap' row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap'
box.label(text='Or just create a new shortcut using cursor_snap') box.label(text='Or just create a new shortcut using cursor_snap')
## Clear keyframe
box = layout.box()
box.label(text='Clear active frame (delete all strokes without deleting the frame)')
row = box.row()
row.label(text='gp.clear_active_frame')
row.operator('wm.copytext', icon='COPYDOWN').text = 'gp.clear_active_frame'
## user prefs ## user prefs
box = layout.box() box = layout.box()
box.label(text='Note: You can access user pref file and startup file in config folder') box.label(text='Note: You can access user pref file and startup file in config folder')
box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG') box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG')
if self.pref_tabs == 'CHECKS':
layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column()
# row = col.row()
col.prop(self.fixprops, 'lock_main_cam')
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
col.prop(self.fixprops, 'set_res_percentage')
col.prop(self.fixprops, 'set_fps', text=f'Reset FPS (to {self.fps})')
col.prop(self.fixprops, 'set_slider_n_sync')
col.prop(self.fixprops, 'set_cursor_type')
col.prop(self.fixprops, 'check_front_axis')
col.prop(self.fixprops, 'check_placement')
col.prop(self.fixprops, 'set_pivot_median_point')
col.prop(self.fixprops, 'disable_guide')
col.prop(self.fixprops, 'list_disabled_anim')
col.prop(self.fixprops, 'autokey_add_n_replace')
# row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)')
if self.pref_tabs == 'UPDATE': if self.pref_tabs == 'UPDATE':
addon_updater_ops.update_settings_ui(self, context) addon_updater_ops.update_settings_ui(self, context)
@@ -408,8 +472,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
classes = ( classes = (
GPTB_prefs, GP_PG_FixSettings,
GP_PG_ToolsSettings, GP_PG_ToolsSettings,
GPTB_prefs,
GPT_OT_auto_tint_gp_layers, GPT_OT_auto_tint_gp_layers,
) )
@@ -418,6 +483,7 @@ GPT_OT_auto_tint_gp_layers,
def register(): def register():
addon_updater_ops.register(bl_info) addon_updater_ops.register(bl_info)
# bpy.types.Scene.gpfixprops = bpy.props.PointerProperty(type = GP_PG_FixSettings) # used in prefs
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
OP_helpers.register() OP_helpers.register()
@@ -429,9 +495,16 @@ def register():
OP_playblast_bg.register() OP_playblast_bg.register()
OP_playblast.register() OP_playblast.register()
OP_palettes.register() OP_palettes.register()
OP_brushes.register()
OP_cursor_snap_canvas.register() OP_cursor_snap_canvas.register()
OP_render.register() OP_render.register()
OP_copy_paste.register() OP_copy_paste.register()
OP_realign.register()
OP_depth_move.register()
OP_key_duplicate_send.register()
OP_eraser_brush.register()
TOOL_eraser_brush.register()
handler_draw_cam.register()
UI_tools.register() UI_tools.register()
keymaps.register() keymaps.register()
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings) bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings)
@@ -453,9 +526,16 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
UI_tools.unregister() UI_tools.unregister()
handler_draw_cam.unregister()
OP_eraser_brush.unregister()
TOOL_eraser_brush.unregister()
OP_key_duplicate_send.unregister()
OP_depth_move.unregister()
OP_realign.unregister()
OP_copy_paste.unregister() OP_copy_paste.unregister()
OP_render.unregister() OP_render.unregister()
OP_cursor_snap_canvas.unregister() OP_cursor_snap_canvas.unregister()
OP_brushes.unregister()
OP_palettes.unregister() OP_palettes.unregister()
OP_file_checker.unregister() OP_file_checker.unregister()
OP_helpers.unregister() OP_helpers.unregister()
@@ -465,6 +545,7 @@ def unregister():
GP_colorize.unregister()## GP_guided_colorize. GP_colorize.unregister()## GP_guided_colorize.
OP_playblast_bg.unregister() OP_playblast_bg.unregister()
OP_playblast.unregister() OP_playblast.unregister()
# del bpy.types.Scene.gpfixprops
del bpy.types.Scene.gptoolprops del bpy.types.Scene.gptoolprops
+170
View File
@@ -0,0 +1,170 @@
import bpy
import gpu
import bgl
# import blf
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
from bpy.app.handlers import persistent
def extrapolate_points_by_length(a, b, length):
'''
Return a third point C from by continuing in AB direction
Length define BC distance. both vector2 and vector3
'''
# return b + ((b - a).normalized() * length)# one shot
ab = b - a
if not ab:
return None
return b + (ab.normalized() * length)
def view3d_camera_border_2d(context, cam):
# based on https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
# cam = context.scene.camera
frame = cam.data.view_frame(scene=context.scene)
# to world-space
frame = [cam.matrix_world @ v for v in frame]
# to pixelspace
region, rv3d = context.region, context.space_data.region_3d
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px
def draw_cam_frame_callback_2d():
context = bpy.context
if context.region_data.view_perspective != 'CAMERA':
return
if context.scene.camera.name != 'draw_cam':
return
main_cam = context.scene.camera.parent
if not main_cam:
return
bgl.glEnable(bgl.GL_BLEND)
frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT
# frame positions
#
# lupext lup rup rupext
# D-----A
# | |
# C-----B
# ldnext ldn rdn lnext
a = frame_point[0]
b = frame_point[1]
c = frame_point[2]
d = frame_point[3]
ext = 10000
rup = extrapolate_points_by_length(b,a, ext)
rdn = extrapolate_points_by_length(a,b, ext)
rupext = rup + ((a-d).normalized() * ext)
rdnext = rdn + ((a-d).normalized() * ext)
lup = extrapolate_points_by_length(c,d, ext)
ldn = extrapolate_points_by_length(d,c, ext)
lupext = lup + ((c-b).normalized() * ext)
ldnext = ldn + ((c-b).normalized() * ext)
# ppp_color = (1.0, 1.0, 0.0, 1)
ppp_color = (0.0, 0.0, 0.0, context.scene.camera.data.passepartout_alpha)
rect = [rup, rdn, rupext, rdnext,
lup, ldn, lupext, ldnext,
a, b, c, d]
## convert to 2d
# region, rv3d = context.region, context.space_data.region_3d
# rect = [location_3d_to_region_2d(region, rv3d, v) for v in rect]
# lupext=6 lup=4 rup=0 rupext=2
# d=11, a=8
# c=10, b=9
# ldnext=7 ldn=5 rdn=1 dnpext=3
indices = [(0,1,2), (1,2,3),
(4,5,6), (5,6,7),
(8,11,4), (8,4,0),
(10,9,5), (9,5,1),
]
# ### passpartout_points
passepartout = batch_for_shader(
shader_2d, 'TRIS', {"pos": rect}, indices=indices)
# shader_2d, 'LINE_LOOP', {"pos": rect})
shader_2d.bind()
shader_2d.uniform_float("color", ppp_color)
passepartout.draw(shader_2d)
### Camera framing trace over
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
"""
## need to accurately detect viewport background color (difficult)
### COUNTER CURRENT FRAMING
if False:
camf = view3d_camera_border_2d(
context, context.scene.camera)
ca = (camf[0].x + 1, camf[0].y + 1)
cb = (camf[1].x + 1, camf[1].y - 1)
cc = (camf[2].x - 1, camf[2].y - 1)
cd = (camf[3].x - 1, camf[3].y + 1)
screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": [ca, cb, cc, cd]})
shader_2d.bind()
shader_2d.uniform_float("color", (0.25, 0.25, 0.25, 1.0))
# shader_2d.uniform_float("color", (0.9, 0.9, 0.9, 1.0))
screen_framing.draw(shader_2d)
"""
### FRAMING
# frame_color = (0.06, 0.4, 0.040, 0.5)
frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": frame_point})
shader_2d.bind()
shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d)
# bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_LINE_SMOOTH)
bgl.glDisable(bgl.GL_BLEND)
draw_handle = None
def register():
if bpy.app.background:
return
global draw_handle
draw_handle = bpy.types.SpaceView3D.draw_handler_add(
draw_cam_frame_callback_2d, (), "WINDOW", "POST_PIXEL")
def unregister():
if bpy.app.background:
return
global draw_handle
if draw_handle:
bpy.types.SpaceView3D.draw_handler_remove(draw_handle, 'WINDOW')
if __name__ == "__main__":
register()
Regular → Executable
+88 -6
View File
@@ -4,20 +4,90 @@ from bpy.props import (
BoolProperty, BoolProperty,
StringProperty, StringProperty,
FloatProperty, FloatProperty,
EnumProperty,
) )
from .OP_cursor_snap_canvas import cursor_follow_update from .OP_cursor_snap_canvas import cursor_follow_update
def change_edit_lines_opacity(self, context): def change_edit_lines_opacity(self, context):
# for o in context.scene.objects:
# if o.type != 'GPENCIL':
# continue
# o.data.edit_line_color[3]=self.edit_lines_opacity
for gp in bpy.data.grease_pencils: for gp in bpy.data.grease_pencils:
if not gp.is_annotation: if not gp.is_annotation:
gp.edit_line_color[3]=self.edit_lines_opacity gp.edit_line_color[3]=self.edit_lines_opacity
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
class GP_PG_FixSettings(bpy.types.PropertyGroup):
lock_main_cam : BoolProperty(
name="Lock Main Cam",
description="Lock the main camera (works only if 'layout' is not in name)",
default=False, options={'HIDDEN'})
set_scene_res : BoolProperty(
name="Reset Scene Resolution",
description="Set the scene resolution to current prefs project settings",
default=True, options={'HIDDEN'})
set_res_percentage : BoolProperty(
name="Reset Resolution Percentage To 100%",
description="",
default=True, options={'HIDDEN'})
set_fps : BoolProperty(
name="Reset Fps",
description="Set the framerate of the scene to current prefs project settings",
default=False, options={'HIDDEN'})
set_slider_n_sync : BoolProperty(
name="Dopesheets Show Slider And Sync Range",
description="Toggle on the use of show slider and sync range",
default=True, options={'HIDDEN'})
set_cursor_type : BoolProperty(
name="Set Select Cursor Mode",
description="Set the type of the selection cursor (according to addon prefs)",
default=True, options={'HIDDEN'})
check_front_axis : BoolProperty(
name="Check If Draw Axis is Front (X-Z)",
description="Alert if the current grease pencil draw axis is not front (X-Z)",
default=True, options={'HIDDEN'})
check_placement : BoolProperty(
name="Check Stroke Placement",
description="Alert if the current grease pencil stroke placement is not Origin",
default=True, options={'HIDDEN'})
set_pivot_median_point : BoolProperty(
name="Set Pivot To Median Point",
description="Change the pivot point to the most used median point",
default=True, options={'HIDDEN'})
disable_guide : BoolProperty(
name="Disable Drawing Guide",
description="Disable constrained guide in draw mode",
default=False, options={'HIDDEN'})
list_disabled_anim : BoolProperty(
name="List Disabled Animation",
description="Alert if there are disabled animations",
default=True, options={'HIDDEN'})
autokey_add_n_replace : BoolProperty(
name="Autokey Mode Add and Replace",
description="Change autokey mode back to 'Add & Replace'",
default=True, options={'HIDDEN'})
class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
eraser_radius : IntProperty(
name="Eraser Radius", description="Radius of eraser brush",
default=20, min=0, max=500, subtype='PIXEL')
drawcam_passepartout : BoolProperty(
name="Show cam passepartout",
description="Show a darkened overlay outside image area in Camera view",
default=True,
options={'HIDDEN'})
autotint_offset : IntProperty( autotint_offset : IntProperty(
name="Tint hue offset", description="offset the tint by this value for better color", name="Tint hue offset", description="offset the tint by this value for better color",
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1, default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1,
@@ -37,13 +107,25 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
edit_lines_opacity : FloatProperty( edit_lines_opacity : FloatProperty(
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils", default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)#, get=None, set=None name="edit lines Opacity", description="Change edit lines opacity for all grease pencils",
default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
## render ## render
name_for_current_render : StringProperty( name_for_current_render : StringProperty(
name="Render_name", description="Name use for render current", name="Render_name", description="Name use for render current",
default="") default="")
keyframe_type : EnumProperty(
name="Keyframe Filter", description="Only jump to defined keyframe type",
default='ALL', options={'HIDDEN', 'SKIP_SAVE'},
items=(
('ALL', 'All', '', 0), # 'KEYFRAME'
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 1),
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 2),
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 3),
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 4),
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
))
""" """
reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature) reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)