Compare commits

..

139 Commits

Author SHA1 Message Date
pullusb cf3f39b730 Respect group lock and hide state 2024-12-16 17:29:27 +01:00
pullusb 1c39d81ef1 update links for latest gpv2 release on readmes 2024-12-03 18:29:20 +01:00
pullusb 7665dd4f4f Add optional object target for constant cursor follow 2024-12-03 16:37:42 +01:00
pullusb 2013c55ba8 Fix broken cursor_follow feature 2024-12-03 16:10:30 +01:00
pullusb 3695470354 Fix problem in viz conflict check 2024-12-03 15:36:55 +01:00
pullusb 186229cdba typo 2024-12-03 14:36:48 +01:00
pullusb e441f485a6 version bump 2024-12-03 14:27:35 +01:00
pullusb 0cee6163aa Improve file checker
4.0.2

changed: File checker doest not fix directly when clicked:
  - List potential change and display an Apply fix
changed: Enhanced visibility conflict list:
  - Also include viewlayer hide value
  - Allow to set all hide value from the state of one of the three
2024-12-03 14:26:43 +01:00
pullusb 58e6816e39 Fix material move (fill opacity problem comes from blender 4.3 itself) 2024-12-02 14:51:43 +01:00
pullusb 4d6dc06e4e Remove useless pseudo groups now that there is a native group system 2024-12-02 11:39:17 +01:00
pullusb c8763f5ca4 add create empty frames to layer dropdowm menu 2024-11-28 12:29:27 +01:00
pullusb f9e7c9cc3b Fix layer nav operator
4.0.1

- fixed: layer nav operator on page up/down
2024-11-28 12:26:33 +01:00
pullusb 86fb848e4a fix batch reproject frame issue 2024-11-27 18:01:18 +01:00
pullusb abb61ca6d4 Initial code for better cursor_follow behavior (wip) 2024-11-27 16:43:41 +01:00
pullusb 7430bca02f Fix modifiers access 2024-11-27 16:40:31 +01:00
pullusb f5c20a3499 fix bad initialized value in clipboard paste.
4.3 do not  initialize points opacity and radius as expected
2024-11-26 16:30:21 +01:00
pullusb edfefa874a robust attribute copy for duplicate frames 2024-11-26 14:16:29 +01:00
pullusb 6dbf666ee3 Fix duplicate-send to to layer 2024-11-21 15:11:06 +01:00
pullusb d3e4072564 wip copy-move keys 2024-11-14 19:09:52 +01:00
pullusb 63f377d7d1 Fix material picker keymap 2024-11-14 18:02:24 +01:00
pullusb 49c70860a6 gpv3 port: create empty frames - add support for group as source
add utility function to check hide-lock state of nested items
2024-11-14 17:27:57 +01:00
pullusb b525cda28e gpv3 : pick closest layer from stroke 2024-11-14 16:18:56 +01:00
pullusb 05053cff68 fix copy paste layers 2024-11-14 16:11:31 +01:00
pullusb 4937aa32c0 gpv3 world copy cut paste 2024-11-13 18:58:28 +01:00
pullusb 5d930df06b add waning message in readme 2024-11-13 11:10:51 +01:00
pullusb 5d35074d3d fix get addons pref
remove edit line opacity
fix menus
2024-11-12 19:06:57 +01:00
pullusb 94fc926f7a Gp modifier to unified modifier 2024-11-11 18:44:35 +01:00
pullusb 0160b4eae4 keymap paint mode name 2024-11-11 17:49:22 +01:00
pullusb 1dfb8cff9c add intermediate stroke container - strokes to drawing.strokes 2024-11-11 17:48:11 +01:00
pullusb 4732110b93 multi_frame editing changed to scene toolsetting property 2024-11-11 17:34:47 +01:00
pullusb 7bc7d5d9ff fix brush context menu 2024-11-11 17:32:58 +01:00
pullusb 998bd4b0cb active_frame attribute to current_frame method 2024-11-11 17:30:33 +01:00
pullusb 25adb5beb6 partial update of GP menu types 2024-11-11 17:27:57 +01:00
pullusb 6e94ee270d point attribute co to position 2024-11-11 16:23:11 +01:00
pullusb 3ece64e517 gpv3 update info to name property 2024-11-11 15:56:43 +01:00
pullusb eae69b6f75 update to gpv3 context modes 2024-11-11 15:47:33 +01:00
pullusb f5a78601b6 Begin port to GPv3 - change GP type 2024-11-11 15:35:39 +01:00
pullusb 98ed92afe2 add link to last gpv2 release 2024-11-11 15:35:27 +01:00
pullusb c02b890915 Add Copy material to layer
3.3.0

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

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

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

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

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

- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2024-02-20 16:07:20 +01:00
pullusb 4fadf35163 add step select gp frames feature
2.5.0

- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
2024-01-24 17:54:14 +01:00
pullusb 429179d936 apply get-draw_plane func changes 2024-01-18 19:32:05 +01:00
pullusb 053a9d7f7b Faster and better batch reproject
2.4.0

- changed: Batch reproject consider camera movement and is almost 8x faster
- added: Batch reproject have "Current" mode (using current tool setting)
2024-01-18 19:24:52 +01:00
pullusb 280a575631 fix json palette export
2.3.4

- fixed: bug when exporting json palettes containing empty material slots
2023-09-27 14:54:56 +02:00
pullusb aa0eb9bd25 update readmes 2023-05-09 17:45:27 +02:00
pullusb 2191be97cb bugfix animation data ui
2.3.3

- fixed: Bug with animation manager objects data
2023-05-03 12:16:15 +02:00
pullusb 990b18f665 Fix animation manager with empty objects
2.3.2

- fixed: Bug with animation manager when there is image.empty object in scene
2023-05-02 14:43:28 +02:00
pullusb 798afbe82a Animation manager improve animated hint
2.3.1

- changed: Animation manager show icons when animation is enabled: fully, partially or not at all.
2023-03-09 17:53:40 +01:00
pullusb 7e92ae182e Anim manager alert when anim off and affect groups
2.3.0

- added: Animation manager buttons are colored red when objects have disabled animation
- fixed: Animation manager not enabling/disabling Action Groups
- fixed: Animation manager `List Disabled Anims` list groups as well
2023-03-09 14:52:58 +01:00
pullusb 9a49175859 Fix type error
2.2.3

- fixed: Type error on realign ops
2023-03-06 16:37:17 +01:00
Pullusb eaf9c3b22f fix draw cam with multi cams
2.2.2

- fixed: draw_cam data not changed when working with multiple camera in a shot
2023-02-23 19:15:55 +01:00
pullusb 8e223b9f3a add view3d utility
2.2.1

- added: class View3D to calculate area 3d related coordinates
2023-02-14 18:49:48 +01:00
pullusb d1b03c804c file checker remove stroke duplicates
2.2.0

- added: _Remove redundant stroke_ in File checker (Just list duplicate numbers in "check only" mode)
2023-02-07 12:55:48 +01:00
pullusb d1748c592d Clean whitespaces 2023-02-07 12:03:00 +01:00
pullusb 652c4632a9 prevent picker register on background mode 2023-01-16 17:58:34 +01:00
pullusb 80ec798144 prevent keymap register in background mode
2.1.6

- Fixed: Prevent some keymaps to register when blender is launched in background mode
2023-01-16 17:56:51 +01:00
pullusb 4ae5d3a55b udpate log 2.1.5 2023-01-11 11:25:01 +01:00
pullusb 6506536fb2 fix modifier layer target check
2.1.5

- fixed: false positive with checkfile in modifier layer target
2023-01-11 11:24:27 +01:00
pullusb 5dbd59652e update readme with prefix tooltip 2023-01-05 12:34:50 +01:00
pullusb 5d55afcf4d enbable msgbus at first activation
2.1.4

- fixed: layer change msgbus not working at first activation of the addon
2023-01-05 12:28:35 +01:00
pullusb b370dd7344 fix namespace pref lists and add a reset button
2.1.3

- fixed: decoralate Prefix and Suffix UI_lists scroll
- fixed: Problem when settings project namespaces
- added: Button to reset project namespace (to have the right order)
2022-11-30 17:58:12 +01:00
pullusb b2a6e6a899 Set Brush operator
2.1.2

- added: `gp.brush_set` operator to manually assign a brush by name to a shortcut
  - preferably add shortcut to `Grease Pencil > Grease Pencil Stroke Paint Mode`
2022-11-23 15:25:28 +01:00
Pullusb bf30254871 imporve and expose follow curve offset
2.1.1

- added: follow curve show offset property in UI
- added: follow curve show clickable warning if object has non-zero location to reset location
- changed: created follow curve use `fixed offset`
2022-11-15 18:36:21 +01:00
Pullusb ef566c494f go edit mode func 2022-11-10 18:51:00 +01:00
Pullusb a021535b3f Add curve follow path creation and management
2.1.0

- added: 3 actions buttons:
  - create curve with follow path and go into curve edit
  - go back to object
  - got to curve edit (if follow path constraint exists with a curve target)
  - if follow path exists, button to remove constraint
2022-11-09 18:59:22 +01:00
Pullusb 336d1b264c initial code for follow curve 2022-11-04 18:45:37 +01:00
Pullusb 4622aa4520 fix prefix set by env
2.0.11

- fix: prefix set by project environment
2022-10-25 17:24:05 +02:00
Pullusb 88cdbf88cd better readability on preference checklist 2022-10-22 16:23:01 +02:00
Pullusb 56a83910bd translate edit line opacity description 2022-10-22 11:39:45 +02:00
Pullusb caa3187772 fix poll errors
2.0.10

- fix: poll error in console
2022-10-16 20:13:52 +02:00
Pullusb e0a50ea49a ui redraw on UIlist actions and code cleanup
2.0.9

- fix: prefix/suffix UIlist actions trigger UI redraw to see changes live
- changed: PropertyGroups are now registered in their own file
- code: cleanup
2022-10-13 00:09:12 +02:00
Pullusb c32ea207c6 Finish prefix and suffix as UI list
2.0.8

- changed: suffix as UIlist in prefs,
- fixed: prefix and suffix register and load
2022-10-09 00:28:14 +02:00
Pullusb 515ba4aa9f fix gp layer navigation autofade
2.0.7

- fix: broken auto-fade with gp layer navigation when used with a customized shortcut.
- changed: supported version number to 3.0.0
2022-10-08 15:46:44 +02:00
Pullusb 02d13c6f29 New project and user prefix management
2.0.6

- changed: Use prefixes toggle is enabled by default
- changed: prefixes are now set in preferences as a reorderable UI list with full name for description
  - This should not affect `PREFIXES` env variable (`'CO, LN'`)
  - Now can be also passed as prefix:name pair ex: `'CO : Color, LN : Line`
2022-10-08 01:15:34 +02:00
Pullusb 8e3b3871e6 code fix on operators and draw cam handler
2.0.5

- changed: redo panel for GP layer picker has better name and display active layer name
- changed: some operator id_name to expose "add shortcut" in context menu for some button
- fixed: error with draw_cam handler when no camera is active
2022-10-07 02:20:28 +02:00
Pullusb 43a28876d1 initial code for namespace prefix lists 2022-10-03 00:08:10 +02:00
Pullusb 050e8a405e X-ray to In Front to match orignal prop name 2022-09-28 11:10:32 +02:00
Pullusb 762b92fbfe choose pts attribute to dump in layer copy
2.0.2

- added: possibility to select which point attribute is copied by GP layer copy to clipboard
2022-07-01 18:14:07 +02:00
Pullusb e1681cd6ad separate anim disable for cameras
2.0.1

- added: enable/disable camera animation separately in `animation manager`
  - for cam,disable properties like focal and shift (to be able to work on a fixed cam)
  - camera is not disable using `obj` anymore
2022-06-23 15:43:25 +02:00
Pullusb 7092bbf1be readme fr fixes 2022-04-18 17:13:01 +02:00
Pullusb 308b41e401 Update shortcut list in readme_FR 2022-04-18 17:01:04 +02:00
Pullusb 5574c75769 playblast dest
2.0.0

- added: possibility to change playblast destination in preferences
- changed: playblast default is `playblast` folder instead of `images`
- changed: completely disable experimental precise eraser
2022-03-24 18:25:56 +01:00
Pullusb bf60370ab0 Fix playblast with api changes
2.0.0

- fix: error using playblast (api changes of Blender 3.0+)
2022-03-24 14:41:32 +01:00
Pullusb f36b31e3aa completed fix #5 for gp layer data action toggle
1.9.9

- fix: Bug setting GP layers actions on/off
- fix: Add GP layers animation datas in `list disabled animation` report
2022-03-05 15:36:01 +01:00
Pullusb 26c71f7bb3 fix #5 animation manager for gp layer data
1.9.8

- fix: animation manager `GP anim on/off` also toggle layers animation datas
2022-03-04 02:18:41 +01:00
Pullusb 8404e16633 upgraded modifier visibility conflict checker
1.9.7

- changed: `list modifiers visibility conflict` (used in `check file` or through search menu) now not limited to GP objects type.
2022-02-27 22:47:48 +01:00
Pullusb 8c49c1efcb fix icon issue and cleaned init-register
1.9.6

- fix: icon removed in 3.0
2022-02-22 11:29:31 +01:00
Pullusb eccb450081 cehck broken modifier layer targets
1.9.5

- added: `Check file` also check if GP modifiers have broken layer target
- changed: `Check file` disable drawing guide is now On by default
2022-02-02 17:08:41 +01:00
Pullusb 651f9e7747 Layer name manager fix modifier targets
1.9.4

- feat: renaming using the name_manager take care of changing layer target values in GP modifiers
2022-01-25 15:13:08 +01:00
Pullusb 1145b73b58 Add git updater (beta)
1.9.3

- feat: Add an update button at bottom of preferences if addon is a cloned repository
2022-01-23 16:27:30 +01:00
Pullusb cab3a91f70 mat exclude by name for palette link
1.9.2

- feat: Palette linker has a name exclusion list in preferences
  - Avoid linking some material that are prefered local to file
  - Default item in list : `line`
2022-01-19 11:36:45 +01:00
Pullusb 7985844226 Add disable use light in file checker
1.9.1

- fix: add error handling on palette linker when blend_path isn't valid anymore
- added: file checker entry, Disable all GP object `use lights` (True by default in addon pref `checklist`)
- added: WIP of Batch reproject all on cursor.
  - No UI. In search menu `Flat Reproject Selected On cursor` (idname `gp.batch_flat_reproject`)
2022-01-18 22:53:08 +01:00
Pullusb 4ac743cd32 doc update 2021-12-22 19:25:35 +01:00
Pullusb 97b09444ab Layer actions and navigations features
1.9.0

- feat: New shortcuts:
  - `F2` in Paint and Edit to rename active layer
  - `Insert` add a new layer (same as Krita)
  - `Shift + Insert` add a new layer and immediately pop-up a rename box
  - `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut
2021-12-22 14:11:31 +01:00
Pullusb 3c7477c442 Fix #4 paste layers skipping empty frames
1.8.1

- fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore
2021-12-17 23:26:48 +01:00
Pullusb 6c19fa54af Palette linker
1.8.0

- feat: palette linker (beta), with pop-up from material stack dropdown
- feat: palette name fuzzy match
- code: add an open addon preference ops
2021-12-04 13:57:32 +01:00
Pullusb 78b70c8fca fix: reset draw cam rotation
1.7.8

- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)
- code: initial enhancement for palette linking
2021-11-25 17:00:09 +01:00
Pullusb 9389faed22 check links copy path
1.7.7

- feat: add copy path to `check link` ops with multiple path representation choices
2021-11-18 21:14:58 +01:00
Pullusb 204553ed47 Expose keymaps
1.7.6

- ui: expose (almost) all keymap added by the addon to allow user for customize/disable as needed
- ui: changed some names
2021-10-29 16:49:38 +02:00
Pullusb 44ccb3d146 select/set color/prefix improved
1.7.5

- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
2021-10-28 14:33:37 +02:00
Pullusb 56cbc04c65 paint mode precise layer picker on W
1.7.4

- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
2021-10-26 02:24:19 +02:00
Pullusb a0ed941e4b gp modifier hide toggle
1.7.3

- added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`)
2021-10-20 21:17:37 +02:00
Pullusb 4c6bd20b60 Check file visibility conflicts check
1.7.2

- added: `Object visibility conflict` in file check
  - print in console when object have render activated but not viewport (& vice-versa)
  - Standalone ops "List Object Visibility Conflicts" (`gp.list_object_visibility`)
- added: `GP Modifier visibility conflict` in file check.
  - print in console when gp modifiers have render activated but not viewport (& vice-versa)
  - Standalone ops "List GP Modifiers Visibility Conflicts" (`gp.list_modifier_visibility`)
- code: show_message_box utils can now receive operator in sublist (if 3 element)
2021-10-20 20:54:59 +02:00
Pullusb b62a23858c improved Filtered create empty frames ops
1.7.1

- feat: Improved `Create Empty Frames` operator with mutliple filters to choose source layers
2021-10-20 16:05:56 +02:00
Pullusb f7e8dce0ff stripped obsolete features and compacted UI
1.7.0

- remove: Obsolete operators and panels
  - Remove "line closer" panel as it's been a native tool for a while in 2.9x (stop register of `GP_guided_colorize > OP_line_closer`)
  - Remove "Render" subpanel, obsolete and not adapted to production
- ui: clean and refactor
  - Gp clipboard panel and aimation Manager subpanel layout to aligned columns (gain space)
  - add `GP` on each panel name for wuick eye search
  - follow cursor now in animation manager subpanel (should be in an extra menu or removed in the end)
2021-10-20 13:56:01 +02:00
Pullusb 8b838b52ca material pick with quick press and add enum filter
1.6.9

- change: material picker (`S` and `Alt+S`) quick trigger, cahnge is only triggered if ley is pressed less than 200ms
  - this is made to let other operator use functionality on long press using `S`
- feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all)
  - by default `S` is still fill only
  - but `Alt+S` is now stroke only instead of all
2021-10-18 19:11:17 +02:00
Pullusb d4ea39fb40 fix some batch reproject issues
1.6.8

- fix: reproject GP repeating projection on same frames
- fix: batch reproject all frame not poping dialog menu
2021-09-27 17:35:40 +02:00
Pullusb ea772b297a fix a poll error
1.6.7

- fix: error with operator `OP_key_duplicate_send` poll flooding console
2021-09-25 20:03:02 +02:00
Pullusb 884ee2a695 fix getenv resolution
1.6.6

- fix: inverted scene resolution from project environnement
2021-09-20 12:21:04 +02:00
Pullusb a9ed3f7e79 Check canvas alignement
1.6.5

- feat: check canvas alignement of the Gp object compare to chosen draw axis
2021-09-15 01:35:18 +02:00
Pullusb dd68e74e2b fix: disable multi-selection layer renaming
1.6.4

- fix: disable multi-selection for layer naming manager
  - the dopesheet selection sometimes still consider layer as selected
2021-09-09 12:23:14 +02:00
Pullusb 9d26ed4085 button manual load environnement namespace
1.6.3
2021-09-07 15:41:54 +02:00
Pullusb 1017c01668 fix bg open error
1.6.2

- disable keymap register for breakdowner on background
2021-08-30 10:40:38 +02:00
Pullusb f05e4fe41d remove auto-updater line 2021-08-23 15:43:59 +02:00
Pullusb 408f017e81 removed updater
1.6.1

- removed: Auto Updater that was failing since 2.93
- prefs: add a checkbox to disable the "load base palette button" in UI.
2021-08-23 14:50:00 +02:00
Pullusb e6ff824684 fix 2021-07-30 18:24:54 +02:00
Pullusb 985e395beb picker on visible 2021-07-29 11:20:43 +02:00
Pullusb a218aefd10 mat picker - remove undo and info report 2021-07-28 10:40:50 +02:00
Pullusb 3974a15ff0 layer group - named regex - fill tool mat picker
1.6.0

- feat: Namespace upgrade
  - support pseudo group naming
  - add group and indent button
- feat: Fill tool shortcut for material color picker (from closest stroke)
  - `S` : get material closest *fill* stroke
  - `Àlt+S` : get closest stroke (fill or stroke)
2021-07-27 18:48:38 +02:00
Pullusb 7ff96ad205 changed error message 2021-07-21 12:31:55 +02:00
Pullusb d480fa0817 typo 2021-07-20 18:58:46 +02:00
Pullusb 212436c451 namespace improvement
1.5.8

- feat: Namespace improvement:
  - new suffixes list that generate suffix buttons
  - dynamic layer name field that show active (msgbus)
  - possible to disable the panel with an option
2021-07-20 18:53:39 +02:00
Pullusb 9612c84396 Check file dry run mode and lock object mode
1.5.7

- feat: check list, Specify in addon pref if you prefer a dry run (check without set)
- feat: check file can change the lock object mode state (do nothing by default)
2021-07-16 18:14:31 +02:00
Pullusb 66ef75c76d gp layer batch rename find-replace
1.5.6

- feat: layer drop-down menu include an operator for batch find/replace GP layers (idname: `gp.rename_gp_layers`)
2021-07-13 16:23:39 +02:00
Pullusb 75c8cb9206 version bump 2021-06-28 12:49:06 +02:00
Pullusb e44607bc2c Merge branch 'master' of https://gitlab.com/autour-de-minuit/blender/gp_toolbox 2021-06-28 12:43:01 +02:00
Pullusb fdd3e2caef add filepath mapping check
1.5.4

- feat: check file: add check for filepath mapping (if all relative or all absolute)
- change: check file: disable resolution set by default
2021-06-28 12:41:56 +02:00
Pullusb 801e14cb7a layer selection management by prefix or color.
1.5.4

- feat: Layer manager
  - select/set layer prefix
  - select/set layer color
- code: refactor name builder function
2021-06-25 18:18:42 +02:00
Pullusb 15aec65b19 Select tool prop to dix prop group and UI change 2021-06-24 16:40:22 +02:00
Pullusb 0740e835d8 first verison of layer name builder
1.5.3

- feat: quick-prefix for layer using pref separator
  - list editable in addon pref
  - add interface button above layers
- code: added environnement variable for prefix and separator:
  - `PREFIXES` : list of prefix (comma separated uppercase letters between 1 and 6 character, ex: 'AN,SP,L')
  - `SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
- UI: add addon prefs namespace ui-box in project settings
2021-06-24 16:12:20 +02:00
Pullusb f599964296 fix load brush import helper destination 2021-06-23 17:25:45 +02:00
Pullusb 4a4d72b410 fix env load 2021-06-23 16:46:30 +02:00
Pullusb 253d1501a1 add environnement variable
1.5.2

- add environnement variables to set addon preferences project settings at register through `os.getenv('KEY')`:
  - `RENDER_WIDTH` : resolution x
  - `RENDER_HEIGHT` : resolution y
  - `FPS` : project frame rate
  - `PALETTES` : path to the blends (or json) containing materials palettes
  - `BRUSHES` : path to the blend containing brushes to load
2021-06-23 16:30:30 +02:00
44 changed files with 7736 additions and 4964 deletions

View File

@ -1,5 +1,412 @@
# Changelog
4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference):
- list potential change and display an `Apply Fix`
changed: Enhanced visibility conflict list:
- also include viewlayer hide value
- allow to set all hide value from the state of one of the three
- fixed: material move operator
4.0.1
- fixed: layer nav operator on page up/down
4.0.0
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
3.2.0
- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
3.1.0
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
3.0.2
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
3.0.1
- fixed: Crash after generating empty frames
3.0.0
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2.5.0
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
2.4.0
- changed: Batch reproject consider camera movement and is almost 8x faster
- added: Batch reproject have "Current" mode (using current tool setting)
2.3.4
- fixed: bug when exporting json palettes containing empty material slots
2.3.3
- fixed: Bug with animation manager objects data
2.3.2
- fixed: Bug with animation manager when there is empty object in scene
2.3.1
- changed: Animation manager show hints when animation is enabled: fully, partially or not at all.
2.3.0
- added: Animation manager buttons are colored red when objects have disabled animation
- fixed: Animation manager not enabling/disabling Action Groups
- fixed: Animation manager `List Disabled Anims` list groups as well
2.2.3
- fixed: Type error on realign ops
2.2.2
- fixed: draw_cam data not changed when working with multiple camera in a shot
2.2.1
- added: class View3D to calculate area 3d related coordinates
2.2.0
- added: _Remove redundant stroke_ in File checker (Just list duplicate numbers in "check only" mode)
2.1.6
- fixed: Prevent some keymaps to register when blender is launched in background mode
2.1.5
- fixed: false positive with checkfile in modifier layer target
2.1.4
- fixed: layer change msgbus not working at first activation of the addon
2.1.3
- fixed: decoralate Prefix and Suffix UI_lists scroll
- fixed: Problem when settings project namespaces
- added: Button to reset project namespace (to have the right order)
2.1.2
- added: `gp.brush_set` operator to manually assign a brush by name to a shortcut
- preferably add shortcut to `Grease Pencil > Grease Pencil Stroke Paint Mode`
2.1.1
- added: follow curve show offset property in UI
- added: follow curve show clickable warning if object has non-zero location to reset location
- changed: created follow curve use `fixed offset`
2.1.1
- added: follow curve show offset property in UI
- added: follow curve show clickable warning if object has non-zero location to reset location
- changed: created follow curve use `fixed offset`
2.1.0
- added: 3 actions buttons:
- create curve with follow path and go into curve edit
- go back to object
- got to curve edit (if follow path constraint exists with a curve target)
- if follow path exists, button to remove constraint
2.0.11
- fix: prefix set by project environment
2.0.10
- fix: poll error in console
2.0.9
- fix: prefix/suffix UIlist actions trigger UI redraw to see changes live
- changed: PropertyGroups are now registered in their own file
- code: cleanup
2.0.8
- changed: suffix as UIlist in prefs,
- fix: prefix and suffix register and load
2.0.7
- fix: broken auto-fade with gp layer navigation when used with a customized shortcut.
- changed: supported version number to 3.0.0
2.0.6
- changed: Use prefixes toggle is enabled by default
- changed: prefixes are now set in preferences as a reorderable UI list with full name for description
- This should not affect `PREFIXES` env variable (`'CO, LN'`)
- Now can be also passed as prefix:name pair ex: `'CO : Color, LN : Line`
2.0.5
- changed: redo panel for GP layer picker has better name and display active layer name
- changed: some operator id_name to expose "add shortcut" in context menu for some button
- fix: error with draw_cam handler when no camera is active
2.0.3
- changed: `X-ray` to `In Front`, match original object property name
2.0.2
- added: possibility to select which point attribute is copied by GP layer copy to clipboard
2.0.1
- added: enable/disable camera animation separately in `animation manager`
- for cam,disable properties like focal and shift (to be able to work on a fixed cam)
- camera is not disable using `obj` anymore
2.0.0
- fix: error using playblast (api changes of Blender 3.0+)
- added: possibility to change playblast destination in preferences
- changed: playblast default is `playblast` folder instead of `images`
- changed: completely disable experimental precise eraser
1.9.9
- fix: Bug setting GP layers actions on/off
- fix: Add GP layers animation datas in `list disabled animation` report
1.9.8
- fix: animation manager `GP anim on/off` also toggle layers animation datas
1.9.7
- changed: `list modifiers visibility conflict` (used in `check file` or through search menu) now not limited to GP objects type.
1.9.6
- fix: icon removed in 3.0
1.9.5
- added: `Check file` also check if GP modifiers have broken layer target
- changed: `Check file` disable drawing guide is now On by default
1.9.4
- feat: renaming using the name_manager take care of changing layer target values in GP modifiers
1.9.3
- feat: Add an update button at bottom of preferences if addon is a cloned repository
1.9.2
- feat: Palette linker has a name exclusion list in preferences
- Avoid linking some material that are prefered local to file
- Default item in list : `line`
1.9.1
- fix: add error handling on palette linker when blend_path isn't valid anymore
- added: file checker entry, Disable all GP object `use lights` (True by default in addon pref `checklist`)
- added: WIP of Batch reproject all on cursor.
- No UI. In search menu `Flat Reproject Selected On cursor` (idname `gp.batch_flat_reproject`)
1.9.0
- feat: New shortcuts:
- `F2` in Paint and Edit to rename active layer
- `Insert` add a new layer (same as Krita)
- `Shift + Insert` add a new layer and immediately pop-up a rename box
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut
1.8.1
- fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore
1.8.0
- feat: palette linker (beta), with pop-up from material stack dropdown
- feat: palette name fuzzy match
- code: add an open addon preference ops
1.7.8
- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)
1.7.7
- feat: add copy path to `check link` ops with multiple path representation choices
1.7.6
- ui: expose (almost) all keymap added by the addon to allow user for customize/disable as needed
- ui: changed some names
1.7.5
- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
- add undo step for `W`'s select layer from closest stroke
1.7.4
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
- fix: copy-paste keymap error on background rendering
1.7.3
- added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`)
1.7.2
- added: `Object visibility conflict` in file check
- print in console when object have render activated but not viewport (& vice-versa)
- Standalone ops "List Object Visibility Conflicts" (`gp.list_object_visibility`)
- added: `GP Modifier visibility conflict` in file check.
- print in console when gp modifiers have render activated but not viewport (& vice-versa)
- Standalone ops "List GP Modifiers Visibility Conflicts" (`gp.list_modifier_visibility`)
- code: show_message_box utils can now receive operator in sublist (if 3 element)
1.7.1
- feat: Improved `Create Empty Frames` operator with mutliple filters to choose source layers
1.7.0
- removed: Obsolete operators and panels
- Remove "line closer" panel as it's been a native tool for a while in 2.9x (stop register of `GP_guided_colorize > OP_line_closer`)
- Remove "Render" subpanel, obsolete and not adapted to production
- ui: clean and refactor
- Gp clipboard panel and aimation Manager subpanel layout to aligned columns (gain space)
- add `GP` on each panel name for wuick eye search
- follow cursor now in animation manager subpanel (should be in an extra menu or removed in the end)
1.6.9
- change: material picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
- this is made to let other operator use functionality on long press using `S`
- feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all)
- by default `S` is still fill only
- but `Alt+S` is now stroke only instead of all
1.6.8
- fix: reproject GP repeating projection on same frames
- fix: batch reproject all frame not poping dialog menu
1.6.7
- fix: error with operator `OP_key_duplicate_send` poll flooding console
1.6.6
- fix: inverted scene resolution from project environnement
1.6.5
- feat: check canvas alignement of the Gp object compare to chosen draw axis
1.6.4
- fix: disable multi-selection for layer naming manager
- the dopesheet selection sometimes still consider layer as selected
1.6.3
<!-- - abort :: add checkbox to prevent from loading project environnement namespace -->
- add buttons to load manually environnement namespace
1.6.2
- disable keymap register for breakdowner on background
1.6.1
- removed: Auto Updater that was failing since 2.93
- prefs: add a checkbox to disable the "load base palette button" in UI.
1.6.0
- feat: Namespace upgrade
- support pseudo group naming
- add group and indent button
- feat: Fill tool shortcut for material color picker (from closest visible stroke)
- `S` : get material closest *fill* stroke
- `Àlt+S` : get closest stroke (fill or stroke)
1.5.8
- feat: Namespace improvement:
- new suffixes list that generate suffix buttons
- dynamic layer name field that show active (msgbus)
- possible to disable the panel with an option
1.5.7
- feat: check list, specify in addon pref if you prefer a dry run (check without set)
- feat: check file can change the lock object mode state (do nothing by default)
1.5.6
- feat: layer drop-down menu include an operator for batch find/replace GP layers (idname: `gp.rename_gp_layers`)
1.5.5
- feat: check file: add check for filepath mapping (if all relative or all absolute)
- change: check file: disable resolution set by default
1.5.4
- feat: Layer manager
- select/set layer prefix
- select/set layer color
- code: refactor name builder function
1.5.3
- feat: layer aquick-prefix for layer using pref separator
- list editable in addon pref
- add interface button above layers
- code: added environnement variable for prefix and separator:
- `PREFIXES` : list of prefix (comma separated uppercase letters between 1 and 6 character, ex: 'AN,SP,L')
- `SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
- UI: add addon prefs namespace ui-box in project settings
1.5.2
- add environnement variables to set addon preferences project settings at register through `os.getenv('KEY')`:
- `RENDER_WIDTH` : resolution x
- `RENDER_HEIGHT` : resolution y
- `FPS` : project frame rate
- `PALETTES` : path to the blends (or json) containing materials palettes
- `BRUSHES` : path to the blend containing brushes to load
1.5.1
- fix: eraser brush
@ -38,29 +445,29 @@
- feat: show main cam frame when in draw_cam
1.3.2:
1.3.2
- change: disable manip cam name drawing
- code: add initial support for main cam frame draw within camera view
1.3.1:
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:
1.3.0
- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
1.2.2:
1.2.2
- fix: realign anim return error
1.2.1:
1.2.1
- fix: Breakdowner initial error check
1.2.0:
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
@ -69,67 +476,67 @@
- doc: Added changelog file (moved list from readme)
- doc: relative link to changelog and FR_readme in main readme
1.1.0:
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:
1.0.9
- feat: Reproject all frames operator
1.0.8:
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:
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:
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:
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:
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:
1.0.2
- pref: Added option to disable always remap relative on save in addon-preference
1.0.1:
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:
1.0.0
- Compatible with official grease pencil tools
- removed box deform and rotate canvas that existed in other
0.9.3:
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:
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:
0.9.1
- Public release
- prefs: added fps as part of project settings
@ -139,38 +546,38 @@
- doc: Added fully-detailed french readme
0.8.0:
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:
0.7.2
- fix: Palette importer bug
0.7.0:
0.7.0
- feat: auto create empty frame on color layer
0.6.3:
0.6.3
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
0.6.2:
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:
0.6.1
- feat: render objects grouped, one anim render with all ticked object using manual output name
0.6.0:
0.6.0
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
0.5.9:
0.5.9
- feat: render exporter
- Render a selection of GP object isolated from the rest
@ -180,21 +587,21 @@
- 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:
0.5.8
- feat: GP material append on active object from single blend file
0.5.7:
0.5.7
- Added warning message for cursor snapping
0.5.5 - 0.5.6:
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:
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)
@ -202,19 +609,19 @@
- shift clic to target selection only
- check file: added disabled fcurved counter alert with detail in console
0.5.3:
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:
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:
0.5.1
- fix: error when empty material slot on GP object.
- fix: cursor snap on GP canvas when GP is parented
@ -229,11 +636,11 @@
- set show slider and sync range
- set fps to 24
0.4.6:
0.4.6
- feat: basic Palette manager with base material check and warning
0.4.5:
0.4.5
- open blender config folder from addon preference
- fix: obj cam parent on selected object
@ -242,29 +649,29 @@
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
0.4.4:
0.4.4
- feat: added cursor follow handlers and UI toggle
0.4.3:
0.4.3
- change playblast out to 'images' and add playblast as name prefix
0.4.2:
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:
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:
0.4.0
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
0.3.8:
0.3.8
- UI: expose onion skin in interface
- UI: expose autolock in interface
@ -272,47 +679,47 @@
- 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:
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:
0.3.6
- UI: Stoplines : add a button for quickly set stoplines visibility.
0.3.5:
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:
0.3.3
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
0.3.3:
0.3.3
- version 1 beta of gmic colorize
- variant of `screen.gp_keyframe_jump` through keymap seetings
0.3.0:
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:
0.2.3
- add operator to `screen.gp_keyframe_jump`
- add shortcut to rotate canvas
- fix duplicate class
0.2.2:
0.2.2
- separated props resolution_percentage parameter
- playblast options for launching folder and opening folder
0.2.1:
0.2.1
- playblast feature
- Button to go zoom 100% or fit screen
@ -320,22 +727,22 @@
- 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:
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:
0.1.5
- added CGC-auto-updater
0.1.3:
0.1.3
- flip cam x
- inital stage of overlay toggle (need pref/multiple pref)
0.1.2:
0.1.2
- subpanel of GP data (instead of direct append)
- initial commit with GP pseudo color

View File

@ -13,7 +13,7 @@
import bpy
from . import OP_line_closer
# from . import OP_line_closer
from . import OP_create_empty_frames
@ -45,11 +45,11 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
OP_create_empty_frames.register()
OP_line_closer.register()
# OP_line_closer.register()
bpy.types.Scene.gpcolor_props = bpy.props.PointerProperty(type = GPCOLOR_PG_settings)
def unregister():
OP_line_closer.unregister()
# OP_line_closer.unregister()
OP_create_empty_frames.unregister()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,33 +1,199 @@
## Create empty keyframe where keyframe exists in layers above.
import bpy
from bpy.props import (FloatProperty,
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
from .. import utils
from ..utils import is_hidden
## copied from OP_key_duplicate_send
def get_layer_list(self, context):
'''return (identifier, name, description) of enum content'''
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
def get_group_list(self, context):
return [(g.name, g.name, '') for g in context.object.data.layer_groups]
class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames"
bl_label = "Create empty frames"
bl_description = "Create new empty frames on active layer where there is a frame in layer above\n(usefull in color layers to match line frames)"
bl_label = "Create Empty Frames"
bl_description = "Create new empty frames on active layer where there is a frame in targeted layers\
\n(usefull in color layers to match line frames)"
bl_options = {'REGISTER','UNDO'}
layers_enum : EnumProperty(
name="Empty Keys from Layer",
description="Reference keys from layer",
items=get_layer_list
)
groups_enum : EnumProperty(
name="Empty Keys from Group",
description="Duplicate keys from group",
items=get_group_list
)
targeted_layers : EnumProperty(
name="Sources", # Empty keys from targets
description="Duplicate keys as empty on current layer from selected targets",
default="ALL_ABOVE",
items=(
('ALL_ABOVE', 'All Layers Above', 'Empty frames from all layers above'),
('ALL_BELOW', 'All Layers Below', 'Empty frames from all layers below'),
('NUMBER', 'Number Above Or Below', 'Positive number above layers\nNegative number below layers'),
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'),
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
)
)
range : EnumProperty(
name="Range",
description="Restraint empty copy from a defined range",
default="FULL",
items=(
('FULL', 'Full range', 'Empty frames from all layers above'),
('BEFORE', 'Before Time Cursor', 'Empty frames from all layers below'),
('AFTER', 'After Time Cursor', 'Only After time cursor'),
('SCENE', 'On scene range', 'Restric to Scene/Preview range'),
)
)
number : IntProperty(name='Number',
default=1,
description='Number of layer to create empty key from\nabove (positive) or layer below (negative)',
options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
def invoke(self, context, event):
# Possible preset with shortcut
# if event.alt:
# self.targeted_layers = 'ALL_VISIBLE'
gp = context.grease_pencil
layer_from_group = None
if gp.layer_groups.active:
layer_from_group = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
## Can just do if not utils.get_closest_active_layer(context.grease_pencil):
if not gp.layers.active and not layer_from_group:
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
return {'CANCELLED'}
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
# layout.label(text='Create Empty Frames From Other Layers')
# target
layout.prop(self, 'targeted_layers')
if self.targeted_layers == 'CHOSEN':
if self.layers_enum:
layout.prop(self, 'layers_enum')
else:
layout.label(text='No other layers to match keyframe!', icon='ERROR')
if self.targeted_layers == 'CHOSEN_GROUP':
if self.groups_enum:
layout.prop(self, 'groups_enum')
else:
layout.label(text='No other groups to match keyframe!', icon='ERROR')
elif self.targeted_layers == 'NUMBER':
row = layout.row()
row.prop(self, 'number')
row.active = self.number != 0
if self.number == 0:
layout.label(text="Can't have 0 as value")
layout.separator()
layout.prop(self, 'range')
if self.range == 'SCENE':
if context.scene.use_preview_range:
layout.label(text='Using preview range', icon='INFO')
def execute(self, context):
obj = context.object
gpl = obj.data.layers
gpl.active_index
gp = obj.data
gpl = gp.layers
## Only possible on 'fill' layer ??
# if not 'fill' in gpl.active.info.lower():
# self.report({'ERROR'}, f"There must be 'fill' text in layer name")
# return {'CANCELLED'}
if gp.layer_groups.active:
reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
else:
reference_layer = gpl.active
active_index = next((i for i, l in enumerate(gpl) if l == reference_layer), None)
print(self.targeted_layers)
if self.targeted_layers == 'ALL_ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i > active_index]
elif self.targeted_layers == 'ALL_BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i < active_index]
elif self.targeted_layers == 'ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1]
elif self.targeted_layers == 'BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
elif self.targeted_layers == 'ALL_VISIBLE':
tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
elif self.targeted_layers == 'CHOSEN':
if not self.layers_enum:
self.report({'ERROR'}, f"No chosen layers, aborted")
return {'CANCELLED'}
tgt_layers = [l for l in gpl if l.name == self.layers_enum]
elif self.targeted_layers == 'CHOSEN_GROUP':
if not self.groups_enum:
self.report({'ERROR'}, f"No chosen groups, aborted")
return {'CANCELLED'}
group = gp.layer_groups.get(self.groups_enum)
tgt_layers = [l for l in gpl if l.parent_group == group]
elif self.targeted_layers == 'NUMBER':
if self.number == 0:
self.report({'ERROR'}, f"Can't have 0 as value")
return {'CANCELLED'}
l_range = active_index + self.number
print('l_range: ', l_range)
if self.number > 0: # positive
tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range]
else:
tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range]
if not tgt_layers:
self.report({'ERROR'}, f"No layers found with chosen Targets")
return {'CANCELLED'}
frame_id_list = []
for i, l in enumerate(gpl):
# don't list layer below
if i <= gpl.active_index:
continue
# print(l.info, "index:", i)
for l in tgt_layers:
for f in l.frames:
## frame filter
if self.range != 'FULL': # FULl = No filter
if self.range == 'BEFORE':
if not f.frame_number <= context.scene.frame_current:
continue
elif self.range == 'AFTER':
if not f.frame_number >= context.scene.frame_current:
continue
elif self.range == 'SCENE':
if context.scene.use_preview_range:
if not context.scene.frame_preview_start <= f.frame_number <= context.scene.frame_preview_end:
continue
else:
if not context.scene.frame_start <= f.frame_number <= context.scene.frame_end:
continue
frame_id_list.append(f.frame_number)
frame_id_list = list(set(frame_id_list))
@ -40,17 +206,17 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if num in current_frames:
continue
#Create empty frame
gpl.active.frames.new(num, active=False)
gpl.active.frames.new(num)
fct += 1
gpl.update()
if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
else:
self.report({'WARNING'}, f"No frames to create !")
return {'FINISHED'}
def register():
bpy.utils.register_class(GP_OT_create_empty_frames)

View File

@ -5,11 +5,14 @@ from ..utils import (location_to_region,
vector_length,
draw_gp_stroke,
extrapolate_points_by_length,
simple_draw_gp_stroke)
simple_draw_gp_stroke,
is_hidden,
is_locked)
import bpy
from math import degrees
from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
@ -53,11 +56,14 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter = defaultdict(list)
plist = []
matrix = ob.matrix_world
for s in f.strokes:#add first and last
for s in f.drawing.strokes: #add first and last
smat = ob.material_slots[s.material_index].material
if not smat:continue#no material on line
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
if len(s.points) < 2:continue#avoid 0 or 1 points
if not smat:
continue #no material on line
if smat.grease_pencil.show_fill:
continue # skip fill lines -> #smat.grease_pencil.show_stroke
if len(s.points) < 2:
continue #avoid 0 or 1 points
plist.append(s.points[0])
plist.append(s.points[-1])
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
@ -70,7 +76,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
for op in plist:#other points
if p == op:# print('same point')
continue
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position))
# print('gap2d: ', gap2d)
if gap2d > tol:
continue
@ -102,16 +108,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter[p].append(op)
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
ctl += 1
print(f'{ctl} line created')
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
def create_closing_line(tolerance=0.2):
for ob in bpy.context.selected_objects:
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
continue
mat_id = get_closeline_mat(ob)# get a the closing material
@ -128,7 +134,7 @@ def create_closing_line(tolerance=0.2):
## filter on selected
if not l.select:continue# comment this line for all
# for f in l.frames:#not all for now
f = l.active_frame
f = l.current_frame()
## create gap stroke
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
@ -143,9 +149,9 @@ def is_deviating_by(s, deviation=0.75):
pb = s.points[-2]
pc = s.points[-3]
a = location_to_region(pa.co)
b = location_to_region(pb.co)
c = location_to_region(pc.co)
a = location_to_region(pa.position)
b = location_to_region(pb.position)
c = location_to_region(pc.position)
#cb-> compare angle with ba->
angle = (b-c).angle(a-b)
@ -158,16 +164,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
'''extend line boundary by given length'''
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
## 2D mode
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
# b = location_to_region(b_loc)
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
# c_loc = region_to_location(c, b_loc)
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
## 3D
a = s.points[id_pair[0]].co# ob.matrix_world @
b = s.points[id_pair[1]].co# ob.matrix_world @
a = s.points[id_pair[0]].position# ob.matrix_world @
b = s.points[id_pair[1]].position# ob.matrix_world @
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
@ -188,15 +194,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
## Change length of current length to designated
# Vector point A to point B (direction), push point B in this direction
a = s.points[-2].co
a = s.points[-2].position
bp = s.points[-1]#end-point
b = bp.co
b = bp.position
ab = b - a
if not ab:
continue
# new pos of B is A + new length in the AB direction
newb = a + (ab.normalized() * length)
bp.co = newb
bp.position = newb
ct += 1
return ct
@ -210,14 +216,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
return
# TODO need custom filters or go in GP refine strokes...
# frame = ob.data.layers.active.active_frame
# frame = ob.data.layers.active.current_frame()
if not frame: return
ct = 0
#TODO need to delete previous closing lines on frame before launching
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
for s in list(frame.strokes):
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in !
for s in list(frame.drawing.strokes):
if s.material_index == mat_id:#is a closeline
continue
if len(s.points) < 2:#not enough point to evaluate
@ -241,7 +247,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
# mode : bpy.props.StringProperty(
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
@ -273,18 +279,18 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide]
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
lays = [l for l in ob.data.layers if not is_hidden(l)]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
if not l.current_frame():
print(f'{l.name} has no active frame')
continue
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
if not fct:
mess = "No strokes extended... see console"
@ -306,7 +312,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target",
@ -334,18 +340,18 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide]
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
lays = [l for l in ob.data.layers if not is_hidden(l)]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
if not l.current_frame():
print(f'{l.name} has no active frame')
continue
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
if not fct:
mess = "No extension modified... see console"
@ -367,15 +373,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
def execute(self, context):
ct = 0
ob = context.object
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
for l in lays:
if not l.active_frame:continue
for s in l.active_frame.strokes:
if not l.current_frame():continue
for s in l.current_frame().drawing.strokes:
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
ct+=1
@ -397,7 +403,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
@classmethod
def poll(cls, context):
return (context.object is not None)# and context.object.type == 'GPENCIL'
return (context.object is not None)# and context.object.type == 'GREASEPENCIL'
## draw stuff inside the header (place before main label)
# def draw_header(self, context):
@ -414,7 +420,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
#diplay closeline visibility
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
row=layout.row()
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')

View File

@ -302,8 +302,10 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
### --- KEYMAP ---
breakdowner_addon_keymaps = []
addon_keymaps = []
def register_keymaps():
if bpy.app.background:
return
# pref = get_addon_prefs()
# if not pref.breakdowner_use_shortcut:
# return
@ -320,24 +322,23 @@ def register_keymaps():
if ops_id not in km.keymap_items:
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY
kmi = km.keymap_items.new(ops_id, type="E", value="PRESS", shift=True)
breakdowner_addon_keymaps.append((km, kmi))
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in breakdowner_addon_keymaps:
if bpy.app.background:
return
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
breakdowner_addon_keymaps.clear()
# del breakdowner_addon_keymaps[:]
addon_keymaps.clear()
### --- REGISTER ---
def register():
# if not bpy.app.background:
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
register_keymaps()
def unregister():
# if not bpy.app.background:
unregister_keymaps()
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)

View File

@ -27,7 +27,7 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
# return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.blend'
@ -48,30 +48,53 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
return {"FINISHED"}
class GPTB_OT_brush_set(bpy.types.Operator):
bl_idname = "gp.brush_set"
bl_label = "Set Brush"
bl_description = "Set Gpencil brush"
bl_options = {"REGISTER", "UNDO"}
brush_name : bpy.props.StringProperty(name='Brush', description='Name of the brush to use')
@classmethod
def poll(cls, context):
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
def execute(self, context):
brush = bpy.data.brushes.get(self.brush_name)
if not brush:
self.report({'ERROR'}, f'Brush "{self.brush_name}" not found')
return {"CANCELLED"}
context.scene.tool_settings.gpencil_paint.brush = brush
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
prefs = get_addon_prefs()
if context.mode == 'PAINT_GREASE_PENCIL':
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
def load_brush_top_bar_ui(self, context):
if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes')
prefs = get_addon_prefs()
if context.mode == 'PAINT_GREASE_PENCIL':
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
classes = (
GPTB_OT_load_brushes,
GPTB_OT_brush_set,
)
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_MT_brush_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)
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)

View File

@ -1,38 +1,15 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
## based on GPclipboard 1.3.2 (just stripped addon prefs)
bl_info = {
"name": "GP clipboard",
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
"author": "Samuel Bernou",
"version": (1, 3, 2),
"blender": (2, 83, 0),
"location": "View3D > Toolbar > Gpencil > GP clipboard",
"warning": "",
"doc_url": "https://github.com/Pullusb/GP_clipboard",
"category": "Object" }
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
## View3D > Toolbar > Gpencil > GP clipboard
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
import bpy
import os
import mathutils
from mathutils import Vector
import json
from time import time
from operator import itemgetter
from itertools import groupby
# from pprint import pprint
from .utils import is_locked, is_hidden
def convertAttr(Attr):
'''Convert given value to a Json serializable format'''
@ -48,97 +25,106 @@ def convertAttr(Attr):
def getMatrix(layer) :
matrix = mathutils.Matrix.Identity(4)
if layer.is_parented:
if layer.parent_type == 'BONE':
object = layer.parent
bone = object.pose.bones[layer.parent_bone]
matrix = bone.matrix @ object.matrix_world
matrix = matrix.copy() @ layer.matrix_inverse
if parent := layer.parent:
if parent.type == 'ARMATURE' and layer.parent_bone:
bone = parent.pose.bones[layer.parent_bone]
matrix = bone.matrix @ parent.matrix_world
matrix = matrix.copy() @ layer.matrix_parent_inverse
else:
matrix = layer.parent.matrix_world @ layer.matrix_inverse
matrix = parent.matrix_world @ layer.matrix_parent_inverse
return matrix.copy()
default_pt_uv_fill = Vector((0.5, 0.5))
# default_pt_uv_fill = Vector((0.5, 0.5))
def dump_gp_point(p, l, obj):
def dump_gp_point(p, l, obj,
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
'''add properties of a given points to a dic and return it'''
pdic = {}
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
point_dict = {}
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
#for att in point_attr_list:
# pdic[att] = convertAttr(getattr(p, att))
# point_dict[att] = convertAttr(getattr(p, att))
if l.parent:
mat = getMatrix(l)
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
else:
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
# pdic['select'] = convertAttr(getattr(p,'select'))# need selection ?
pdic['strength'] = convertAttr(getattr(p,'strength'))
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
if radius and p.radius != 1.0:
point_dict['radius'] = convertAttr(getattr(p,'radius'))
if opacity and p.opacity != 1.0:
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
## get vertex color (long...)
if p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
pdic['vertex_color'] = convertAttr(p.vertex_color)
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
point_dict['vertex_color'] = convertAttr(p.vertex_color)
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
if p.uv_fill != default_pt_uv_fill:
pdic['uv_fill'] = convertAttr(p.uv_fill)
if p.uv_factor != 0.0:
pdic['uv_factor'] = convertAttr(p.uv_factor)
if p.uv_rotation != 0.0:
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
if rotation and p.rotation != 0.0:
point_dict['rotation'] = convertAttr(p.rotation)
return pdic
## No time infos
# if delta_time and p.delta_time != 0.0:
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
return point_dict
def dump_gp_stroke_range(s, sid, l, obj):
def dump_gp_stroke_range(s, sid, l, obj,
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
'''Get a grease pencil stroke and return a dic with attribute
(points attribute being a dic of dics to store points and their attributes)
'''
sdic = {}
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
for att in stroke_attr_list:
sdic[att] = getattr(s, att)
stroke_dict = {}
# stroke_attr_list = ('line_width',)
# for att in stroke_attr_list:
# stroke_dict[att] = getattr(s, att)
## Dump following these value only if they are non default
if s.material_index != 0:
sdic['material_index'] = s.material_index
stroke_dict['material_index'] = s.material_index
if getattr(s, 'draw_cyclic', None):# pre-2.92
sdic['draw_cyclic'] = s.draw_cyclic
if s.cyclic:
stroke_dict['cyclic'] = s.cyclic
if getattr(s, 'use_cyclic', None):# from 2.92
sdic['use_cyclic'] = s.use_cyclic
if s.softness != 0.0:
stroke_dict['softness'] = s.softness
if s.uv_scale != 1.0:
sdic['uv_scale'] = s.uv_scale
if s.aspect_ratio != 1.0:
stroke_dict['aspect_ratio'] = s.aspect_ratio
if s.uv_rotation != 0.0:
sdic['uv_rotation'] = s.uv_rotation
if s.start_cap != 0:
stroke_dict['start_cap'] = s.start_cap
if s.hardness != 1.0:
sdic['hardness'] = s.hardness
if s.end_cap != 0:
stroke_dict['end_cap'] = s.end_cap
if s.uv_translation != Vector((0.0, 0.0)):
sdic['uv_translation'] = convertAttr(s.uv_translation)
if fill_color and s.fill_color[:] != (0,0,0,0):
stroke_dict['fill_color'] = convertAttr(s.fill_color)
if s.vertex_color_fill[:] != (0,0,0,0):
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
if fill_opacity and s.fill_opacity != 0.0:
stroke_dict['fill_opacity'] = s.fill_opacity
## No time infos
# if s.time_start != 0.0:
# stroke_dict['time_start'] = s.time_start
points = []
if sid is None: # no ids, just full points...
for p in s.points:
points.append(dump_gp_point(p,l,obj))
points.append(dump_gp_point(p, l, obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
else:
for pid in sid:
points.append(dump_gp_point(s.points[pid],l,obj))
sdic['points'] = points
return sdic
points.append(dump_gp_point(s.points[pid], l, obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
stroke_dict['points'] = points
return stroke_dict
def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow filter)
def copycut_strokes(layers=None, copy=True, keep_empty=True):
'''
copy all visibles selected strokes on active frame
layers can be None, a single layer object or list of layer object as filter
@ -154,7 +140,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
# color = gp.palettes.active.colors.active.name
if not layers:
# by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]#[]
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
if not isinstance(layers, list):
# if a single layer object is send put in a list
layers = [layers]
@ -162,32 +148,36 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
stroke_list = [] # one stroke list for all layers.
for l in layers:
f = l.active_frame
f = l.current_frame()
if f: # active frame can be None
if not copy:
staylist = [] # init part of strokes that must survive on this layer
for s in f.strokes:
if s.select:
rm_list = [] # init strokes that must be removed from this layer
for s_index, stroke in enumerate(f.drawing.strokes):
if stroke.select:
# separate in multiple stroke if parts of the strokes a selected.
sel = [i for i, p in enumerate(s.points) if p.select]
sel = [i for i, p in enumerate(stroke.points) if p.select]
substrokes = [] # list of list containing isolated selection
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
# continuity stroke have same substract result between point index and enumerator
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g))
substrokes.append(group)
for ss in substrokes:
if len(ss) > 1: # avoid copy isolated points
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj))
# Cutting operation
if not copy:
maxindex = len(s.points)-1
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier
f.strokes.remove(s)
maxindex = len(stroke.points)-1
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
# f.drawing.strokes.remove(stroke) # gpv2
rm_list.append(s_index)
else:
neg = [i for i, p in enumerate(s.points) if not p.select]
neg = [i for i, p in enumerate(stroke.points) if not p.select]
staying = []
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
@ -201,37 +191,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
for ns in staying:
if len(ns) > 1:
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
# make a negative list containing all last index
'''#full stroke version
# if s.colorname == color: #line for future filters
stroke_list.append(dump_gp_stroke(s,l))
#delete stroke on the fly
if not copy:
f.strokes.remove(s)
'''
if rm_list:
f.drawing.remove_strokes(indices=rm_list)
if not copy:
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
# delete all selected strokes...
for s in f.strokes:
if s.select:
f.strokes.remove(s)
if selected_ids:
f.drawing.remove_strokes(indices=selected_ids)
# ...recreate these uncutted ones
#pprint(staylist)
if staylist:
add_multiple_strokes(staylist, l)
#for ns in staylist:#weirdly recreate the stroke twice !
# add_stroke(ns, f, l)
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
if not copy and not keep_empty:#
if not len(f.strokes):
# If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
if not copy and not keep_empty:
if not len(f.drawing.strokes):
l.frames.remove(f)
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
return stroke_list
@ -253,7 +236,7 @@ def copy_all_strokes(layers=None):
if not layers:
# by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
if not isinstance(layers, list):
# if a single layer object is send put in a list
layers = [layers]
@ -261,12 +244,12 @@ def copy_all_strokes(layers=None):
stroke_list = []# one stroke list for all layers.
for l in layers:
f = l.active_frame
f = l.current_frame()
if not f:
continue# active frame can be None
for s in f.strokes:
for s in f.drawing.strokes:
## full stroke version
# if s.select:
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
@ -276,11 +259,11 @@ def copy_all_strokes(layers=None):
return stroke_list
"""
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
'''
copy all stroke, not affected by selection on active frame
layers can be None, a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time()
scene = bpy.context.scene
@ -293,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
if not layers:
# by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
if not isinstance(layers, list):
# if a single layer object is send put in a list
layers = [layers]
@ -301,74 +284,69 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
stroke_list = []
for l in layers:
f = l.active_frame
f = l.current_frame()
if not f:
continue# active frame can be None
for s in f.strokes:
for s in f.drawing.strokes:
## full stroke version
# if s.select:
# send index of all points to get the whole stroke with "range"
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj) )
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
return stroke_list
def add_stroke(s, frame, layer, obj, select=False):
'''add stroke on a given frame, (layer is for parentage setting)'''
# print(3*'-',s)
ns = frame.strokes.new()
pts_to_add = len(s['points'])
frame.drawing.add_strokes([pts_to_add])
ns = frame.drawing.strokes[-1]
## set strokes atrributes
for att, val in s.items():
if att not in ('points'):
setattr(ns, att, val)
pts_to_add = len(s['points'])
# print(pts_to_add, 'points')#dbg
ns.points.add(pts_to_add)
ob_mat_inv = obj.matrix_world.inverted()
## patch pressure 1
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
if layer.parent:
layer_matrix = getMatrix(layer).inverted()
transform_matrix = ob_mat_inv @ layer_matrix
else:
transform_matrix = ob_mat_inv
if layer.is_parented:
mat = getMatrix(layer).inverted()
## Set points attributes
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
if k == 'position':
setattr(ns.points[i], k, v)
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
else:
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
setattr(ns.points[i], k, v)
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
## Opacity initialized at 0.0 (should be 1.0)
if not 'opacity' in pt:
ns.points[i].opacity = 1.0
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
ns.points.update()
## Radius initialized at 0.0 (should probably be 0.01)
if not 'radius' in pt:
ns.points[i].radius = 0.01
## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list)
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
if no layer specified, active layer is used
if use_current_frame is True, a new frame will be created only if needed
if select is True, newly added strokes are selected
if select is True, newly added strokes are set selected
if stroke list is empty create an empty frame at current frame
'''
scene = bpy.context.scene
obj = bpy.context.object
@ -381,8 +359,8 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
fnum = scene.frame_current
target_frame = False
act = layer.active_frame
for s in stroke_list:
act = layer.current_frame()
## set frame if needed
if act:
if use_current_frame or act.frame_number == fnum:
#work on current frame if exists
@ -394,12 +372,10 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
#or active exists but not aligned scene.current with use_current_frame disabled
target_frame = layer.frames.new(fnum)
for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select)
'''
for s in stroke_data:
add_stroke(s, target_frame)
'''
print(len(stroke_list), 'strokes pasted')
# print(len(stroke_list), 'strokes pasted')
### OPERATORS
@ -407,24 +383,24 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
class GPCLIP_OT_copy_strokes(bpy.types.Operator):
bl_idname = "gp.copy_strokes"
bl_label = "GP Copy strokes"
bl_description = "Copy strokes to str in paperclip"
bl_description = "Copy strokes to text in paperclip"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time()
#ct = check_pressure()
#ct = check_radius()
strokelist = copycut_strokes(copy=True, keep_empty=True)
if not strokelist:
self.report({'ERROR'},'rien a copier')
self.report({'ERROR'}, 'Nothing to copy')
return {"CANCELLED"}
bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
#if ct:
@ -437,20 +413,20 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
class GPCLIP_OT_cut_strokes(bpy.types.Operator):
bl_idname = "gp.cut_strokes"
bl_label = "GP Cut strokes"
bl_description = "Cut strokes to str in paperclip"
bl_options = {"REGISTER"}
bl_description = "Cut strokes to text in paperclip"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time()
strokelist = copycut_strokes(copy=False, keep_empty=True)#ct = check_pressure()
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
if not strokelist:
self.report({'ERROR'},'Nothing to cut')
return {"CANCELLED"}
@ -467,10 +443,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected to paste on')
# return {"CANCELLED"}
@ -493,14 +469,47 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
bl_idname = "gp.copy_multi_strokes"
bl_label = "GP Copy multi strokes"
bl_description = "Copy multiple layers>frames>strokes (unlocked and unhided ones) to str in paperclip"
bl_label = "GP Copy Multi Strokes"
bl_description = "Copy multiple layers>frames>strokes from selected layers to str in paperclip"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
radius : bpy.props.BoolProperty(name='radius', default=True,
description='Dump point radius attribute (already skipped if at default value)')
opacity : bpy.props.BoolProperty(name='opacity', default=True,
description='Dump point opacity attribute (already skipped if at default value)')
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
description='Dump point vertex_color attribute (already skipped if at default value)')
fill_color : bpy.props.BoolProperty(name='fill color', default=True,
description='Dump point fill_color attribute (already skipped if at default value)')
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
description='Dump point fill_opacity attribute (already skipped if at default value)')
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
description='Dump point uv_factor attribute (already skipped if at default value)')
rotation : bpy.props.BoolProperty(name='rotation', default=True,
description='Dump point rotation attribute (already skipped if at default value)')
def invoke(self, context, event):
# self.file_dump = event.ctrl
return context.window_manager.invoke_props_dialog(self) # , width=400
# return self.execute(context)
def draw(self, context):
layout=self.layout
layout.use_property_split = True
col = layout.column()
col.label(text='Keep following point attributes:')
col.prop(self, 'radius')
col.prop(self, 'opacity')
col.prop(self, 'vertex_color')
col.prop(self, 'fill_color')
col.prop(self, 'fill_opacity')
col.prop(self, 'rotation')
return
def execute(self, context):
bake_moves = True
@ -510,10 +519,10 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
obj = context.object
gpl = obj.data.layers
t0 = time()
#ct = check_pressure()
#ct = check_radius()
layerdic = {}
layerpool = [l for l in gpl if not l.hide and l.select]# and not l.lock
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"}
@ -525,17 +534,20 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
frame_dic = {}
for f in l.frames:
if skip_empty_frame and not len(f.strokes):
if skip_empty_frame and not len(f.drawing.strokes):
continue
context.scene.frame_set(f.frame_number) # use matrix of this frame
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
frame_dic[f.frame_number] = strokelist
layerdic[l.info] = frame_dic
layerdic[l.name] = frame_dic
else: # bake position: copy frame where object as moved even if frame is unchanged
for l in layerpool:
print('dump layer:', l.name)
if not l.frames:
continue# skip empty layers
@ -559,14 +571,17 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
break
## skip empty frame if specified
if skip_empty_frame and not len(f.strokes):
if skip_empty_frame and not len(f.drawing.strokes):
continue
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
frame_dic[i] = strokelist
prevmat = curmat
layerdic[l.info] = frame_dic
layerdic[l.name] = frame_dic
## All to clipboard manager
bpy.context.window_manager.clipboard = json.dumps(layerdic)
@ -579,14 +594,14 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
bl_idname = "gp.paste_multi_strokes"
bl_label = "GP paste multi strokes"
bl_description = "Paste multiple layers>frames>strokes from paperclip"
bl_label = "GP Paste Multi Strokes"
bl_description = "Paste multiple layers>frames>strokes from paperclip on active layer"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
org_frame = context.scene.frame_current
@ -615,7 +630,7 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
layer = gpl.new(layname)
for fnum, fstrokes in allframes.items():
context.scene.frame_set(int(fnum)) # use matrix of this frame for copying (maybe just evaluate depsgraph for object
add_multiple_strokes(fstrokes, use_current_frame=False)#create a new frame at each encoutered
add_multiple_strokes(fstrokes, use_current_frame=False) # create a new frame at each encoutered occurence
print('total_time', time() - t0)
@ -637,13 +652,16 @@ class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.operator('gp.copy_strokes', text='Copy strokes', icon='COPYDOWN')
row.operator('gp.cut_strokes', text='Cut strokes', icon='PASTEFLIPUP')
layout.operator('gp.paste_strokes', text='Paste strokes', icon='PASTEDOWN')
layout.separator()
layout.operator('gp.copy_multi_strokes', text='Copy layers', icon='COPYDOWN')
layout.operator('gp.paste_multi_strokes', text='Paste layers', icon='PASTEDOWN')
col = layout.column(align=True)
row = col.row(align=True)
row.operator('gp.copy_strokes', text='Copy Strokes', icon='COPYDOWN')
row.operator('gp.cut_strokes', text='Cut Strokes', icon='PASTEFLIPUP')
col.operator('gp.paste_strokes', text='Paste Strokes', icon='PASTEDOWN')
# layout.separator()
col = layout.column(align=True)
col.operator('gp.copy_multi_strokes', text='Copy Layers', icon='COPYDOWN')
col.operator('gp.paste_multi_strokes', text='Paste Layers', icon='PASTEDOWN')
###---TEST zone
@ -712,6 +730,9 @@ GPCLIP_PT_clipboard_ui,
)
def register():
if bpy.app.background:
return
for cl in classes:
bpy.utils.register_class(cl)
@ -719,6 +740,9 @@ def register():
register_keymaps()
def unregister():
if bpy.app.background:
return
unregister_keymaps()
for cl in reversed(classes):
bpy.utils.unregister_class(cl)

View File

@ -2,6 +2,8 @@
import bpy
import mathutils
from bpy_extras import view3d_utils
from bpy.app.handlers import persistent
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap
@ -13,7 +15,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
# return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
#print('-!SNAP!-')
@ -23,7 +25,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
return {"FINISHED"}
def execute(self, context):
if not context.object or context.object.type != 'GPENCIL':
if not context.object or context.object.type != 'GREASEPENCIL':
self.report({'INFO'}, 'Not GP, Cursor surface project')
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
return {"FINISHED"}
@ -48,7 +50,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
if warning:
self.report({'WARNING'}, ', '.join(warning))
plane_co, plane_no = get_gp_draw_plane(context)
plane_co, plane_no = get_gp_draw_plane()
if not plane_co:#default to object location
plane_co = context.object.matrix_world.to_translation()#context.object.location
@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
k.idname = new_idname
# prev_matrix = mathutils.Matrix()
prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post)
## used in properties file to register in boolprop update
def cursor_follow_update(self, context):
'''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix
# imported in properties to register in boolprop update
if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False
@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change'''
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
if not bpy.context.object:
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
if not ob:
return
global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world
if not prev_matrix:
prev_matrix = current_matrix.copy()
@ -147,14 +155,43 @@ def cursor_follow(scene):
## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use
prev_matrix = current_matrix.copy()
prev_active_obj = None
## Add check for object selection change
def selection_changed():
"""Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow:
return
if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object:
## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well)
def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active')
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
args=(),
notify=selection_changed,
options={'PERSISTENT'},
)
@persistent
def subscribe_object_change_handler(dummy):
subscribe_object_change()
classes = (
GPTB_OT_cusor_snap,
@ -163,14 +200,18 @@ GPTB_OT_cusor_snap,
def register():
for cls in classes:
bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
# bpy.app.handlers.frame_change_post.append(cursor_follow)
## Follow cursor matrix update on object change
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
# ## Directly set msgbus to work at first addon activation # select_change
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
def unregister():
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
@ -180,3 +221,5 @@ def unregister():
# force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

View File

@ -1,6 +1,5 @@
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
@ -12,6 +11,7 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
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
from .utils import is_locked, is_hidden
def get_gp_mat(gp, name, set_active=False):
@ -190,7 +190,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self):
bgl.glEnable(bgl.GL_BLEND)
gpu.state.blend_set('ALPHA')
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
@ -201,7 +201,7 @@ class GPTB_OT_eraser(Operator):
bg_color = area.spaces.active.shading.background_color
#print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.bind()
shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path:
@ -210,7 +210,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
bgl.glDisable(bgl.GL_BLEND)
gpu.state.blend_set('NONE')
@ -232,7 +232,7 @@ class GPTB_OT_eraser(Operator):
hld_stroke.points.add(count=1)
p = hld_stroke.points[-1]
p.co = mat_inv @ mouse_3d
p.position = mat_inv @ mouse_3d
p.pressure = search_radius * 2000
#context.scene.cursor.location = mouse_3d
@ -253,14 +253,14 @@ class GPTB_OT_eraser(Operator):
#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)
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
# f.drawing.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')
bpy.ops.object.mode_set(mode='EDIT')
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
@ -282,7 +282,7 @@ class GPTB_OT_eraser(Operator):
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]
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
#print('select_circle', time()-t1)
t2 = time()
@ -310,18 +310,18 @@ class GPTB_OT_eraser(Operator):
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_p1.position = 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_p2.position = 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_p3.position = mat_inv@intersects[1]
new_points += [(stroke, p1_index+3)]
#print('subdivide', time() - t3)
@ -330,7 +330,7 @@ class GPTB_OT_eraser(Operator):
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]
selected_strokes = [s for f in self.gp_frames for s in f.drawing.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()
@ -342,7 +342,7 @@ class GPTB_OT_eraser(Operator):
'''
t4 = time()
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
if selected_strokes:
bpy.ops.gpencil.delete(type='POINTS')
@ -359,9 +359,9 @@ class GPTB_OT_eraser(Operator):
#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')
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
def modal(self, context, event):
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
@ -441,23 +441,23 @@ class GPTB_OT_eraser(Operator):
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]
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
self.gp_frames = [l.current_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, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.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)]
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
points_data = [(s, f, m, p, org + ((matrix @ p.position)-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
# p.position = co
t0 = time()
@ -482,7 +482,7 @@ class GPTB_OT_eraser(Operator):
self.hld_strokes = []
for f in self.gp_frames:
hld_stroke = f.strokes.new()
hld_stroke = f.drawing.strokes.new()
hld_stroke.start_cap_mode = 'ROUND'
hld_stroke.end_cap_mode = 'ROUND'
hld_stroke.material_index = self.hld_index

View File

@ -1,20 +1,47 @@
import bpy
import os
from pathlib import Path
from .utils import show_message_box, get_addon_prefs
import numpy as np
from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes
return number of duplication found/deleted
'''
# TODO: add additional check of material (even if unlikely to happen)
ct = 0
gp_datas = [gp for gp in bpy.data.grease_pencils]
for gp in gp_datas:
for l in gp.layers:
for f in l.frames:
stroke_list = []
for s in reversed(f.drawing.strokes):
point_list = [p.position for p in s.points]
if point_list in stroke_list:
ct += 1
if apply:
# Remove redundancy
f.drawing.strokes.remove(s)
else:
stroke_list.append(point_list)
return ct
class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker"
bl_label = "File check"
bl_label = "Check File"
bl_description = "Check / correct some aspect of the file, properties and such and report"
bl_options = {"REGISTER"}
# @classmethod
# def poll(cls, context):
# return context.region_data.view_perspective == 'CAMERA'
## list of action :
# Lock main cam:
## list of actions :
# Lock main cam
# set scene res
# set scene percentage at 100:
# set show slider and sync range
@ -23,13 +50,37 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# GP stroke placement/projection check
# Disabled animation
# Objects visibility conflict
# Objects modifiers visibility conflict
# GP modifiers broken target check
# Set onion skin filter to 'All type'
# Set filepath type
# Set Lock object mode state
# Disable use light on all object
# Remove redundant strokes in frames
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
options={'SKIP_SAVE'})
def invoke(self, context, event):
# need some self-control (I had to...)
self.ctrl = event.ctrl
return self.execute(context)
def execute(self, context):
prefs = get_addon_prefs()
prefs = utils.get_addon_prefs()
fix = prefs.fixprops
problems = []
## Old method : Apply fixes based on pref (inverted by ctrl key)
# # If Ctrl is pressed, invert behavior (invert boolean)
# apply ^= self.ctrl
apply = self.apply_fixes
if self.ctrl:
apply = True
## Lock main cam:
if fix.lock_main_cam:
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
@ -44,6 +95,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
triple = (True,True,True)
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
problems.append('Lock main camera')
if apply:
cam.lock_location = cam.lock_rotation = triple
## set scene res at pref res according to addon pref
@ -52,18 +104,21 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# 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:
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
if apply:
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
## set scene percentage at 100:
if fix.set_res_percentage:
if context.scene.render.resolution_percentage != 100:
problems.append('Resolution output to 100%')
if apply:
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') )
if apply:
context.scene.render.fps = prefs.fps
## set show slider and sync range
@ -74,18 +129,17 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if area.type == 'DOPESHEET_EDITOR':
if hasattr(area.spaces[0], 'show_sliders'):
setattr(area.spaces[0], 'show_sliders', True)
if hasattr(area.spaces[0], 'show_locked_time'):
setattr(area.spaces[0], 'show_locked_time', True)
## set cursor type (according to prefs ?)
if fix.set_cursor_type:
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = prefs.select_active_tool
## set cursor type
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
tool = fix.select_active_tool
if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
bpy.ops.wm.tool_set_by_id(name=tool)# Tweaktoolcode
problems.append(f'tool changed to {tool.split(".")[1]}')
if apply:
bpy.ops.wm.tool_set_by_id(name=tool) # Tweaktoolcode
# ## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# if not context.scene.tool_settings.use_gpencil_draw_additive:
@ -102,6 +156,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
## GP Use light disable
if fix.set_gp_use_lights_off:
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
if gp_with_lights:
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
if apply:
for o in gp_with_lights:
o.use_grease_pencil_lights = False
## Disabled animation
if fix.list_disabled_anim:
@ -114,24 +176,102 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict
if fix.list_obj_vis_conflict:
viz_ct = 0
for o in context.scene.objects:
if not (o.hide_get() == o.hide_viewport == o.hide_render):
hv = 'No' if o.hide_get() else 'Yes'
vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
if viz_ct:
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict:
mod_viz_ct = 0
for o in context.scene.objects:
for m in o.modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
if mod_viz_ct:
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
## check if GP modifier have broken layer targets
if fix.list_broken_mod_targets:
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
lay_name_list = [l.name for l in o.data.layers]
for m in o.modifiers:
if not hasattr(m, 'layer_filter'):
continue
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
print(mess)
problems.append(mess)
## Use median point
if fix.set_pivot_median_point:
if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
if apply:
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
if fix.disable_guide:
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
problems.append(f"Disabled Draw Guide")
if apply:
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'")
if apply:
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
if fix.file_path_type != 'none':
pathes = []
for p in bpy.utils.blend_paths():
if fix.file_path_type == 'RELATIVE':
if not p.startswith('//'):
pathes.append(p)
elif fix.file_path_type == 'ABSOLUTE':
if p.startswith('//'):
pathes.append(p)
if pathes:
mess = f'{len(pathes)}/{len(bpy.utils.blend_paths())} paths not {fix.file_path_type.lower()} (see console)'
problems.append(mess)
print(mess)
print('\n'.join(pathes))
print('-')
if fix.lock_object_mode != 'none':
lockmode = bpy.context.scene.tool_settings.lock_object_mode
if fix.lock_object_mode == 'LOCK':
if not lockmode:
problems.append(f"Lock object mode toggled On")
if apply:
bpy.context.scene.tool_settings.lock_object_mode = True
elif fix.lock_object_mode == 'UNLOCK':
if lockmode:
problems.append(f"Lock object mode toggled Off")
if apply:
bpy.context.scene.tool_settings.lock_object_mode = False
if fix.remove_redundant_strokes:
ct = remove_stroke_exact_duplications(apply=apply)
if ct > 0:
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
problems.append(mess)
# ## Set onion skin filter to 'All type'
# fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data
@ -143,7 +283,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object
# if ob.type == 'GPENCIL':
# if ob.type == 'GREASEPENCIL':
# ob.data.onion_keyframe_type = 'ALL'
#### --- print fix/problems report
@ -154,47 +294,92 @@ class GPTB_OT_file_checker(bpy.types.Operator):
print(p)
else:
print(p[0])
if not self.apply_fixes:
## button to call the operator again with apply_fixes set to True
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
# Show in viewport
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO')
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO')
else:
self.report({'INFO'}, 'All good')
return {"FINISHED"}
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
class GPTB_OT_copy_string_to_clipboard(bpy.types.Operator):
bl_idname = "gp.copy_string_to_clipboard"
bl_label = "Copy String"
bl_description = "Copy passed string to clipboard"
bl_options = {"REGISTER"}
def execute(self, context):
all_lnks = []
has_broken_link = False
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
has_broken_link = True
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
all_lnks.sort(key=lambda x: x[1], reverse=True)
if all_lnks:
print('===File check===')
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
def execute(self, context):
if not self.string:
# self.report({'ERROR'}, 'Nothing to copy')
return {"CANCELLED"}
bpy.context.window_manager.clipboard = self.string
self.report({'INFO'}, f'Copied: {self.string}')
return {"FINISHED"}
class GPTB_OT_copy_multipath_clipboard(bpy.types.Operator):
bl_idname = "gp.copy_multipath_clipboard"
bl_label = "Choose Path to Copy"
bl_description = "Copy Chosen Path"
bl_options = {"REGISTER"}
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
def invoke(self, context, event):
if not self.string:
return {"CANCELLED"}
self.pathes = []
try:
absolute = os.path.abspath(bpy.path.abspath(self.string))
abs_parent = os.path.dirname(os.path.abspath(bpy.path.abspath(self.string)))
path_abs = str(Path(bpy.path.abspath(self.string)).resolve())
except:
# case of invalid / non-accessable path
bpy.context.window_manager.clipboard = self.string
return context.window_manager.invoke_props_dialog(self, width=800)
self.pathes.append(('Raw Path', self.string))
self.pathes.append(('Parent', os.path.dirname(self.string)))
if absolute != self.string:
self.pathes.append(('Absolute', absolute))
if absolute != self.string:
self.pathes.append(('Absolute Parent', abs_parent))
if absolute != path_abs:
self.pathes.append(('Resolved',path_abs))
self.pathes.append(('File name', os.path.basename(self.string)))
maxlen = max(len(l[1]) for l in self.pathes)
popup_width = 800
if maxlen < 50:
popup_width = 500
elif maxlen > 100:
popup_width = 1000
return context.window_manager.invoke_props_dialog(self, width=popup_width)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.separator()
col = layout.column()
for l in self.pathes:
split=col.split(factor=0.2, align=True)
split.operator('gp.copy_string_to_clipboard', text=l[0], icon='COPYDOWN').string = l[1]
split.label(text=l[1])
def execute(self, context):
return {"FINISHED"}
class GPTB_OT_links_checker(bpy.types.Operator):
@ -223,14 +408,28 @@ class GPTB_OT_links_checker(bpy.types.Operator):
layout.separator()
# layout = layout.column() # thinner linespace
for l in self.all_lnks:
if l[1] == 'LIBRARY_DATA_BROKEN':
if l[1] == 'CANCEL':
layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.75)
continue
if l[1] == 'LIBRARY_DATA_BROKEN':
split=layout.split(factor=0.85)
split.label(text=l[0], icon=l[1])
split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()#os.path.abspath(bpy.path.abspath(dirname(l[0])))
# layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.70, align=True)
split.label(text=l[0], icon=l[1])
## resolve() return somethin different than os.path.abspath.
# split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
# split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()
split.operator('wm.path_open', text='Open Folder', icon='FILE_FOLDER').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).parent.as_posix()
split.operator('wm.path_open', text='Open File', icon='FILE_TICK').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).as_posix()
split.operator('gp.copy_multipath_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0]
# split.operator('gp.copy_string_to_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0] # copy blend path directly
def invoke(self, context, event):
self.all_lnks = []
@ -239,19 +438,32 @@ class GPTB_OT_links_checker(bpy.types.Operator):
abs_ct = 0
rel_ct = 0
## check for broken links
viewed = []
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
# avoid relisting same path mutliple times
if current in viewed:
continue
# TODO find a proper way to show the number of user of this path...
viewed.append(current)
realib = Path(current) # path as-is
lfp = Path(lib) # absolute path
try: # Try because some path may fail parsing
if not lfp.exists():
self.broke_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )
else:
if realib.as_posix().startswith('//'):
rel_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )
else:
abs_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )
except:
self.broke_ct += 1
self.all_lnks.append( (f"{current}" , 'CANCEL') ) # error accessing file
if not self.all_lnks:
self.report({'INFO'}, 'No external links in files')
@ -273,50 +485,221 @@ class GPTB_OT_links_checker(bpy.types.Operator):
print(p[0])
# Show in viewport
maxlen = max(len(x) for x in viewed)
# if broke_ct == 0:
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
# return {"FINISHED"}
try:
self.proj = context.preferences.addons['pipe_sync'].preferences['local_folder']
except:
self.proj = None
return context.window_manager.invoke_props_dialog(self, width=800)
popup_width = 800
if maxlen < 50:
popup_width = 500
elif maxlen > 100:
popup_width = 1000
'''### OLD
class GPTB_OT_check_scene(bpy.types.Operator):
bl_idname = "gp.scene_check"
bl_label = "Check GP scene"
bl_description = "Check and fix scene settings"
self.proj = os.environ.get('PROJECT_ROOT')
return context.window_manager.invoke_props_dialog(self, width=popup_width)
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
bl_idname = "gp.list_viewport_render_visibility"
bl_label = "List Viewport And Render Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def invoke(self, context, event):
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
# TODO: Add visibility check with viewlayer visibility as well
layout = self.layout
for o in self.ob_list:
row = layout.row()
row.label(text=o.name)
row.prop(o, 'hide_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(o, 'hide_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
## check scene resolution / 100% / framerate
context.scene.render.resolution_percentage = 100
context.scene.render.resolution_x = 3072# define addon properties to make generic ?
context.scene.render.resolution_y = 1620# define addon properties to make generic ?
context.scene.render.fps = 24# define addon properties to make generic ?
return {'FINISHED'}
## check GP datas name
gp_os = [o for o in context.scene.objects if o.type == 'GPENCIL' if o.data.users == 1]#no multiple users
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewlayer"
bl_label = "Sync Visibility From Viewlayer"
bl_description = "Set viewport and render visibility to match viewlayer visibility"
bl_options = {"REGISTER", "UNDO"}
for gpo in gp_os:
if gpo.data.name.startswith('Stroke'):# dont touch already renamed group
if gpo.data.name != gpo.name:
print('renaming GP data:', gpo.data.name, '-->', gpo.name)
gpo.data.name = gpo.name
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_get() # Get viewlayer visibility
obj.hide_viewport = is_hidden
obj.hide_render = is_hidden
return {'FINISHED'}
## disable autolock
context.scene.tool_settings.lock_object_mode = False
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewport"
bl_label = "Sync Visibility From Viewport"
bl_description = "Set viewlayer and render visibility to match viewport visibility"
bl_options = {"REGISTER", "UNDO"}
return {"FINISHED"}
'''
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_viewport
obj.hide_set(is_hidden)
obj.hide_render = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_render"
bl_label = "Sync Visibility From Render"
bl_description = "Set viewlayer and viewport visibility to match render visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_render
obj.hide_set(is_hidden)
obj.hide_viewport = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
bl_idname = "gp.sync_visibible_to_render"
bl_label = "Sync Overall Viewport Visibility To Render"
bl_description = "Set render visibility from"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
obj.hide_render = not obj.visible_get()
return {'FINISHED'}
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
"""Property group to handle object visibility"""
is_hidden: BoolProperty(
name="Hide in Viewport",
description="Toggle object visibility in viewport",
get=lambda self: self.get("is_hidden", False),
set=lambda self, value: self.set_visibility(value)
)
object_name: StringProperty(name="Object Name")
def set_visibility(self, value):
"""Set the visibility using hide_set()"""
obj = bpy.context.view_layer.objects.get(self.object_name)
if obj:
obj.hide_set(value)
self["is_hidden"] = value
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
bl_idname = "gp.list_object_visibility_conflicts"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
def invoke(self, context, event):
# Clear and rebuild both collections
self.visibility_items.clear()
# Store objects with conflicts
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
# Create visibility items in same order
for obj in objects_with_conflicts:
item = self.visibility_items.add()
item.object_name = obj.name
item["is_hidden"] = obj.hide_get()
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# Add sync buttons at the top
row = layout.row(align=False)
row.label(text="Sync All Visibility From:")
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
layout.separator()
col = layout.column()
# We can safely iterate over visibility_items since objects are stored in same order
for vis_item in self.visibility_items:
obj = context.view_layer.objects.get(vis_item.object_name)
if not obj:
continue
row = col.row(align=False)
row.label(text=obj.name)
## Viewlayer visibility "as prop" to allow slide toggle
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
# Direct object properties
row.prop(obj, 'hide_viewport', text='', emboss=False)
row.prop(obj, 'hide_render', text='', emboss=False)
def execute(self, context):
return {'FINISHED'}
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List Objects Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if not len(o.modifiers):
continue
mods = []
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
if not self.ob_list:
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
return
for o in self.ob_list:
layout.label(text=o[0].name, icon=o[2])
for m in o[1]:
row = layout.row()
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON')
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
classes = (
# GPTB_OT_check_scene,
GPTB_OT_list_viewport_render_visibility, # Only viewport and render
GPTB_OT_sync_visibility_from_viewlayer,
GPTB_OT_sync_visibility_from_viewport,
GPTB_OT_sync_visibility_from_render,
GPTB_OT_sync_visibible_to_render,
GPTB_PG_object_visibility,
GPTB_OT_list_object_visibility_conflicts,
GPTB_OT_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard,
GPTB_OT_file_checker,
GPTB_OT_links_checker,
)

160
OP_flat_reproject.py Normal file
View File

@ -0,0 +1,160 @@
import bpy
import mathutils
from mathutils import Matrix, Vector
from mathutils.geometry import intersect_line_plane
from math import pi
import numpy as np
from time import time
from .utils import (location_to_region, region_to_location)
## DISABLED (in init, also in menu append, see register below)
"""
## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject
:all_stroke: affect hidden, locked layers
'''
if restore_frame:
oframe = bpy.context.scene.frame_current
omode = bpy.context.mode
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
# frame_list = list(set(frame_list))
# frame_list.sort()
# for fnum in frame_list:
# bpy.context.scene.frame_current = fnum
t0 = time()
scn = bpy.context.scene
laynum = len(obj.data.layers)
for i, l in enumerate(obj.data.layers):
## \x1b[2K\r ?
fnum = len(l.frames)
zf = len(str(fnum))
for j, f in enumerate(reversed(l.frames)): # whynot...
print(f'{obj.name} : {i+1}/{laynum} : {l.name} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
scn.frame_set(f.frame_number) # more chance to update the matrix
bpy.context.view_layer.update() # update the matrix ?
bpy.context.scene.camera.location = bpy.context.scene.camera.location
scn.frame_current = f.frame_number
for s in f.drawing.strokes:
for p in s.points:
p.position = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.position), scn.cursor.location)
if restore_frame:
bpy.context.scene.frame_current = oframe
print(' '*50,end='\x1b[1K\r') # clear the line
print(f'{obj.name} ok ({time()-t0:.2f})')
"""
"""
def batch_flat_reproject(obj):
'''Reproject all strokes on 3D cursor for all existing frame of passed GP object'''
scn = bpy.context.scene
cam = scn.camera
for l in obj.data.layers:
for f in l.frames:
scn.frame_set(f.frame_number)
cam_mat = cam.matrix_local.copy()
origin = cam.matrix_world.to_translation()
mat_inv = obj.matrix_world.inverted()
plane_no = Vector((0,0,1))
plane_no.rotate(cam_mat)
plane_co = scn.cursor.location
for s in f.drawing.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points]
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
points_co = [co for vector in points_co for co in vector]
s.points.foreach_set('co', points_co)
s.points.add(1) # update
s.points.pop() # update
#for p in s.points:
# loc_2d = location_to_region(obj.matrix_world @ p.position)
# p.position = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
"""
def batch_flat_reproject(obj):
'''Reproject strokes of passed GP object on 3D cursor full scene range'''
scn = bpy.context.scene
cam = scn.camera
for i in range(scn.frame_start, scn.frame_end + 1):
scn.frame_set(i)
cam_mat = cam.matrix_local.copy()
origin = cam.matrix_world.to_translation()
mat_inv = obj.matrix_world.inverted()
plane_no = Vector((0,0,1))
plane_no.rotate(cam_mat)
plane_co = scn.cursor.location
for l in obj.data.layers:
f = l.current_frame()
if not f: # No active frame
continue
if f.frame_number != scn.frame_current:
f = l.frames.copy(f) # duplicate content of the previous frame
for s in f.drawing.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points]
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
points_co = [co for vector in points_co for co in vector]
s.points.foreach_set('co', points_co)
s.points.add(1) # update
s.points.pop() # update
class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
bl_idname = "gp.batch_flat_reproject"
bl_label = "Flat Reproject Selected On cursor"
bl_description = "Reproject all frames of all selected gp object on cursor"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
for o in context.selected_objects:
if o.type != 'GREASEPENCIL' or not o.select_get():
continue
batch_flat_reproject(o)
return {"FINISHED"}
### -- MENU ENTRY --
def flat_reproject_clean_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
def flat_reproject_context_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
classes = (
GPTB_OT_batch_flat_reproject,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(flat_reproject_context_menu)
# bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu)
def unregister():
# bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(flat_reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)

214
OP_follow_curve.py Normal file
View File

@ -0,0 +1,214 @@
import bpy
from mathutils import Vector
from . import utils
class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
bl_idname = "object.create_follow_path_curve"
bl_label = "Create Follow Path Curve"
bl_description = "Create curve and add follow path constraint\
\n(remove location offset from object if any)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object
def execute(self, context):
ob = context.object
# settings = context.scene.anim_cycle_settings
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
## For bones
# root_name = fn.get_root_name(context=context)
# root = ob.pose.bones.get(root_name)
# if not root:
# self.report({'ERROR'}, f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name')
# return {"CANCELLED"}
## create curve at bone position
# loc = ob.matrix_world @ root.matrix.to_translation()
# root_axis_vec = fn.get_direction_vector_from_enum(settings.forward_axis)
## get real world direction of the root
# world_forward = (root.matrix @ root_axis_vec) - root.matrix.to_translation()
loc = ob.matrix_world.to_translation()
## X global
# TODO: Set direction orientation in view space (UP, LEFT, RIGHT, DOWN)
direction = Vector((1,0,0))
curve = utils.create_curve(location=loc,
direction=direction.normalized() * 2,
name='curve_path',
context=context)
utils.create_follow_path_constraint(ob, curve)
## reset location to remove offset
ob.location = (0,0,0)
# ob.keyframe_insert('location')
ob.rotation_euler = (0,0,0)
# ob.keyframe_insert('rotation_euler')
# refresh evaluation so constraint shows up correctly
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
return {"FINISHED"}
class GPTB_OT_edit_curve(bpy.types.Operator):
bl_idname = "object.edit_curve"
bl_label = "Edit Curve"
bl_description = "Edit curve used as follow path constraint"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
@classmethod
def poll(cls, context):
return context.object
def execute(self, context):
ob = context.object
curve = next((c.target for c in ob.constraints if c.type == 'FOLLOW_PATH' and c.target), None)
if curve is None:
self.report({"ERROR"}, 'No follow path curve found')
return {"CANCELLED"}
# Object mode, set curve as active, go Edit
utils.go_edit_mode(curve)
# curve context.mode -> EDIT_CURVE
# b.id_data.select_set(False)
ob.select_set(False)
return {"FINISHED"}
class GPTB_OT_remove_follow_path(bpy.types.Operator):
bl_idname = "object.remove_follow_path"
bl_label = "Remove Follow Path Constraint"
bl_description = "Remove follow path on object"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object
def execute(self, context):
ob = context.object
const = next((c for c in ob.constraints if c.type == 'FOLLOW_PATH'), None)
if not const:
self.report({'ERROR'}, f'No follow path constraint on "{ob.name}" found')
return {"CANCELLED"}
# store position
mat = ob.matrix_world.copy()
ob.constraints.remove(const)
# restore position
ob.matrix_world = mat
self.report({'INFO'}, f'Removed follow_path constraint on "{ob.name}"')
# Also remove offset action ? maybe give the choice
return {"FINISHED"}
class GPTB_OT_go_to_object(bpy.types.Operator):
bl_idname = "object.go_to_object"
bl_label = "Go To Object"
bl_description = "Go to object in pose mode"
bl_options = {"REGISTER", "INTERNAL"}
obj_name : bpy.props.StringProperty(options={'SKIP_SAVE'})
def execute(self, context):
obj = context.scene.objects.get(self.obj_name)
if not obj:
self.report({'ERROR'}, f'Could not find object {self.obj_name} in scene objects')
return {"CANCELLED"}
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
for ob in context.scene.objects:
ob.select_set(False)
# Set active
obj.select_set(True)
context.view_layer.objects.active = obj
if obj.type == 'ARMATURE':
bpy.ops.object.mode_set(mode='POSE', toggle=False)
self.report({'INFO'}, f'Back to pose mode, {obj.name}')
elif obj.type == 'GREASEPENCIL':
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL', toggle=False)
else:
self.report({'INFO'}, f'Back to object mode, {obj.name}')
return {"FINISHED"}
class GPTB_OT_object_from_curve(bpy.types.Operator):
bl_idname = "object.object_from_curve"
bl_label = "Back To Following Object"
bl_description = "Go on following object from current curve"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'CURVE'
def invoke(self, context, event):
curve = context.object
self.objects = []
for o in context.scene.objects:
if o.type != 'ARMATURE':
for c in o.constraints:
if c.type == 'FOLLOW_PATH' and c.target and c.target == curve:
self.objects.append(o)
else:
for pb in o.pose.bones:
for c in pb.constraints:
if c.type == 'FOLLOW_PATH' and c.target and c.target == curve:
self.objects.append(o)
break
if not self.objects:
self.report({'ERROR'}, 'No object following current curve found')
return {"CANCELLED"}
curve.select_set(False)
if len(self.objects) > 1:
return context.window_manager.invoke_props_popup(self, event) # execute on change
# set pose mode on only object available
obj = self.objects[0]
bpy.ops.object.go_to_object(obj_name=obj.name)
# bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
# for ob in context.scene.objects:
# ob.select_set(False)
# obj.select_set(True)
# context.view_layer.objects.active = obj
# bpy.ops.object.mode_set(mode='POSE', toggle=False)
# self.report({'INFO'}, f'Back to pose mode {obj.name} (constraint on {pb.name})')
return self.execute(context)
def draw(self, context):
layout = self.layout
for obj in self.objects:
layout.operator('object.go_to_object', text=obj.name, icon='OBJECT_DATA').obj_name = obj.name
def execute(self, context):
return {"FINISHED"}
classes = (
GPTB_OT_create_follow_path_curve,
GPTB_OT_edit_curve,
GPTB_OT_remove_follow_path,
GPTB_OT_go_to_object,
GPTB_OT_object_from_curve,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

84
OP_git_update.py Normal file
View File

@ -0,0 +1,84 @@
import bpy
import shutil
import subprocess
from pathlib import Path
import os
from os.path import basename
# import re
from .utils import show_message_box
"""## not used for now
class GPTB_OT_check_git(bpy.types.Operator):
'''check if git is in path'''
bl_idname = "gptb.check_git"
bl_label = "Check if git is in system path"
bl_options = {'REGISTER', 'INTERNAL'}
def invoke(self, context, event):
self.ok = shutil.which('git')
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
if self.ok:
layout.label(text='Ok ! git is in system PATH', icon='INFO')
else:
layout.label(text='Git is not in system PATH', icon='CANCEL')
layout.operator('wm.url_open', text='Download And Install From Here', icon='URL').url = 'https://git-scm.com/download/'
"""
def git_update(folder: str) -> str:
''' Try to git pull fast foward only in passed folder and return console output'''
os.chdir(folder)
name = basename(folder)
print(f'Pulling in {name}')
pull_cmd = ['git', 'pull', '--ff-only'] # git pull --ff-only
pull_ret = subprocess.check_output(pull_cmd)
return pull_ret.decode()
class GPTB_OT_git_pull(bpy.types.Operator):
"""Update addon with git pull if possible"""
bl_idname = "gptb.git_pull"
bl_label = "Gptoolbox Git Pull Update"
bl_options = {'REGISTER', 'INTERNAL'}
# def invoke(self, context, event):
# return self.execute(context)
# def draw(self, context):
def execute(self, context):
if not shutil.which('git'):
self.report({'ERROR'}, 'Git not found in path, if just installed, restart Blender/Computer')
return {'CANCELLED'}
ret = git_update(Path(__file__).parent.as_posix())
if 'Already up to date' in ret:
self.report({'INFO'}, 'Already up to date')
show_message_box(ret.rstrip('\n').split('\n'))
elif 'Fast-forward' in ret and 'Updating' in ret:
self.report({'INFO'}, 'Updated ! Restart Blender')
show_message_box(['Updated! Restart Blender.'] + ret.rstrip('\n').split('\n'))
return {'FINISHED'}
classes = (
# GPTB_OT_check_git,
GPTB_OT_git_pull,
)
def register():
if bpy.app.background:
return
for cl in classes:
bpy.utils.register_class(cl)
def unregister():
if bpy.app.background:
return
for cl in reversed(classes):
bpy.utils.unregister_class(cl)

View File

@ -1,12 +1,19 @@
import bpy
import mathutils
import math
from time import ctime
from mathutils import Vector #, Matrix
from pathlib import Path
from math import radians
from .utils import get_gp_objects, set_collection, show_message_box
from bpy.types import Operator
class GPTB_OT_copy_text(bpy.types.Operator):
from .view3d_utils import View3D
from . import utils
class GPTB_OT_copy_text(Operator):
bl_idname = "wm.copytext"
bl_label = "Copy to clipboard"
bl_label = "Copy To Clipboard"
bl_description = "Insert passed text to clipboard"
bl_options = {"REGISTER", "INTERNAL"}
@ -18,23 +25,53 @@ class GPTB_OT_copy_text(bpy.types.Operator):
self.report({'INFO'}, mess)
return {"FINISHED"}
class GPTB_OT_flipx_view(bpy.types.Operator):
bl_idname = "gp.mirror_flipx"
bl_label = "cam mirror flipx"
class GPTB_OT_flipx_view(Operator):
bl_idname = "view3d.camera_mirror_flipx"
bl_label = "Cam Mirror Flipx"
bl_description = "Invert X scale on camera to flip image horizontally"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.region_data.view_perspective == 'CAMERA'
return context.area.type == 'VIEW_3D' and \
context.region_data.view_perspective == 'CAMERA'
def execute(self, context):
context.scene.camera.scale.x *= -1
return {"FINISHED"}
class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
class GPTB_OT_view_camera_frame_fit(Operator):
bl_idname = "view3d.view_camera_frame_fit"
bl_label = "View Fit"
bl_description = "Fit the camera in view (view 1:1)"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.area.type == 'VIEW_3D' and \
context.region_data.view_perspective == 'CAMERA'
def zoom_from_fac(self, zoomfac):
from math import sqrt
return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0
def execute(self, context):
# Calculate zoom level to fit in view considering upper and side panel (Not done by native view 1:1)
# context.space_data.region_3d.view_camera_zoom = 0 # (value range: -30, - 600)
view3d = View3D()
view3d.fit_camera_view()
## re-center
# context.space_data.region_3d.view_camera_offset = (0,0)
# With a margin
# Calculate pan to fit view in viewport
return {"FINISHED"}
class GPTB_OT_rename_data_from_obj(Operator):
bl_idname = "gp.rename_data_from_obj"
bl_label = "Rename GP from object"
bl_label = "Rename GP From Object"
bl_description = "Rename the GP datablock with the same name as the object"
bl_options = {"REGISTER"}
@ -42,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
if not self.rename_all:
@ -56,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
else:
oblist = []
for o in context.scene.objects:
if o.type == 'GPENCIL':
if o.type == 'GREASEPENCIL':
if o.name == o.data.name:
continue
oblist.append(f'{o.data.name} -> {o.name}')
@ -113,9 +150,9 @@ def get_gp_alignement_vector(context):
elif orient == 'CURSOR':
return Vector((0,0,1))#.rotate(context.scene.cursor.matrix)
class GPTB_OT_draw_cam(bpy.types.Operator):
class GPTB_OT_draw_cam(Operator):
bl_idname = "gp.draw_cam_switch"
bl_label = "Draw cam switch"
bl_label = "Draw Cam Switch"
bl_description = "switch between main camera and draw (manipulate) camera"
bl_options = {"REGISTER"}
@ -170,7 +207,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
# dcam_col = bpy.data.collections.get(camcol_name)
# if not dcam_col:
set_collection(drawcam, camcol_name)
utils.set_collection(drawcam, camcol_name)
# Swap to it, unhide if necessary and hide previous
context.scene.camera = maincam
@ -188,7 +225,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
if not drawcam:
created=True
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
set_collection(drawcam, 'manip_cams')
utils.set_collection(drawcam, 'manip_cams')
if dcam_name == 'draw_cam':
drawcam.parent = maincam
@ -197,10 +234,13 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
drawcam.lock_location = (True,True,True)
# drawcam.hide_viewport = True
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout
drawcam.data = maincam.data # get data from parent
# Hide the other passepartout to let only the custom OpenGL one
maincam.data.show_passepartout = False
else:
# object cam
if created:
drawcam['maincam_name'] = context.scene.camera.name
drawcam.parent = act
@ -210,7 +250,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
drawcam.parent = act
vec = Vector((0,1,0))
if act.type == 'GPENCIL':
if act.type == 'GREASEPENCIL':
#change vector according to alignement
vec = get_gp_alignement_vector(context)
@ -238,15 +278,16 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
return {"FINISHED"}
class GPTB_OT_set_view_as_cam(bpy.types.Operator):
class GPTB_OT_set_view_as_cam(Operator):
bl_idname = "gp.set_view_as_cam"
bl_label = "Cam at view"
bl_label = "Cam At View"
bl_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.region_data.view_perspective != 'CAMERA'# need to be out of camera
return context.area.type == 'VIEW_3D' and \
context.region_data.view_perspective != 'CAMERA'# need to be out of camera
# return context.scene.camera and not context.scene.camera.name.startswith('Cam')
def execute(self, context):
@ -279,9 +320,9 @@ class GPTB_OT_set_view_as_cam(bpy.types.Operator):
return {"FINISHED"}
class GPTB_OT_reset_cam_rot(bpy.types.Operator):
class GPTB_OT_reset_cam_rot(Operator):
bl_idname = "gp.reset_cam_rot"
bl_label = "Reset rotation"
bl_label = "Reset Rotation"
bl_description = "Reset rotation of the draw manipulation camera"
bl_options = {"REGISTER"}
@ -290,52 +331,115 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator):
return context.scene.camera and not context.scene.camera.name.startswith('Cam')
# return context.region_data.view_perspective == 'CAMERA'# check if in camera
def execute(self, context):
# dcam_name = 'draw_cam'
# camcol_name = 'manip_cams'
drawcam = context.scene.camera
if drawcam.parent.type == 'CAMERA':
## align to parent camera
drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset
# drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset
elif drawcam.parent:
## there is a parent, so align the Y of the camera to object's Z
# drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong
pass
def get_center_view(self, context, cam):
from bpy_extras.view3d_utils import location_3d_to_region_2d
frame = cam.data.view_frame()
mat = cam.matrix_world
frame = [mat @ v for v in frame]
frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
return mathutils.Vector((center_x, center_y))
def get_ui_ratio(self, context):
'''correct ui overlap from header/toolbars'''
regs = context.area.regions
if context.preferences.system.use_region_overlap:
w = context.area.width
# minus tool header
h = context.area.height - regs[0].height
else:
# minus tool leftbar + sidebar right
w = context.area.width - regs[2].width - regs[3].width
# minus tool header + header
h = context.area.height - regs[0].height - regs[1].height
self.ratio = h / w
self.ratio_inv = w / h
def execute(self, context):
cam = context.scene.camera
if not cam.parent or cam.parent.type != 'CAMERA':
self.report({'ERROR'}, "No parents to refer to for rotation reset")
return {"CANCELLED"}
# store original rotation mode
org_rotation_mode = cam.rotation_mode
# set to euler to works with quaternions, restored at finish
cam.rotation_mode = 'XYZ'
# store camera matrix world
org_cam_matrix = cam.matrix_world.copy()
org_cam_z = cam.rotation_euler.z
## initialize current view_offset in camera
view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset)
# Do the reset to parent transforms
cam.matrix_world = cam.parent.matrix_world # wrong, get the parent rotation offset
# Get diff angle
angle = cam.rotation_euler.z - org_cam_z
# create rotation matrix with negative angle (we want to counter the move)
neg = -angle
rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]])
# restore original rotation mode
cam.rotation_mode = org_rotation_mode
self.get_ui_ratio(context)
# apply rotation matrix
new_cam_offset = view_cam_offset.copy()
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio)) # apply screen ratio
new_cam_offset.rotate(rot_mat2d)
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv)) # restore screen ratio
context.space_data.region_3d.view_camera_offset = new_cam_offset
return {"FINISHED"}
class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
class GPTB_OT_toggle_mute_animation(Operator):
bl_idname = "gp.toggle_mute_animation"
bl_label = "Toggle animation mute"
bl_label = "Toggle Animation Mute"
bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False)
skip_gp : bpy.props.BoolProperty(default=False)
skip_obj : bpy.props.BoolProperty(default=False)
mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL
def invoke(self, context, event):
self.selection = event.shift
return self.execute(context)
def execute(self, context):
def set_action_mute(self, act):
for i, fcu in enumerate(act.fcurves):
print(i, fcu.data_path, fcu.array_index)
# fcu.group don't have mute attribute in api.
fcu.mute = self.mute
for g in act.groups:
g.mute = self.mute
def execute(self, context):
if self.selection:
pool = context.selected_objects
else:
pool = context.scene.objects
for o in pool:
if self.skip_gp and o.type == 'GPENCIL':
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
continue
if self.skip_obj and o.type != 'GPENCIL':
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
continue
if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue
# mute attribute animation for GP and cameras
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
gp_act = o.data.animation_data.action
if gp_act:
print(f'\n---{o.name} data:')
self.set_action_mute(gp_act)
if not o.animation_data:
continue
@ -343,15 +447,45 @@ class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
if not act:
continue
for i, fcu in enumerate(act.fcurves):
print(i, fcu.data_path, fcu.array_index)
fcu.mute = self.mute
print(f'\n---{o.name}:')
self.set_action_mute(act)
return {'FINISHED'}
class GPTB_OT_list_disabled_anims(bpy.types.Operator):
class GPTB_OT_toggle_hide_gp_modifier(Operator):
bl_idname = "gp.toggle_hide_gp_modifier"
bl_label = "Toggle Modifier Hide"
bl_description = "Show/Hide viewport on GP objects modifier\
\nOnly touch modifier that are showed in render\
\nShift + click to affect selection only"
bl_options = {"REGISTER"}
show : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
def invoke(self, context, event):
self.selection = event.shift
return self.execute(context)
def execute(self, context):
if self.selection:
pool = context.selected_objects
else:
pool = context.scene.objects
for o in pool:
if o.type != 'GREASEPENCIL':
continue
for m in o.modifiers:
# skip modifier that are not visible in render
if not m.show_render:
continue
m.show_viewport = self.show
return {'FINISHED'}
class GPTB_OT_list_disabled_anims(Operator):
bl_idname = "gp.list_disabled_anims"
bl_label = "List disabled anims"
bl_label = "List Disabled Anims"
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
bl_options = {"REGISTER"}
@ -372,25 +506,41 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
pool = context.scene.objects
for o in pool:
# if self.skip_gp and o.type == 'GPENCIL':
# if self.skip_gp and o.type == 'GREASEPENCIL':
# continue
# if self.skip_obj and o.type != 'GPENCIL':
# if self.skip_obj and o.type != 'GREASEPENCIL':
# continue
if o.type == 'GREASEPENCIL':
if o.data.animation_data:
gp_act = o.data.animation_data.action
if gp_act:
for i, fcu in enumerate(gp_act.fcurves):
if fcu.mute:
if o not in oblist:
oblist.append(o)
li.append(f'{o.name}:')
li.append(f' - {fcu.data_path} {fcu.array_index}')
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
for g in act.groups:
if g.mute:
li.append(f'{o.name} - group: {g.name}')
for i, fcu in enumerate(act.fcurves):
# print(i, fcu.data_path, fcu.array_index)
if fcu.mute:
if o not in oblist:
oblist.append(o)
li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}')
else:
li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}')
li.append(f'{o.name}:')
li.append(f' - {fcu.data_path} {fcu.array_index}')
if li:
show_message_box(li)
utils.show_message_box(li)
else:
self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}")
return {'FINISHED'}
@ -398,7 +548,7 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
## TODO presets are still not used... need to make a custom preset save/remove/quickload manager to be efficient (UIlist ?)
class GPTB_OT_overlay_presets(bpy.types.Operator):
class GPTB_OT_overlay_presets(Operator):
bl_idname = "gp.overlay_presets"
bl_label = "Overlay presets"
bl_description = "Overlay save/load presets for showing only whats needed"
@ -453,7 +603,7 @@ class GPTB_OT_overlay_presets(bpy.types.Operator):
return {'FINISHED'}
class GPTB_OT_clear_active_frame(bpy.types.Operator):
class GPTB_OT_clear_active_frame(Operator):
bl_idname = "gp.clear_active_frame"
bl_label = "Clear Active Frame"
bl_description = "Delete all strokes in active frames"
@ -461,7 +611,7 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
obj = context.object
@ -469,32 +619,186 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator):
if not l:
self.report({'ERROR'}, 'No layers')
return {'CANCELLED'}
f = l.active_frame
f = l.current_frame()
if not f:
self.report({'ERROR'}, 'No active frame')
return {'CANCELLED'}
ct = len(f.strokes)
ct = len(f.drawing.strokes)
if not ct:
self.report({'ERROR'}, 'Active frame already empty')
return {'CANCELLED'}
for s in reversed(f.strokes):
f.strokes.remove(s)
for s in reversed(f.drawing.strokes):
f.drawing.strokes.remove(s)
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
return {'FINISHED'}
class GPTB_OT_check_canvas_alignement(Operator):
bl_idname = "gp.check_canvas_alignement"
bl_label = "Check Canvas Alignement"
bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
# if lock_axis is 'VIEW' then the draw axis is always aligned
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
def execute(self, context):
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
self.report({'INFO'}, 'Drawing plane use "View" (always aligned)')
return {'FINISHED'}
_angle, ret, message = utils.check_angle_from_view(obj=context.object, context=context)
if not ret or not message:
self.report({'ERROR'}, 'Could not get view angle infos')
return {'CANCELLED'}
title = 'Aligned \o/' if ret == 'INFO' else "Not aligned !"
if context.region_data.view_perspective != 'CAMERA':
title = title + ' ( not in camera view)'
utils.show_message_box(_message=message, _title=title, _icon=ret)
# self.report({ret}, message)
return {'FINISHED'}
class GPTB_OT_step_select_frames(Operator):
bl_idname = "gptb.step_select_frames"
bl_label = "Step Select Frame"
bl_description = "Select frames by a step frame value"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
start : bpy.props.IntProperty(name='Start Frame',
description='Start frame of the step animation',
default=100)
step : bpy.props.IntProperty(name='Step',
description='Step of the frame, value of 2 select one frame on two',
default=2,
min=2)
strict : bpy.props.BoolProperty(name='Strict',
description='Strictly select step frame from start to scene end range\
\nElse reset step when a gap exsits already',
default=False)
# TODO: add option to start at cursor (True default)
def invoke(self, context, execute):
## list frame to keep
return context.window_manager.invoke_props_dialog(self, width=450)
# return self.execute(context)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, 'step')
col.prop(self, 'strict')
if self.strict:
col.prop(self, 'start')
## helper (need more work)
# col.separator()
# range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}"
# col.label(text=f'Will keep {range_string}')
if not self.strict:
col.label(text=f'Each gap will be considered a new step start', icon='INFO')
def execute(self, context):
numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)]
self.to_select = numbers
## Negative switch : list frames to remove
self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers]
gp = context.object.data
## Get frame summary (reset start after each existing gaps)
key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames]))
key_summary.sort()
print('key summary: ', key_summary)
start = key_summary[0]
if self.strict:
to_select = self.to_select
else:
to_select = []
prev = None
for k in key_summary:
print(k, prev)
if prev is not None and k != prev + 1:
## this is a gap ! new start
prev = start = k
# print('new start', start)
continue
new_range = [i for i in range(start, key_summary[-1] + 1, self.step)]
# print('new_range: ', new_range)
if k not in new_range:
to_select.append(k)
prev = k
## deselect all
for l in gp.layers:
for f in l.frames:
f.select = False
print('To select:', to_select)
gct = 0
for i in to_select:
ct = 0
for l in gp.layers:
frame = next((f for f in l.frames if f.frame_number == i), None)
if not frame:
continue
## Select instead of remove
frame.select = True
## Optionnally remove frames ?
# l.frames.remove(frame)
ct += 1
# print(f'{i}: Selected {ct} frame(s)')
gct += ct
self.report({'INFO'}, f'Selected {gct} frames')
return {"FINISHED"}
class GPTB_OT_open_addon_prefs(Operator):
bl_idname = "gptb.open_addon_prefs"
bl_label = "Open Addon Prefs"
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
utils.open_addon_prefs()
return {'FINISHED'}
classes = (
GPTB_OT_copy_text,
GPTB_OT_flipx_view,
GPTB_OT_view_camera_frame_fit,
GPTB_OT_rename_data_from_obj,
GPTB_OT_draw_cam,
GPTB_OT_set_view_as_cam,
GPTB_OT_reset_cam_rot,
GPTB_OT_toggle_mute_animation,
GPTB_OT_toggle_hide_gp_modifier,
GPTB_OT_list_disabled_anims,
GPTB_OT_clear_active_frame,
GPTB_OT_check_canvas_alignement,
GPTB_OT_step_select_frames,
GPTB_OT_open_addon_prefs,
)
def register():

View File

@ -1,10 +1,15 @@
import bpy
from bpy.types import Operator
from . import utils
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]
if not context:
return [('None', 'None','None')]
if not context.object:
return [('None', 'None','None')]
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
# try:
# except:
# return [("", "", "")]
@ -13,22 +18,31 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate and send to layer'
bl_label = 'Duplicate Send To Layer'
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
# important to have the updated enum here as bl_property
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
description="Duplicate selected keys in active layer and send them to chosen layer",
items=get_layer_list,
options={'HIDDEN'},
)
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod
def description(cls, context, properties):
if properties.delete_source:
return f"Move selected keys in active layer to chosen layer"
else:
return f"Copy selected keys in active layer and send to chosen layer"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' and context.space_data.ui_mode == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'\
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' 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)
@ -50,31 +64,27 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace)
## remove overlapping frames
## Remove overlapping frames
for f in reversed(to_replace):
target_layer.frames.remove(f)
target_layer.frames.remove(f.frame_number)
## copy original frames
## Copy original frames
for f in selected_frames:
target_layer.frames.copy(f)
utils.copy_frame_at(f, target_layer, f.frame_number)
# target_layer.frames.copy(f) # GPv2
sent = len(selected_frames)
## delete original frames as an option
## Delete original frames as an option
if self.delete_source:
for f in reversed(selected_frames):
act_layer.frames.remove(f)
act_layer.frames.remove(f.frame_number)
mess = f'{sent} keys moved'
else:
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'}
@ -103,15 +113,13 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
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)
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
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)
# km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter)
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True)
kmi.properties.delete_source = True
addon_keymaps.append((km,kmi))
@ -122,18 +130,35 @@ def unregister_keymaps():
addon_keymaps.clear()
def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN'
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_duplicate_send_to_layer,
)
def register():
if not bpy.app.background:
if bpy.app.background:
return
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
bpy.types.DOPESHEET_MT_key.append(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
def unregister():
if not bpy.app.background:
if bpy.app.background:
return
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,5 +1,5 @@
import bpy
from .utils import get_addon_prefs
from .utils import get_addon_prefs, is_locked, is_hidden
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@ -10,7 +10,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
next : BoolProperty(
name="Next GP keyframe", description="Go to next active GP keyframe",
@ -36,6 +36,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
))
def execute(self, context):
@ -44,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
return {"CANCELLED"}
if self.target == 'ACTIVE':
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not l.hide]
gpl = [l for l in context.object.data.layers if not is_hidden(l)]
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 is_hidden(l) and not is_locked(l)]
if self.keyframe_type != 'NONE':
# use shortcut choice override
@ -171,13 +172,14 @@ def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Screen", space_type = "EMPTY")
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_next_keycode, value="PRESS", alt=pref.kfj_next_alt, ctrl=pref.kfj_next_ctrl, shift=pref.kfj_next_shift, any=False)
kmi.properties.next = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_prev_keycode, value="PRESS", alt=pref.kfj_prev_alt, ctrl=pref.kfj_prev_ctrl, shift=pref.kfj_prev_shift, any=False)
kmi.properties.next = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_next_keycode, value="PRESS", alt=pref.kfj_next_alt, ctrl=pref.kfj_next_ctrl, shift=pref.kfj_next_shift, any=False)
kmi.properties.next = True
addon_keymaps.append((km, kmi))
def unregister_keymaps():
# print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg
for km, kmi in addon_keymaps:

786
OP_layer_manager.py Normal file
View File

@ -0,0 +1,786 @@
from os import error
import bpy
import re
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.app.handlers import persistent
from .utils import get_addon_prefs, is_vector_close
# --- OPS ---
# PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
# pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter
# pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
# pattern = r'^(?P<tag>[A-Z]{1,6}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # named
# pattern = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # group start ' - '
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
# TODO: allow a more flexible prefix pattern
def layer_name_build(layer, prefix='', desc='', suffix=''):
'''GET a layer and argument to build and assign name
return new name
'''
prefs = get_addon_prefs()
sep = prefs.separator
name = old = layer.name
pattern = PATTERN.replace('_', sep) # set separator
res = re.search(pattern, name.strip())
# prefix -> tag
# prefix2 -> tag2
# desc -> name
# suffix -> sfix
grp = '' if res.group('grp') is None else res.group('grp')
tag = '' if res.group('tag') is None else res.group('tag')
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
name = '' if res.group('name') is None else res.group('name')
sfix = '' if res.group('sfix') is None else res.group('sfix')
inc = '' if res.group('inc') is None else res.group('inc')
if grp:
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
if prefix:
if prefix == 'prefixkillcode':
tag = ''
else:
tag = prefix.upper().strip() + sep
# if prefix2:
# tag2 = prefix2.upper().strip() + sep
if desc:
name = desc
if suffix:
if suffix == 'suffixkillcode':
sfix = ''
else:
sfix = sep + suffix.upper().strip()
# check if name is available without the increment ending
new = f'{grp}{tag}{name}{sfix}'
layer.name = new
## update name in modifier targets
if old != new:
# find objects using this GP datablock
for ob_user in [o for o in bpy.data.objects if o.data == layer.id_data]: # bpy.context.scene.objects
# maybe a more elegant way exists to find all objects users ?
# update Gpencil modifier targets
for mod in ob_user.modifiers:
if not hasattr(mod, 'layer_filter'):
continue
if mod.layer_filter == old:
mod.layer_filter = new
"""
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
'''GET a layer and infos to build name
Can take one or two prefix and description/name of the layer)
'''
prefs = get_addon_prefs()
sep = prefs.separator
name = layer.name
pattern = pattern.replace('_', sep) # set separator
res = re.search(pattern, name.strip())
p1 = '' if res.group(1) is None else res.group(1)
p2 = '' if res.group(2) is None else res.group(2)
p3 = '' if res.group(3) is None else res.group(3)
p4 = '' if res.group(4) is None else res.group(4)
if prefix:
if prefix == 'prefixkillcode':
p1 = ''
else:
p1 = prefix.upper().strip() + sep
if prefix2:
p2 = prefix2.upper().strip() + sep
if desc:
p3 = desc
if suffix:
if suffix == 'suffixkillcode':
p4 = ''
else:
p4 = sep + suffix.upper().strip()
new = f'{p1}{p2}{p3}{p4}'
layer.name = new
"""
## multi-prefix solution (Caps letters)
class GPTB_OT_layer_name_build(Operator):
bl_idname = "gp.layer_name_build"
bl_label = "Layer Name Build"
bl_description = "Change prefix of layer name"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
prefix : StringProperty(default='', options={'SKIP_SAVE'})
# prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
desc : StringProperty(default='', options={'SKIP_SAVE'})
suffix : StringProperty(default='', options={'SKIP_SAVE'})
tooltip : StringProperty(default='', options={'SKIP_SAVE'})
@classmethod
def description(cls, context, properties):
tag = properties.prefix if properties.prefix else properties.suffix
if properties.tooltip:
return f"Use prefix: {tag} ({properties.tooltip})"
else:
return f"Use prefix: {tag}"
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
act = ob.data.layer_groups.active
if not act:
self.report({'ERROR'}, 'No layer active')
return {"CANCELLED"}
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
## /!\ Deactivate multi-selection on layer !
## Somethimes it affect a random layer that is still considered selected
# for l in gpl:
# if l.select or l == act:
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
return {"FINISHED"}
#-## SELECTION MANAGEMENT ##-#
def activate_channel_group_color(context):
if not context.preferences.edit.use_anim_channel_group_colors:
context.preferences.edit.use_anim_channel_group_colors = True
def refresh_areas():
for area in bpy.context.screen.areas:
area.tag_redraw()
def build_layers_targets_from_dopesheet(context):
'''Return all selected layers on context GP dopesheet according to seelction and filters'''
ob = context.object
gpl = context.object.data.layers
act = gpl.active
dopeset = context.space_data.dopesheet
if dopeset.show_only_selected:
pool = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
else:
pool = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
if not dopeset.show_hidden:
pool = [o for o in pool if o.visible_get()]
layer_pool = [l for o in pool for l in o.data.layers]
layer_pool = list(set(layer_pool)) # remove dupli-layers from same data source with
# apply search filter
if dopeset.filter_text:
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.name.lower()) ^ dopeset.use_filter_invert]
return layer_pool
def build_dope_gp_list(layer_list):
'''Take a list of GP layers return a dict with pairs {gp data : own layer list}'''
from collections import defaultdict
gps = defaultdict(list)
for l in layer_list:
gps[l.id_data].append(l)
return gps
class GPTB_OT_select_set_same_prefix(Operator):
bl_idname = "gp.select_same_prefix"
bl_label = "Select Same Prefix"
bl_description = "Select layers that have the same prefix as active\nSet with ctrl+clic"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=(
('SELECT', "Select", "Select layer with same prefix as active"),
('SET', "Set", "Set prefix on selected layer to the same as active"),
),
)
def invoke(self, context, event):
if event.ctrl:
self.mode = 'SET'
return self.execute(context)
def execute(self, context):
prefs = get_addon_prefs()
sep = prefs.separator # '_'
gp = context.object.data
act = gp.layers.active
pool = build_layers_targets_from_dopesheet(context)
if not pool:
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
return {"CANCELLED"}
gp_dic = build_dope_gp_list(pool)
if not act:
# Check in other displayed layer if there is an active one
for gp, _layer_list in gp_dic.items():
if gp.layers.active:
# overwrite gp variable at the same time
act = gp.layers.active
break
if not act:
self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
res = re.search(PATTERN, act.name)
if not res:
self.report({'ERROR'}, f'Error scanning {act.name}')
return {"CANCELLED"}
namespace = res.group('tag')
if not namespace:
self.report({'WARNING'}, f'No prefix detected in {act.name} with separator {sep}')
return {"CANCELLED"}
if self.mode == 'SELECT':
## with split
# namespace = act.name.split(sep,1)[0]
# namespace_bool_list = [l.name.split(sep,1)[0] == namespace for l in gpl]
## with reg # only active
# namespace_bool_list = [l.name.split(sep,1)[0] + sep == namespace for l in gpl]
# gpl.foreach_set('select', namespace_bool_list)
## don't work Need Foreach set per gp
# for l in pool:
# l.select = l.name.split(sep,1)[0] + sep == namespace
for gp, layers in gp_dic.items():
# check namespace + restrict selection to visible layers according to filters
# TODO : Should use the regex pattern to detect and compare r.group('tag')
namespace_bool_list = [(l in layers) and (l.name.split(sep,1)[0] + sep == namespace) for l in gp.layers]
gp.layers.foreach_set('select', namespace_bool_list)
elif self.mode == 'SET':
for l in pool:
if not l.select or l == act:
continue
layer_name_build(l, prefix=namespace.strip(sep))
refresh_areas()
return {"FINISHED"}
class GPTB_OT_select_set_same_color(Operator):
bl_idname = "gp.select_same_color"
bl_label = "Select Same Color"
bl_description = "Select layers that have the same color as active\nSet with ctrl+clic"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=(
('SELECT', "Select", "Select layer with same prefix as active"),
('SET', "Set", "Set prefix on selected layer to the same as active"),
),
)
def invoke(self, context, event):
if event.ctrl:
self.mode = 'SET'
return self.execute(context)
def execute(self, context):
gp = context.object.data
act = gp.layers.active
pool = build_layers_targets_from_dopesheet(context)
if not pool:
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
return {"CANCELLED"}
gp_dic = build_dope_gp_list(pool)
if not act:
# Check in other displayed layer if there is an active one
for gp, _layer_list in gp_dic.items():
if gp.layers.active:
# overwrite gp variable at the same time
act = gp.layers.active
break
if not act:
self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
color = act.channel_color
if self.mode == 'SELECT':
## NEED FOREACH TO APPLY SELECT
## Only on active object
# same_color_bool = [l.channel_color == act.channel_color for l in gpl]
# gpl.foreach_set('select', same_color_bool) # only
# On multiple objects -- don't work, need foreach
# for l in pool:
# print(l.id_data.name, l.name, l.channel_color == act.channel_color)
# l.select = l.channel_color == act.channel_color
"""
gps = []
for l in pool:
if l.id_data not in gps:
gps.append(l.id_data)
for gp in gps:
same_color_bool = [(l in pool) and is_vector_close(l.channel_color, color) for l in gp.layers]
gp.layers.foreach_set('select', same_color_bool)
"""
for gp, layers in gp_dic.items():
# check color and restrict selection to visible layers according to filters
same_color_bool = [(l in layers) and is_vector_close(l.channel_color, color) for l in gp.layers]
gp.layers.foreach_set('select', same_color_bool)
elif self.mode == 'SET':
activate_channel_group_color(context)
for l in pool: # only on active object use gpl
if not l.select or l == act:
continue
l.channel_color = color
refresh_areas()
return {"FINISHED"}
def replace_layer_name(target, replacement, selected_only=True, prefix_only=True, regex=False):
prefs = get_addon_prefs()
sep = prefs.separator
if not target:
return
gpl = bpy.context.object.data.layers
if selected_only:
lays = [l for l in gpl if l.select] # exclude : l.name != 'background'
else:
lays = [l for l in gpl] # exclude : if l.name != 'background'
ct = 0
for l in lays:
old = l.name
if regex:
new = re.sub(target, replacement, l.name)
if old != new:
l.name = new
print('rename:', old, '-->', new)
ct += 1
continue
if prefix_only:
if not sep in l.name:
# only if separator exists
continue
splited = l.name.split(sep)
prefix = splited[0]
new_prefix = prefix.replace(target, replacement)
if prefix != new_prefix:
splited[0] = new_prefix
l.name = sep.join(splited)
print('rename:', old, '-->', l.name)
ct += 1
else:
new = l.name.replace(target, replacement)
if old != new:
l.name = new
print('rename:', old, '-->', new)
ct += 1
return ct
class GPTB_OT_rename_gp_layer(Operator):
'''rename GP layers based on a search and replace'''
bl_idname = "gp.rename_gp_layers"
bl_label = "Rename Gp Layers"
bl_description = "Search/Replace string in all GP layers"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
selected: BoolProperty(name="Selected Only", description="Affect only selected layers", default=False)
prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skip layer without separator in name)", default=False)
use_regex: BoolProperty(name="Regex", description="use regular expression (advanced), equivalent to python re.sub()", default=False)
def execute(self, context):
count = replace_layer_name(self.find, self.replace, selected_only=self.selected, prefix_only=self.prefix, regex=self.use_regex)
if count:
mess = str(count) + ' layers renamed'
self.report({'INFO'}, mess)
else:
self.report({'WARNING'}, 'No text found !')
return{'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
row = layout.row()
row_a= row.row()
row_a.prop(self, "selected")
row_b= row.row()
row_b.prop(self, "prefix")
row_c= row.row()
row_c.prop(self, "use_regex")
row_b.active = not self.use_regex
layout.prop(self, "find")
layout.prop(self, "replace")
## --- UI layer panel---
def layer_name_builder_ui(self, context):
'''appended to DATA_PT_grease_pencil_layers'''
prefs = get_addon_prefs()
if not prefs.show_prefix_buttons:
return
if not len(prefs.prefixes.namespaces) and not len(prefs.suffixes.namespaces):
return
layout = self.layout
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
# layout.separator()
col = layout.column()
line_limit = 8
if len(prefs.prefixes.namespaces):
ct = 0
# can't use enumerate cause there can be hided prefix
for namespace in prefs.prefixes.namespaces:
if namespace.hide:
continue
if ct % line_limit == 0:
row = col.row(align=True)
ct += 1
op = row.operator("gp.layer_name_build", text=namespace.tag)
op.prefix = namespace.tag
op.tooltip = namespace.name
if ct > 0:
row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'
## old single string prefix method
"""
if prefs.prefixes:
p = prefs.prefixes.split(',')
for i, prefix in enumerate(all_prefixes):
if i % line_limit == 0:
row = col.row(align=True)
row.operator("gp.layer_name_build", text=prefix.upper() ).prefix = prefix
row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'
## secondary prefix ?
if prefs.suffixes:
all_suffixes = prefs.suffixes.split(',')
for i, suffix in enumerate(all_suffixes):
if i % line_limit == 0:
row = col.row(align=True)
row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
"""
## name (description of layer content)
row = col.row(align=True)
row.prop(context.scene.gptoolprops, 'layer_name', text='')
## mimic groups using dash (disabled for now)
# row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW')
# row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE')
## no need for desc ops, already trigerred from update
# row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name
if len(prefs.suffixes.namespaces):
ct = 0
# can't use enumerate cause there can be hided prefix
for namespace in prefs.suffixes.namespaces:
if namespace.hide:
continue
if ct % line_limit == 0:
row = col.row(align=True)
ct += 1
op = row.operator("gp.layer_name_build", text=namespace.tag)
op.suffix = namespace.tag
op.tooltip = namespace.name
if ct > 0:
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
## --- UI dopesheet ---
def gpencil_dopesheet_header(self, context):
'''to append in DOPESHEET_HT_header'''
layout = self.layout
st = context.space_data
if st.mode != 'GREASEPENCIL':
return
row = layout.row(align=True)
# row.operator('gp.active_channel_color_to_selected', text='', icon='RESTRICT_COLOR_ON')
row.operator('gp.select_same_prefix', text='', icon='SYNTAX_OFF') # SORTALPHA, SMALL_CAPS
row.operator('gp.select_same_color', text='', icon='RESTRICT_COLOR_ON')
## --- UI context menu ---
def gpencil_layer_dropdown_menu(self, context):
'''to append in GPENCIL_MT_layer_context_menu'''
self.layout.operator('gp.create_empty_frames', icon='KEYFRAME')
self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')
## handler and msgbus
def obj_layer_name_callback():
'''assign layer name properties so user an tweak it'''
ob = bpy.context.object
if not ob or ob.type != 'GREASEPENCIL':
return
if not ob.data.layers.active:
return
## Set selection to active object ot avoid un-sync selection on Layers stack
## (happen when an objet is selected but not active with 'lock object mode')
for l in ob.data.layers:
l.select = l == ob.data.layers.active
res = re.search(PATTERN, ob.data.layers.active.name.strip())
if not res:
return
if not res.group('name'):
return
# print('grp:', res.group('grp'))
# print('tag:', res.group('tag'))
# print('name:', res.group('name'))
# print('sfix:', res.group('sfix'))
# print('inc:', res.group('inc'))
bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
## old gpv2
# def subscribe_layer_change():
# subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
# bpy.msgbus.subscribe_rna(
# key=subscribe_to,
# # owner of msgbus subcribe (for clearing later)
# # owner=handle,
# owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
# # Args passed to callback function (tuple)
# args=(),
# # Callback function for property update
# notify=obj_layer_name_callback,
# options={'PERSISTENT'},
# )
def subscribe_layer_change():
subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
# owner=handle,
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime...
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=obj_layer_name_callback,
options={'PERSISTENT'},
)
@persistent
def subscribe_layer_change_handler(dummy):
subscribe_layer_change()
##--- Add layers
class GPTB_PT_layer_name_ui(bpy.types.Panel):
bl_space_type = 'TOPBAR' # dummy
bl_region_type = 'HEADER'
bl_options = {'INSTANCED'}
bl_label = 'Layer Rename'
bl_ui_units_x = 14
def invoke(self, context, event):
# all_addons_l = get_modifier_list()
wm = context.window_manager
wm.invoke_props_dialog(self) # , width=600
return {'FINISHED'}
def draw(self, context):
layout = self.layout
# def row_with_icon(layout, icon):
# # Edit first editable button in popup
# row = layout.row()
# row.activate_init = True
# row.label(icon=icon)
# return row
# row = row_with_icon(layout, 'OUTLINER_DATA_GP_LAYER')
row = layout.row()
row.activate_init = True
row.label(icon='OUTLINER_DATA_GP_LAYER')
row.prop(context.object.data.layers.active, 'name', text='')
def add_layer(context):
bpy.ops.gpencil.layer_add()
context.object.data.layers.active.use_lights = False
class GPTB_OT_add_gp_layer_with_rename(Operator):
bl_idname = "gp.add_layer_rename"
bl_label = "Add Rename GPencil Layer"
bl_description = "Create a new gp layer with use light toggled off and popup a rename box"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
add_layer(context)
bpy.ops.wm.call_panel(name="GPTB_PT_layer_name_ui", keep_open = False)
return {"FINISHED"}
class GPTB_OT_add_gp_layer(Operator):
bl_idname = "gp.add_layer"
bl_label = "Add GPencil Layer"
bl_description = "Create a new gp layer with use light toggled off"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
add_layer(context)
return {"FINISHED"}
addon_keymaps = []
def register_keymaps():
if bpy.app.background:
return
addon = bpy.context.window_manager.keyconfigs.addon
##---# Insert Layers
## Insert new gp layer (with no use_light)
km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
kmi = km.keymap_items.new('gp.add_layer', type='INSERT', value='PRESS')
addon_keymaps.append((km, kmi))
## Insert new gp layer (with no use_light and immediately pop up a box to rename)
# km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
kmi = km.keymap_items.new('gp.add_layer_rename', type='INSERT', value='PRESS', shift=True)
addon_keymaps.append((km, kmi))
##---# F2 rename calls
## Direct rename active layer in Paint mode
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
kmi.properties.name = 'GPTB_PT_layer_name_ui'
kmi.properties.keep_open = False
addon_keymaps.append((km, kmi))
## Same in edit mode
km = addon.keymaps.new(name = "Grease Pencil Stroke Edit Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
kmi.properties.name = 'GPTB_PT_layer_name_ui'
kmi.properties.keep_open = False
addon_keymaps.append((km, kmi))
def unregister_keymaps():
if bpy.app.background:
return
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
classes=(
GPTB_OT_rename_gp_layer,
GPTB_OT_layer_name_build,
GPTB_OT_select_set_same_prefix,
GPTB_OT_select_set_same_color,
## Layer add and pop-up rename
GPTB_PT_layer_name_ui, # pop-up
GPTB_OT_add_gp_layer_with_rename, # shift+Ins
GPTB_OT_add_gp_layer, # Ins
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.append(gpencil_layer_dropdown_menu)
bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
register_keymaps()
# Directly set msgbus to work at first addon activation
bpy.app.timers.register(subscribe_layer_change, first_interval=1)
def unregister():
unregister_keymaps()
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# Delete layer index trigger
# /!\ can remove msgbus made for other functions or other addons using same owner
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

205
OP_layer_namespace.py Normal file
View File

@ -0,0 +1,205 @@
import bpy
import os
import re
from .utils import get_addon_prefs
from .functions import redraw_ui
from .__init__ import set_namespace_env
class GPTB_OT_reset_project_namespaces(bpy.types.Operator):
bl_idname = "gptb.reset_project_namespaces"
bl_label = "Reload Project Names"
bl_description = "Reset projects namespaced from environnement variable"
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
prefs = get_addon_prefs()
prefix_list = os.getenv('PREFIXES')
suffix_list = os.getenv('SUFFIXES')
if not prefix_list and not suffix_list:
self.report({'WARNING'}, "No name list in env (variables: 'PREFIXES','SUFFIXES')")
return {'CANCELLED'}
for propname in ('prefixes', 'suffixes'):
pg = getattr(prefs, propname)
uilist = pg.namespaces
uilist.clear()
missing = []
if prefix_list:
set_namespace_env('PREFIXES', prefs.prefixes)
else:
missing.append('prefixes')
if suffix_list:
set_namespace_env('SUFFIXES', prefs.suffixes)
else:
missing.append('suffixes')
if missing:
self.report({'WARNING'}, f'No {" and ".join(missing)} presets to load from project env')
return {'FINISHED'}
class GPTB_OT_add_namespace_entry(bpy.types.Operator):
bl_idname = "gptb.add_namespace_entry"
bl_label = "Add Namespace Entry"
bl_description = "Add item in list"
bl_options = {'REGISTER', 'INTERNAL'}
idx : bpy.props.IntProperty()
new : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
propname : bpy.props.StringProperty(default='prefixes', options={'SKIP_SAVE'})
def invoke(self, context, event):
self.pg = getattr(get_addon_prefs(), self.propname)
self.proptype = self.propname[:-2]
## Basic:
# self.pg.namespaces.add()
# return {'FINISHED'}# can just add empty entry and leave...
if self.new:
self.pg.namespaces.add()
self.idx = len(self.pg.namespaces) - 1
return context.window_manager.invoke_props_dialog(self, width=450)
def draw(self, context):
layout = self.layout
# layout.use_property_split = True
item = self.pg.namespaces[self.idx]
layout.label(text=f'Enter {self.proptype.title()}:', icon='INFO')
layout.prop(item, 'tag', text=self.proptype.title())
if item.tag and not re.match(r'^[A-Z]{2}$', item.tag):
layout.label(text=f'{self.propname.title()} are preferably two capital letter (ex: CO)', icon='ERROR')
layout.separator()
layout.label(text='Provide a name (Optional):', icon='INFO')
layout.prop(item, 'name')
def execute(self, context):
item = self.pg.namespaces[self.idx]
## Here can perform post add checks
# (check for duplicate ?)
# all_prefix = [n.tag for i, n in enumerate(self.pg.namespaces) if i != self.pg.idx]
if self.new:
# in case of new addition, remove just added if nothing specified
if not item.tag and not item.name:
self.pg.namespaces.remove(self.idx)
redraw_ui()
return {'FINISHED'}
class GPTB_OT_remove_namespace_entry(bpy.types.Operator):
bl_idname = "gptb.remove_namespace_entry"
bl_label = "Remove Namespace Entry"
bl_description = "Remove item in list"
bl_options = {'REGISTER', 'INTERNAL'}
propname : bpy.props.StringProperty(default='prefixes', options={'SKIP_SAVE'})
def execute(self, context):
self.pg = getattr(get_addon_prefs(), self.propname)
entry_count = len(self.pg.namespaces)
if not entry_count:
return {'CANCELLED'}
# check if index is out of range
if not (0 <= self.pg.idx < entry_count):
self.report({"ERROR"}, 'Must select an entry to remove it')
return {'CANCELLED'}
item = self.pg.namespaces[self.pg.idx]
if item.is_project:
self.report({"ERROR"}, 'Cannot remove a prefix that is defined by project, hide it instead')
return {'CANCELLED'}
self.pg.namespaces.remove(self.pg.idx)
self.pg.idx -= 1
redraw_ui()
return {'FINISHED'}
class GPTB_OT_move_item(bpy.types.Operator):
bl_idname = "gptb.move_item"
bl_label = "Move Item"
bl_description = "Move item in list up or down"
bl_options = {'REGISTER', 'INTERNAL'}
# direction : bpy.props.IntProperty(default=1)
direction : bpy.props.EnumProperty(
items=(
('UP', 'Move Up', 'Move up'),
('DOWN', 'Move down', 'Move down'),
),
default='UP',
)
propname : bpy.props.StringProperty()
def execute(self, context):
pg = getattr(get_addon_prefs(), self.propname)
uilist = pg.namespaces
index = pg.idx
neighbor = index + (-1 if self.direction == 'UP' else 1)
uilist.move(neighbor, index)
list_length = len(uilist) - 1 # (index starts at 0)
new_index = index + (-1 if self.direction == 'UP' else 1)
list_index = max(0, min(new_index, list_length))
setattr(pg, 'idx', list_index)
redraw_ui()
return {'FINISHED'}
def draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname):
# self.use_filter_show = True # force open/close the search feature
# prefs = get_addon_prefs()
# split = layout.split(align=False, factor=0.3)
row = layout.row()
hide_ico = 'HIDE_ON' if item.hide else 'HIDE_OFF'
source_ico = 'NETWORK_DRIVE' if item.is_project else 'USER' # BLANK1
row.label(text='', icon=source_ico)
row.prop(item, 'hide', text='', icon=hide_ico, invert_checkbox=True)
subrow = row.row(align=True)
subrow.prop(item, 'tag', text='')
subrow.prop(item, 'name', text='')
subrow.enabled = not item.is_project
# row = layout.split(align=False)
# row.label(text=item.prefix)
# row.label(text=item.name)
# if self.show_desc:
# row.label(text=item.description)
# row.operator('sbam.open_online_repo', text='', icon='URL')
class GPTB_UL_namespace_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname)
## Need to duplicate UL as a separate class for suffixes\
## otherwise displayed row in UI are synchronised
class GPTB_UL_namespace_list_suffix(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname)
classes = (
## layer name management
GPTB_OT_reset_project_namespaces,
GPTB_OT_add_namespace_entry,
GPTB_OT_remove_namespace_entry,
GPTB_OT_move_item,
GPTB_UL_namespace_list,
GPTB_UL_namespace_list_suffix,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

152
OP_layer_nav.py Normal file
View File

@ -0,0 +1,152 @@
import bpy
from . import utils
class GPT_OT_layer_nav(bpy.types.Operator):
bl_idname = "gp.layer_nav"
bl_label = "GP Layer Navigator"
bl_description = "Change active GP layer and highlight active for a moment"
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
direction : bpy.props.EnumProperty(
name='direction',
items=(('UP', 'Up', ''),('DOWN', 'Down', ''), ('NONE', 'None', '')),
default='UP',
description='Direction to change layer in active GPencil stack',
options={'SKIP_SAVE'})
## hardcoded values
# interval = 0.04 # 0.1
# limit = 1.8
# fade_val = 0.35
# use_fade_in = True
# fade_in_time = 0.5
def invoke(self, context, event):
## initialise vvalue from prefs
prefs = utils.get_addon_prefs()
if not prefs.nav_use_fade:
if self.direction == 'DOWN':
utils.iterate_active_layer(context.grease_pencil, -1)
# utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'name') # gpv2
if self.direction == 'UP':
utils.iterate_active_layer(context.grease_pencil, 1)
return {'FINISHED'}
## get up and down keys for use in modal
self.up_keys = []
self.down_keys = []
for km in context.window_manager.keyconfigs.user.keymaps:
for kmi in km.keymap_items:
if kmi.idname == 'gp.layer_nav':
if kmi.properties.direction == 'UP':
self.up_keys.append(kmi.type)
elif kmi.properties.direction == 'DOWN':
self.down_keys.append(kmi.type)
self.interval = prefs.nav_interval
self.limit = prefs.nav_limit
self.fade_val = prefs.nav_fade_val
self.use_fade_in = prefs.nav_use_fade_in
self.fade_in_time = prefs.nav_fade_in_time
self.lapse = 0
wm = context.window_manager
args = (self, context)
if context.space_data.overlay.use_gpencil_fade_layers:
self.fade_target = context.space_data.overlay.gpencil_fade_layer
else:
self.fade_target = 1.0
self.fade_start = self.limit - self.fade_in_time
self.first = True
self._timer = wm.event_timer_add(self.interval, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def store_settings(self, context):
self.org_use_gpencil_fade_layers = context.space_data.overlay.use_gpencil_fade_layers
self.org_gpencil_fade_layer = context.space_data.overlay.gpencil_fade_layer
context.space_data.overlay.use_gpencil_fade_layers = True
context.space_data.overlay.gpencil_fade_layer = self.fade_val
def modal(self, context, event):
trigger = False
if event.type in {'RIGHTMOUSE', 'ESC', 'LEFTMOUSE'}:
self.stop_mod(context)
return {'CANCELLED'}
if event.type == 'TIMER':
self.lapse += self.interval
if self.lapse >= self.limit:
self.stop_mod(context)
return {'FINISHED'}
## Fade
if self.use_fade_in and (self.lapse > self.fade_start):
fade = utils.transfer_value(self.lapse, self.fade_start, self.limit, self.fade_val, self.fade_target)
# print(f'lapse {self.lapse} - fade {fade}')
context.space_data.overlay.gpencil_fade_layer = fade
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
_val = utils.iterate_active_layer(context.grease_pencil, -1)
trigger = True
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
_val = utils.iterate_active_layer(context.grease_pencil, 1)
trigger = True
if trigger:
self.direction = 'NONE'
if self.first:
self.store_settings(context)
self.first=False
if self.use_fade_in:
# reset fade to wanted value
context.space_data.overlay.gpencil_fade_layer = self.fade_val
self.lapse = 0 # reset counter
return {'RUNNING_MODAL'}#running modal prevent original usage to be triggered (capture keys)
return {'PASS_THROUGH'}
def stop_mod(self, context):
# restore fade
context.space_data.overlay.use_gpencil_fade_layers = self.org_use_gpencil_fade_layers
context.space_data.overlay.gpencil_fade_layer = self.org_gpencil_fade_layer
wm = context.window_manager
wm.event_timer_remove(self._timer)
addon_keymaps = []
def register_keymap():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS')
kmi.properties.direction = 'UP'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_DOWN', value='PRESS')
kmi.properties.direction = 'DOWN'
addon_keymaps.append((km, kmi))
def unregister_keymap():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
def register():
bpy.utils.register_class(GPT_OT_layer_nav)
register_keymap()
def unregister():
unregister_keymap()
bpy.utils.unregister_class(GPT_OT_layer_nav)

168
OP_layer_picker.py Normal file
View File

@ -0,0 +1,168 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
class GP_OT_pick_closest_layer(Operator):
bl_idname = "gp.pick_closest_layer"
bl_label = "Get Closest Stroke Layer"
bl_description = "Pick closest stroke layer"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
stroke_filter : bpy.props.EnumProperty(name='Target',
default='STROKE',
items=(
('STROKE', 'Stroke', 'Target only Stroke', 0),
('FILL', 'Fill', 'Target only Fill', 1),
('ALL', 'All', 'All stroke types', 2),
),
options={'SKIP_SAVE'})
def filter_stroke(self, context):
kd = mathutils.kdtree.KDTree(len(self.point_pair))
for i, pair in enumerate(self.point_pair):
kd.insert(pair[0], i)
kd.balance()
mouse_vec3 = Vector((*self.init_mouse, 0))
co, index, _dist = kd.find(mouse_vec3)
layer = self.point_pair[index][1]
return layer
def invoke(self, context, event):
self.t0 = time()
self.limit = self.t0 + 0.2 # 200 miliseconds
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
self.idx = None
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def draw(self, context):
layout = self.layout
if context.object.data.layers.active:
layout.label(text=f'Layer: {context.object.data.layers.active.name}')
layout.prop(self, 'stroke_filter')
def modal(self, context, event):
if time() > self.limit:
return {'CANCELLED'}
if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised)
if time() > self.limit:
# dont know if condition is neeed
return {'CANCELLED'}
return self.execute(context)
# return {'FINISHED'}
return {'PASS_THROUGH'}
# return {'RUNNING_MODAL'}
def execute(self, context):
t1 = time()
# self.prefs = get_addon_prefs()
self.ob = context.object
mat = self.ob.matrix_world
gp = self.ob.data
self.inv_mat = self.ob.matrix_world.inverted()
self.point_pair = []
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for layer in gp.layers:
if is_hidden(layer):
continue
for f in layer.frames:
if not f.select:
continue
for s in f.drawing.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
else:
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for layer in gp.layers:
if is_hidden(layer) or not layer.current_frame():
continue
for s in layer.current_frame().drawing.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
if not self.point_pair:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'}
layer_target = self.filter_stroke(context)
if isinstance(layer_target, str):
self.report({'ERROR'}, layer_target)
return {'CANCELLED'}
del self.point_pair # auto garbage collected ?
self.ob.data.layers.active = layer_target
## debug show trigger time
# print(f'Trigger time {time() - self.t0:.3f}')
# print(f'Search time {time() - t1:.3f}')
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.name}')
return {'FINISHED'}
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new(
# name="",
idname="gp.pick_closest_layer",
type="W",
value="PRESS",
)
kmi.properties.stroke_filter = 'STROKE'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new(
# name="",
idname="gp.pick_closest_layer",
type="W",
value="PRESS",
alt = True,
)
kmi.properties.stroke_filter = 'FILL'
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
classes=(
GP_OT_pick_closest_layer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
def unregister():
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -0,0 +1,183 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from . import utils
# def get_layer_list(self, context):
# '''return (identifier, name, description) of enum content'''
# if not context:
# return [('None', 'None','None')]
# if not context.object:
# return [('None', 'None','None')]
# return [(l.name, l.name, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
## in Class
# bl_property = "layers_enum"
# layers_enum : bpy.props.EnumProperty(
# name="Send Material To Layer",
# description="Send active material to layer",
# items=get_layer_list,
# options={'HIDDEN'},
# )
class GPTB_OT_move_material_to_layer(Operator) :
bl_idname = "gp.move_material_to_layer"
bl_label = 'Move Material To Layer'
bl_description = 'Move active material to an existing or new layer'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
layer_name : bpy.props.StringProperty(
name='Layer Name', default='', options={'SKIP_SAVE'})
copy : bpy.props.BoolProperty(
name='Copy to layer', default=False,
description='Copy strokes to layer instead of moving',
options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
if self.layer_name:
return self.execute(context)
if not len(context.object.data.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
mat = context.object.data.materials[context.object.active_material_index]
self.mat_name = mat.name
# wm.invoke_search_popup(self)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# layout.operator_context = "INVOKE_DEFAULT"
layout.prop(self, 'copy', text='Copy Strokes')
action_label = 'Copy' if self.copy else 'Move'
layout.label(text=f'{action_label} material "{self.mat_name}" to layer:', icon='MATERIAL')
col = layout.column()
col.prop(self, 'layer_name', text='', icon='ADD')
# if self.layer_name:
# col.label(text='Ok/Enter to create new layer', icon='INFO')
col.separator()
for l in reversed(context.object.data.layers):
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
row = col.row()
row.alignment = 'LEFT'
op = col.operator('gp.move_material_to_layer', text=l.name, icon=icon, emboss=False)
op.layer_name = l.name
op.copy = self.copy
def execute(self, context):
if not self.layer_name:
print('Out')
return {'CANCELLED'}
## Active + selection
pool = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
if not context.object in pool:
pool.append(context.object)
mat = context.object.data.materials[context.object.active_material_index]
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
# import time
# t = time.time() # Dbg
total = 0
oct = 0
for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
if mat_index is None:
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
continue
gpl = ob.data.layers
if not (target_layer := gpl.get(self.layer_name)):
target_layer = gpl.new(self.layer_name)
## List existing frames
key_dict = {f.frame_number : f for f in target_layer.frames}
### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0
sct = 0
for layer in gpl:
if layer == target_layer:
## ! infinite loop if target layer is included
continue
for fr in layer.frames:
## skip if no stroke has active material
if not next((s for s in fr.drawing.strokes if s.material_index == mat_index), None):
continue
## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(fr.frame_number)):
dest_key = target_layer.frames.new(fr.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {fr.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s_idx, s in enumerate(fr.drawing.strokes):
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s_idx)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
## Remove from source frame (fr)
if not self.copy:
# print('Removing frames') # Dbg
if stroke_to_delete:
fr.drawing.remove_strokes(indices=stroke_to_delete)
## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame
fct += 1
if fct:
oct += 1
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
total += fct
report_type = 'INFO' if total else 'WARNING'
if self.copy:
self.report({report_type}, f'Copied {total} frames accross {oct} object(s)')
else:
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
return {'FINISHED'}
# def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GPENCIL':
# self.layout.operator_context = 'INVOKE_REGION_WIN'
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_move_material_to_layer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

346
OP_material_picker.py Normal file
View File

@ -0,0 +1,346 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from time import time
from .utils import (get_gp_draw_plane,
location_to_region,
region_to_location,
is_locked,
is_hidden)
### passing by 2D projection
def get_3d_coord_on_drawing_plane_from_2d(context, co):
plane_co, plane_no = get_gp_draw_plane()
rv3d = context.region_data
view_mat = rv3d.view_matrix.inverted()
if not plane_no:
plane_no = Vector((0,0,1))
plane_no.rotate(view_mat)
depth_3d = view_mat @ Vector((0, 0, -1000))
org = region_to_location(co, view_mat.to_translation())
view_point = region_to_location(co, depth_3d)
hit = geometry.intersect_line_plane(org, view_point, plane_co, plane_no)
if hit and plane_no:
return context.object, hit, plane_no
return None, None, None
"""
class GP_OT_pick_closest_material(Operator):
bl_idname = "gp.pick_closest_material"
bl_label = "Get Closest Stroke Material"
bl_description = "Pick closest stroke material"
bl_options = {"REGISTER"} # , "UNDO"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def filter_stroke(self, context):
# get stroke under mouse using kdtree
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair):
kd.insert(pair[0], i)
kd.balance()
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
if not hit:
return 'No hit on drawing plane', None
mouse_3d = hit
mouse_local = self.inv_mat @ mouse_3d # local space
co, index, _dist = kd.find(mouse_local) # local space
# co, index, _dist = kd.find(mouse_3d) # world space
# context.scene.cursor.location = co # world space
s = point_pair[index][1]
## find point index in stroke
self.idx = None
for i, p in enumerate(s.points):
if p.position == co:
self.idx = i
break
del point_pair
return s, self.ob.matrix_world @ co
def invoke(self, context, event):
# self.prefs = get_addon_prefs()
self.ob = context.object
self.gp = self.ob.data
self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted()
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in self.gp.layers:
if is_hidden(l):# is_locked(l) or
continue
for f in l.frames:
if not f.select:
continue
for s in f.drawing.strokes:
self.stroke_list.append(s)
else:
# [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in self.gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue
for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s)
if self.fill_only:
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
if not self.stroke_list:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'}
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
self.stroke, self.coord = self.filter_stroke(context)
if isinstance(self.stroke, str):
self.report({'ERROR'}, self.stroke)
return {'CANCELLED'}
del self.stroke_list
if self.idx is None:
self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'}
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
## directly use world position ?
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
self.plen = len(self.stroke.points)
# context.scene.cursor.location = self.coord #Dbg
return self.execute(context)
# context.window_manager.modal_handler_add(self)
# return {'RUNNING_MODAL'}
def execute(self, context):
self.ob.active_material_index = self.stroke.material_index
# self.report({'INFO'}, f'Mat: {self.ob.data.materials[self.stroke.material_index].name}')
return {'FINISHED'}
# def modal(self, context, event):
# if event.type == 'MOUSEMOVE':
# mouse = Vector((event.mouse_region_x, event.mouse_region_y))
# delta = mouse - self.init_mouse
# if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
# print(f'{self.stroke}, points num {len(self.stroke.points)}, material index:{self.stroke.material_index}')
# return {'FINISHED'}
# if event.type in {'RIGHTMOUSE', 'ESC'}:
# # for i, p in enumerate(self.stroke.points): # reset position
# # self.stroke.points[i].position = self.init_pos[i]
# context.area.tag_redraw()
# return {'CANCELLED'}
# return {'RUNNING_MODAL'}
"""
class GP_OT_pick_closest_material(Operator):
bl_idname = "gp.pick_closest_material"
bl_label = "Get Closest Stroke Material"
bl_description = "Pick closest stroke material"
bl_options = {"REGISTER"} # , "UNDO"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
stroke_filter : bpy.props.EnumProperty(default='FILL',
items=(
('FILL', 'Fill', 'Target only Fill materials', 0),
('STROKE', 'Stroke', 'Target only Stroke materials', 1),
('ALL', 'All', 'All material', 2),
),
options={'SKIP_SAVE'})
def filter_stroke(self, context):
# get stroke under mouse using kdtree
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair):
kd.insert(pair[0], i)
kd.balance()
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
if not hit:
return 'No hit on drawing plane', None
mouse_3d = hit
mouse_local = self.inv_mat @ mouse_3d # local space
co, index, _dist = kd.find(mouse_local) # local space
# co, index, _dist = kd.find(mouse_3d) # world space
# context.scene.cursor.location = co # world space
s = point_pair[index][1]
## find point index in stroke
self.idx = None
for i, p in enumerate(s.points):
if p.position == co:
self.idx = i
break
del point_pair
return s, self.ob.matrix_world @ co
def invoke(self, context, event):
self.t0 = time()
self.limit = self.t0 + 0.2 # 200 miliseconds
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
self.idx = None
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if time() > self.limit:
return {'CANCELLED'}
if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised)
if time() > self.limit:
# dont know if condition is neeed
return {'CANCELLED'}
return self.execute(context)
# return {'FINISHED'}
return {'PASS_THROUGH'}
# return {'RUNNING_MODAL'}
def execute(self, context):
# self.prefs = get_addon_prefs()
self.ob = context.object
gp = self.ob.data
self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted()
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in gp.layers:
if is_hidden(l):# is_locked(l) or
continue
for f in l.frames:
if not f.select:
continue
for s in f.drawing.strokes:
self.stroke_list.append(s)
else:
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue
for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s)
if self.stroke_filter == 'FILL':
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
elif self.stroke_filter == 'STROKE':
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_stroke]
# else ALL (no filter)
if not self.stroke_list:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'}
stroke, self.coord = self.filter_stroke(context)
if isinstance(stroke, str):
self.report({'ERROR'}, stroke)
return {'CANCELLED'}
del self.stroke_list
if self.idx is None:
self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'}
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated
# self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
# self.plen = len(stroke.points)
self.ob.active_material_index = stroke.material_index
## debug show trigger time
# print(f'Trigger time {time() - self.t0:.3f}')
self.report({'INFO'}, f'Mat: {self.ob.data.materials[stroke.material_index].name}')
return {'FINISHED'}
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new(
# name="",
idname="gp.pick_closest_material",
type="S", # type="LEFTMOUSE",
value="PRESS",
# key_modifier='S', # S like Sample
)
kmi.properties.stroke_filter = 'FILL'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new(
# name="",
idname="gp.pick_closest_material",
type="S", # type="LEFTMOUSE",
value="PRESS",
alt = True,
# key_modifier='S', # S like Sample
)
kmi.properties.stroke_filter = 'STROKE'
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
classes=(
GP_OT_pick_closest_material,
)
def register():
if bpy.app.background:
return
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
def unregister():
if bpy.app.background:
return
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -5,7 +5,6 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from .utils import convert_attr, get_addon_prefs
### --- Json serialized material load/save
def load_palette(context, filepath):
@ -43,7 +42,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# Start Clean (delete unuesed sh*t)
@ -83,7 +82,7 @@ class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.json'
@ -111,7 +110,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
@ -130,6 +129,8 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
dic = {}
allmat=[]
for mat in ob.data.materials:
if not mat:
continue
if not mat.is_grease_pencil:
continue
if mat in allmat:
@ -165,12 +166,10 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
def load_blend_palette(context, filepath):
'''Load materials on current active object from current chosen blend'''
#from pathlib import Path
#palette_fp = C.preferences.addons['GP_toolbox'].preferences['palette_path']
#fp = Path(palette_fp) / 'christina.blend'
print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects:
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
print(f'{ob.name} not a GP object')
continue
@ -225,7 +224,7 @@ class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.blend'
@ -253,7 +252,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
ob = context.object
@ -261,7 +260,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
self.report({'ERROR'}, 'No materials to transfer')
return {"CANCELLED"}
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
if not selection:
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
@ -314,7 +313,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
self.ob = context.object
@ -355,7 +354,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
import re
diff_ct = 0
todel = []
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
return
if not hasattr(ob, 'material_slots'):
return
@ -411,7 +410,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
# 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:
for s in f.drawing.strokes:
if s.material_index == i:
s.material_index = new_mat_id
@ -428,7 +427,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
# if self.skip_binded_empty_slots:
# for l in ob.data.layers:
# for f in l.frames:
# for s in f.strokes:
# for s in f.drawing.strokes:
# if s.material_index == i:
# is_binded = True
# break
@ -460,7 +459,6 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
return {"FINISHED"}
classes = (
GPTB_OT_load_palette,
GPTB_OT_save_palette,
@ -468,6 +466,7 @@ GPTB_OT_load_default_palette,
GPTB_OT_load_blend_palette,
GPTB_OT_copy_active_to_selected_palette,
GPTB_OT_clean_material_stack,
)
def register():

581
OP_palettes_linker.py Normal file
View File

@ -0,0 +1,581 @@
import bpy
import re
import json
import os
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from . import utils
# from . import blendfile
from bpy.types import (
Panel,
Operator,
PropertyGroup,
UIList,
)
from bpy.props import (
IntProperty,
BoolProperty,
StringProperty,
FloatProperty,
EnumProperty,
PointerProperty,
)
#--- OPERATORS
def print_materials_sources(ob):
for m in ob.data.materials:
if m.library:
print(f'{m.name} - {Path(m.library.filepath).name}')
else:
print(m.name)
def replace_mat_slots(src_mat, obj):
for ms in obj.material_slots:
if ms.material.name == src_mat.name:
# Only on different linked, else mat.name differ (.001))
ms.material = src_mat
class GPTB_OT_import_obj_palette(Operator):
bl_idname = "gp.import_obj_palette"
bl_label = "Import Object Palette"
bl_description = "Import object palette from blend"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
## get targets
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
if not selection:
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
return {"CANCELLED"}
prefs = utils.get_addon_prefs()
exclusions = [name.strip() for name in prefs.mat_link_exclude.split(',')] if prefs.mat_link_exclude else []
# Avoid looping on linked duplicate
objs = []
datas = []
for o in selection:
if o.data in datas:
continue
objs.append(o)
datas.append(o.data)
del datas # datas.clear()
pl_prop = context.scene.bl_palettes_props
blend_path = pl_prop.blends[pl_prop.bl_idx].blend_path
target_objs = [pl_prop.objects[pl_prop.ob_idx].name]
# Future improvement
# target_objs = [o.name for o in pl_prop.objects if o.select]
if not target_objs:
self.report({'ERROR'}, 'Need at least one palette source selected')
return {"CANCELLED"}
mode = pl_prop.import_type
if mode == 'LINK' and not bpy.data.is_saved: # autorise for absolute path
self.report({'ERROR'}, 'Blend file must be saved to use link mode')
return {"CANCELLED"}
if mode != 'LINK':
self.report({'ERROR'}, 'Not supported yet, use link')
return {'CANCELLED'}
if not Path(blend_path).exists():
utils.show_message_box([['gp.palettes_reload_blends', 'Invalid blend path! Click here to refresh source blends', 'FILE_REFRESH']], 'Invalid Palette', 'ERROR')
return {'CANCELLED'}
# get relative path
blend_path = bpy.path.relpath(blend_path)
# TODO append object to list all material that belongs to it...
linked_objs = utils.link_objects_in_blend(blend_path, target_objs, link=True)
if not linked_objs:
self.report({'ERROR'}, f'Could not link/append obj from {blend_path}')
return {"CANCELLED"}
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
if linked_objs[i].type != 'GREASEPENCIL':
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
bpy.data.objects.remove(linked_objs.pop(i))
if not linked_objs:
self.report({'ERROR'}, f'Linked object was not a Grease Pencil')
return {"CANCELLED"}
print('blend_path: ', blend_path)
# if materials have been renamed, there must be already be appended / linked
# to_clear = []
ct = 0
for src_ob in linked_objs:
ct += len(src_ob.data.materials)
if mode == 'LINK': # link new mats and update already linked ones
## link mats
for ob in objs:
for src_ob in linked_objs:
for src_mat in src_ob.data.materials:
## filter mat
if src_mat.name in exclusions:
continue
mat = ob.data.materials.get(src_mat.name)
if mat and mat.library == src_mat.library:
# print('already exists')
continue # same material, skip
elif mat:
# print('already but not same lib')
## same material but not from same lib
## remap_user will replace this mat in all objects blend...
mat.user_remap(src_mat)
## (we might want to keep links in other objects untouched ?)
## else use a basic material slot swap (loop, can be added on multiple slots)
# replace_mat_slots(ob, src_mat)
else:
# print('Not in dest')
## material not in dest, append
ob.data.materials.append(src_mat)
elif mode == 'APPEND':
## append, overwrite all already existing materials with new ones
pass
# ct = 0
# for o in selection:
# for mat in ob.data.materials:
# if mat in o.data.materials[:]:
# continue
# o.data.materials.append(mat)
# ct += 1
elif mode == 'APPEND_REUSE':
## append, Skip existing material
pass
if ct:
self.report({'INFO'}, f'{ct} Materials appended')
# else:
# self.report({'WARNING'}, 'All materials are already in other selected object')
# unlink objects and their gp data
for src_ob in linked_objs:
bpy.data.grease_pencils.remove(src_ob.data)
return {"FINISHED"}
class GPTB_OT_palette_fuzzy_search_obj(Operator):
bl_idname = "gptb.palette_fuzzy_search_obj"
bl_label = "Palette Fuzzy Match"
bl_description = "Try to find a palette with name closest to active object"
bl_options = {"REGISTER"}
def execute(self, context):
if not context.object:
self.report({'ERROR'}, 'No active object to search name from')
return {"CANCELLED"}
bl_props = context.scene.bl_palettes_props
final_ratio = 0
new_idx = None
for i, o in enumerate(bl_props.objects):
ratio = utils.fuzzy_match_ratio(context.object.name, o.name, case_sensitive=False)
if ratio > final_ratio:
new_idx = i
final_ratio = ratio
limit = 0.3
if final_ratio < limit:
self.report({'ERROR'}, f'Could not find a name matching at least {limit*100:.0f}% "{context.object.name}"')
return {"CANCELLED"}
if new_idx is None:
self.report({'ERROR'}, f'Could not find match')
return {"CANCELLED"}
bl_props.ob_idx = new_idx
self.report({'INFO'}, f'Select {bl_props.objects[bl_props.ob_idx].name} (match at {final_ratio*100:.1f}% with "{context.object.name}")')
return {"FINISHED"}
## Unused for now, all libs are linked to one library data. need to replace material links one by one.
class GPTB_OT_palette_version_update(Operator):
bl_idname = "gptb.palette_version_update"
bl_label = "Update Palette Version"
bl_description = "Update linked material to selected palette version if curent link has same basename"
bl_options = {"REGISTER"}
mat_scope : EnumProperty(
name='Targeted Materials',
items=(('ALL', "All Materials", "Update all linked material in file to next version"),
('SELECTED', "Selected Objects", "Update all linked material on selected gp objects"),
),
default='ALL',
description='Choose material targeted for library update'
)
mat_type : EnumProperty(
name='Materials Type',
items=(('ALL', "All Materials", "Update both gp and obj materials"),
('GP', "Gpencil Materials", "update only grease pencil materials"),
('OBJ', "Non-Gpencil Materials", "update only non-gpencil objects materials"),
),
default='GP',
description='Filter material type for library update'
)
def invoke(self, context, event):
self.bl_props = context.scene.bl_palettes_props
if not self.bl_props.blends or not self.bl_props.blends[0].blend_path:
self.report({'ERROR'}, 'No blend selected')
return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self, width=450)
def draw(self, context):
layout = self.layout
layout.label(text=f'Update links path to palette: {self.bl_props.blends[self.bl_props.bl_idx].blend_name}', icon='LINK_BLEND')
self.bl_props
layout.prop(self, 'mat_scope')
layout.prop(self, 'mat_type')
col = layout.column(align=True)
col.label(text='Does not check if material exists in target blend', icon='INFO')
col.label(text='Just change source filepath if different version of same source name is found')
# col.label(text='version of same source name is found')
def execute(self, context):
if self.mat_scope == 'SELECTED' and not context.selected_objects:
self.report({'ERROR'}, 'No selected objects')
return {"CANCELLED"}
bl_props = context.scene.bl_palettes_props
bl = bl_props.blends[bl_props.bl_idx]
bl_name, bl_path = bl.blend_name, bl.blend_path
if not Path(bl_path).exists():
self.report({'ERROR'}, f'Current selected blend source seem unreachable, try to refresh\ninvalid path: {bl_path}')
return {"CANCELLED"}
reversion = re.compile(r'\d{2,4}$') # version padding from 2 to 4
bl_relpath = bpy.path.relpath(bl_path)
if self.mat_scope == 'SELECTED':
pool = []
for o in context.selected_objects:
for m in o.data.materials:
pool.append(m)
elif self.mat_scope == 'ALL':
pool = [m for m in bpy.data.materials]
ct = 0
for m in pool:
if not m.library:
continue
if self.mat_type == 'GP' and not m.is_grease_pencil:
continue
if self.mat_type == 'OBJ' and m.is_grease_pencil:
continue
cur_fp = m.library.filepath
if not cur_fp:
print(f'! {m.name} has an empty library filepath !')
continue
p_cur_fp = Path(cur_fp)
if p_cur_fp.stem == bl_name:
continue # already good
if reversion.sub('', p_cur_fp.stem) != reversion.sub('', bl_name):
continue # not same stem base
# Same stem without version, can update to this one
print(f'{m.name}: {p_cur_fp} >> {bl_relpath}')
ct += 1
m.library.filepath = bl_relpath
if ct:
self.report({'INFO'}, f'{ct} material link path updated')
else:
self.report({'WARNING'}, 'No material path updated')
return {"FINISHED"}
#--- UI LIST
class GPTB_UL_blend_list(UIList):
# order_by_distance : BoolProperty(default=True)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
layout.label(text=item.blend_name)
def draw_filter(self, context, layout):
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use * as wildcard)
# reverse order
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
def filter_items(self, context, data, propname):
# example : https://docs.blender.org/api/blender_python_api_current/bpy.types.UIList.html
# This function gets the collection property (as the usual tuple (data, propname)), and must return two lists:
# * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the
# matching item as filtered (i.e. to be shown), and 31 other bits are free for custom needs. Here we use the
# * The second one is for reordering, it must return a list containing the new indices of the items (which
# gives us a mapping org_idx -> new_idx).
# Please note that the default UI_UL_list defines helper functions for common tasks (see its doc for more info).
# If you do not make filtering and/or ordering, return empty list(s) (this will be more efficient than
# returning full lists doing nothing!).
collec = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
# Default return values.
flt_flags = []
flt_neworder = []
# Filtering by name #not working damn !
if self.filter_name:
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
reverse=self.use_filter_sort_reverse)#self.use_filter_name_reverse)
return flt_flags, flt_neworder
class GPTB_UL_object_list(UIList):
# order_by_distance : BoolProperty(default=True)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
self.use_filter_show = True # force open the search feature
layout.label(text=item.name)
def draw_filter(self, context, layout):
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use * as wildcard)
# reverse order
subrow.operator('gptb.palette_fuzzy_search_obj', text='', icon='ZOOM_SELECTED') # built-in reverse
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
def filter_items(self, context, data, propname):
collec = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
# Default return values.
flt_flags = []
flt_neworder = []
if self.filter_name:
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
reverse=self.use_filter_sort_reverse)
return flt_flags, flt_neworder
def reload_blends(self, context):
scn = context.scene
pl_prop = scn.bl_palettes_props
uilist = scn.bl_palettes_props.blends
uilist.clear()
pl_prop['bl_idx'] = 0
prefs = utils.get_addon_prefs()
if pl_prop.use_project_path:
palette_fp = prefs.palette_path
else:
palette_fp = pl_prop.custom_dir
if not palette_fp: # singular
item = uilist.add()
item.blend_name = 'No Palette Path Specified'
reload_objects(self, context)
return
palettes_dir = Path(os.path.abspath(bpy.path.abspath(palette_fp)))
if not palettes_dir.exists():
item = uilist.add()
item.blend_name = 'Palette Path not found'
reload_objects(self, context)
return
# list blends
pattern = r'[vV](\d{2,3})' # rightest = r'[vV](\d+)(?!.*[vV]\d)'
blends = [] # recursive
for root, _dirs, files in os.walk(palettes_dir):
for f in files:
fp = Path(root) / f
if not f.endswith('.blend'):
continue
if not re.search(pattern, f):
continue
if not fp.is_file():
continue
blends.append((str(fp), fp.stem, ""))
## only in palette folder.
# blends = [(o.path, Path(o).stem, "") for o in os.scandir(palettes_dir)
# if o.is_file()
# and o.name.endswith('.blend')
# and re.search(pattern, o.name)]
# blends.sort(key=lambda x: x[1], reverse=False) # sort alphabetically
blends.sort(key=lambda x: int(re.search(pattern, x[1]).group(1)), reverse=False) # sort by version
# print('blends found', len(blends))
for bl in blends: # populate list
item = uilist.add()
scn.bl_palettes_props['bl_idx'] = len(uilist) - 1 # don't trigger updates
item.blend_path = bl[0]
item.blend_name = bl[1]
scn.bl_palettes_props.bl_idx = len(uilist) - 1 # trigger update ()
# reload_objects(self, context) # triggered by above assignation
# return len(blends) # return value must be None
class GPTB_OT_palettes_reload_blends(Operator):
bl_idname = "gp.palettes_reload_blends"
bl_label = "Reload Palette Blends"
bl_description = "Reload the blends in UI list of palettes linker"
bl_options = {"REGISTER"} # , "INTERNAL"
def execute(self, context):
reload_blends(self, context)
# ret = reload_blends(self, context)
# if ret is None:
# self.report({'ERROR'}, 'No blend scanned, check palette path')
# else:
# self.report({'INFO'}, f'{ret} blends found')
return {"FINISHED"}
def reload_objects(self, context):
scn = context.scene
prefs = utils.get_addon_prefs()
pal_prop = scn.bl_palettes_props
blend_uil = pal_prop.blends
obj_uil = pal_prop.objects
obj_uil.clear()
pal_prop['ob_idx'] = 0
file_libs = [l.filepath for l in bpy.data.libraries if l.filepath]
if not len(blend_uil) or (len(blend_uil) == 1 and not bool(blend_uil[0].blend_path)):
item = obj_uil.add()
item.name = 'No blend to list object'
return
if not blend_uil[pal_prop.bl_idx].blend_path:
item = obj_uil.add()
item.name = 'Selected blend has no path'
return
path_to_blend = Path(blend_uil[pal_prop.bl_idx].blend_path)
## get list of string of all object except camera
ob_list = utils.check_objects_in_blend(str(path_to_blend), avoid_camera=True)
ob_list.sort(reverse=False) # filter object by name
for ob_name in ob_list: # populate list
item = obj_uil.add()
item.name = ob_name
# print('path_to_blend: ', path_to_blend)
item.path = str(path_to_blend / 'Object' / ob_name)
pal_prop.ob_idx = len(obj_uil) - 1
## those temp libraries are not saved (auto-cleared)
## But best to keep library list tidy while file is opened
for lib in reversed(bpy.data.libraries):
if lib.filepath and not lib.users_id:
if lib.filepath not in file_libs:
bpy.data.libraries.remove(lib)
# return len(ob_list) # must return None if used in update
del ob_list
#--- PROPERTIES
class GPTB_PG_blend_prop(PropertyGroup):
blend_name : StringProperty() # stem of the path
blend_path : StringProperty() # full path
class GPTB_PG_object_prop(PropertyGroup):
name : StringProperty() # stem of the path
path : StringProperty() # Object / Material ?
## select feature to get multiple at once
# select : BoolProperty(default=False) # Object / Material ?
class GPTB_PG_palette_settings(PropertyGroup):
bl_idx : IntProperty(update=reload_objects) # update_on_index_change to reload object
blends : bpy.props.CollectionProperty(type=GPTB_PG_blend_prop)
ob_idx : IntProperty()
objects : bpy.props.CollectionProperty(type=GPTB_PG_object_prop)
use_project_path : BoolProperty(name='Use Project Palettes',
default=True, description='Use palettes directory specified in gp toolbox addon preferences',
update=reload_blends)
show_path : BoolProperty(name='Show path',
default=True, description='Show Palette directoty filepath')
custom_dir : StringProperty(name='Custom Palettes Directory', subtype='DIR_PATH',
description='Use choosen directory to load blend palettes',
update=reload_blends)
import_type : EnumProperty(
name="Import Type", description="Choose inmport type: link, append, append reuse (keep existing materials)",
default='LINK', options={'ANIMATABLE'}, update=None, get=None, set=None,
items=(
('LINK', 'Link', 'Link materials to selected object', 0),
('APPEND', 'Append', 'Append materials to selected objects', 1),
('APPEND_REUSE', 'Append (Reuse)', 'Append materials to selected objects\nkeep those already there', 2),
)
)
# fav_blend: StringProperty() ## mark a blend as prefered ? (need to be stored in prefereneces to restore in other blend...)
classes = (
# blend list
GPTB_PG_blend_prop,
GPTB_UL_blend_list,
GPTB_OT_palettes_reload_blends,
# object in blend list
GPTB_OT_palette_fuzzy_search_obj,
GPTB_PG_object_prop,
GPTB_UL_object_list,
# prop containing two above
GPTB_PG_palette_settings,
GPTB_OT_import_obj_palette,
# GPTB_OT_palette_version_update,
# TEST_OT_import_obj_palette_test,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.bl_palettes_props = bpy.props.PointerProperty(type=GPTB_PG_palette_settings)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.bl_palettes_props

View File

@ -40,6 +40,8 @@ exclude = (
'audio_bitrate',
]
"""
'''
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
@ -80,11 +82,51 @@ def render_with_restore():
return RenderFileRestorer()
'''
class render_with_restore:
def __init__(self):
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
self.zones = [rd, ff, im]
self.val_dic = {}
self.cam = bpy.context.scene.camera
def __enter__(self):
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
if self.cam and self.cam.name == 'draw_cam':
if self.cam.parent:
bpy.context.scene.camera = self.cam.parent
def __exit__(self, type, value, traceback):
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.items():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
if self.cam:
bpy.context.scene.camera = self.cam
def playblast(viewport = False, stamping = True):
scn = bpy.context.scene
res_factor = scn.gptoolprops.resolution_percentage
playblast_path = get_addon_prefs().playblast_path
rd = scn.render
ff = rd.ffmpeg
with render_with_restore():
@ -116,13 +158,18 @@ def playblast(viewport = False, stamping = True):
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
blend = Path(bpy.data.filepath)
date_format = "%Y-%m-%d_%H-%M-%S"
fp = join(blend.parent, "images", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
## old direct place
# fp = join(blend.parent, "playblast", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'playblast_{blend.stem}_{strftime(date_format)}.mp4'
fp = str(fp)
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
rd.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
rd.stamp_font_size = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
# bpy.ops.render.render_wrap(use_view=viewport)
### render

View File

@ -22,13 +22,18 @@ exclude = (
)
def delete_file(filepath):
fp = Path(filepath)
if fp.exists() and fp.is_file():
try:
if os.path.isfile(filepath) :
print('removing', filepath)
os.remove(filepath)
print('removing', fp)
fp.unlink(missing_ok=False)
# os.remove(fp)
return True
except PermissionError:
print(f'impossible to remove {filepath}')
print(f'impossible to remove (permission error): {fp}')
return False
except FileNotFoundError:
print(f'Impossible to remove (file not found error): {fp}')
return False
# render function
@ -58,7 +63,7 @@ def render_function(cmd, total_frame, scene) :
frame_count += 1
try :
# print('frame_count: ', frame_count, 'total_frame: ', total_frame)
bpy.context.window_manager.pblast_completion = frame_count / total_frame * 100
bpy.context.window_manager.pblast_completion = int(frame_count / total_frame * 100)
except AttributeError :
#debug
if debug : print("AttributeError avoided")
@ -231,9 +236,7 @@ class BGBLAST_OT_playblast_modal_check(bpy.types.Operator):
self.report({'INFO'}, "Render Finished")
### classic sauce
"""
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
@ -266,6 +269,38 @@ def render_with_restore():
print(e)
return RenderFileRestorer()
"""
class render_with_restore:
def __init__(self):
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
self.zones = [rd, ff, im]
self.val_dic = {}
def __enter__(self):
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
def __exit__(self, type, value, traceback):
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.items():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
def playblast(context, viewport = False, stamping = True):
@ -274,6 +309,7 @@ def playblast(context, viewport = False, stamping = True):
rd = scn.render
ff = rd.ffmpeg
playblast_path = get_addon_prefs().playblast_path
prefix = 'tempblast_'
# delete old playblast and blend files
@ -316,16 +352,20 @@ def playblast(context, viewport = False, stamping = True):
# rd.is_movie_format# check if its movie mode
## set filepath
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
blend = Path(bpy.data.filepath)
# mode incremental or just use fulldate (cannot create conflict and filter OK but long name)
date_format = "%Y-%m-%d_%H-%M-%S"
fp = join(blend.parent, "playblast", f'{prefix}{blend.stem}_{strftime(date_format)}.mp4')
## old
blend = Path(bpy.data.filepath)
# fp = blend.parent / "playblast" / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4'
## with path variable
fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4'
fp = str(fp)
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
rd.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
rd.stamp_font_size = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
# get total number of frames

View File

@ -1,8 +1,6 @@
from .utils import get_gp_objects, get_gp_datas, get_addon_prefs
import bpy
from .utils import get_gp_datas, get_addon_prefs, translate_range
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
def get_hue_by_name(name, offset=0):
'''
@ -76,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
# namespace_order
namespaces=[]
for l in gpl:
ns= l.info.lower().split(separator, 1)[0]
ns= l.name.lower().split(separator, 1)[0]
if ns not in namespaces:
namespaces.append(ns)
@ -90,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
### step from 0.1 to 0.9
for i, l in enumerate(gpl):
if l.info.lower() not in ('background',):
if l.name.lower() not in ('background',):
print()
print('>', l.info)
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
print('>', l.name)
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
print("namespace", ns)#Dbg
if context.scene.gptoolprops.autotint_namespace:
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
h = get_hue_by_name(ns, hue_offset)#l.name == individuels
else:
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
@ -127,3 +125,10 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
def invoke(self, context, event):
self.autotint_offset = context.scene.gptoolprops.autotint_offset
return self.execute(context)
def register():
bpy.utils.register_class(GPT_OT_auto_tint_gp_layers)
def unregister():
bpy.utils.unregister_class(GPT_OT_auto_tint_gp_layers)

View File

@ -1,10 +1,13 @@
import bpy
import mathutils
import numpy as np
from mathutils import Matrix, Vector
from math import pi
import numpy as np
from time import time
from mathutils.geometry import intersect_line_plane
from . import utils
from .utils import is_hidden, is_locked
def get_scale_matrix(scale):
# recreate a neutral mat scale
@ -16,46 +19,74 @@ def get_scale_matrix(scale):
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method
:all_stroke: affect hided, locked layers
:all_stroke: affect hidden, 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
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
frame_list = list(set(frame_list))
frame_list.sort()
for l in obj.data.layers:
for f in l.frames:
if not len(f.strokes):
scn = bpy.context.scene
for i in frame_list:
scn.frame_set(i) # refresh scene
# scn.frame_current = i # no refresh
origin = scn.camera.matrix_world.to_translation()
matrix_inv = obj.matrix_world.inverted()
# origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
# matrix = np.array(obj.matrix_world, dtype='float64')
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
#mat = src.matrix_world
for layer in obj.data.layers:
if not all_strokes:
if not layer.select:
continue
if is_hidden(layer) or is_locked(layer):
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')
frame = next((f for f in layer.frames if f.frame_number == i), None)
if frame is None:
print(layer.name, 'Not found')
# FIXME: some strokes are ignored
# print(frame'skip {layer.name}, no frame at {i}')
continue
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
for s in frame.drawing.strokes:
# print(layer.name, s.material_index)
bpy.ops.object.mode_set(mode=omode)
## Batch matrix apply (Here is slower than list comprehension).
# nb_points = len(s.points)
# coords = np.empty(nb_points * 3, dtype='float64')
# s.points.foreach_get('co', coords)
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
## list comprehension method
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
# Basic method (Slower than foreach_set and compatible with GPv3)
## TODO: use low level api with curve offsets...
for pt_index, point in enumerate(s.points):
point.position = matrix_inv @ new_world_co_3d[pt_index]
## GPv2: ravel and use foreach_set
## Ravel new coordinate on the fly
## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
# s.points.foreach_set('co', new_local_coords)
if restore_frame:
bpy.context.scene.frame_current = oframe
## Update the layer and redraw all viewports
obj.data.layers.update()
utils.refresh_areas()
def align_global(reproject=True, ref=None, all_strokes=True):
@ -87,24 +118,24 @@ def align_global(reproject=True, ref=None, all_strokes=True):
# world_coords = []
for l in o.data.layers:
for f in l.frames:
for s in f.strokes:
for s in f.drawing.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()
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
# for p in s.points:
## GPv2
# 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
# world_co = mat @ p.position
# p.position = new_mat.inverted() @ world_co
## GOOD :
# p.co = p.co @ mat.inverted() @ new_mat
p.position = p.position @ mat.inverted() @ new_mat
if o.parent:
o.matrix_world = new_mat
@ -127,7 +158,7 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
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]
rot_keys = [int(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:
@ -164,9 +195,9 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat
for s in f.strokes:
for s in f.drawing.strokes:
## foreach
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
coords = [p.position @ 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])
@ -215,7 +246,7 @@ class GPTB_OT_realign(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
reproject : bpy.props.BoolProperty(
name='Reproject', default=True,
@ -231,8 +262,8 @@ class GPTB_OT_realign(bpy.types.Operator):
## 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')
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
self.report({'ERROR'}, 'Does not work in Multiframe mode')
return {"CANCELLED"}
self.alert = ''
@ -313,24 +344,25 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
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", ""),
items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""),
('SIDE', "Side", ""),
('TOP', "Top", ""),
('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
),
default='FRONT')
default='CURRENT')
def invoke(self, context, event):
if context.object.data.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
self.report({'ERROR'}, 'Does not work in Multi-edit')
return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self)
@ -338,15 +370,36 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
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.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera', icon='ERROR')
layout.prop(self, "all_strokes")
layout.prop(self, "type")
layout.prop(self, "type", text='Project Axis')
## Hint show axis
if self.type == 'CURRENT':
## Show as prop
# row = layout.row()
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
# row.enabled = False
orient = {
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
}
box = layout.box()
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context):
t0 = time()
orient = self.type
if self.type == 'CURRENT':
orient = None
batch_reproject(context.object, proj_type=self.type, all_strokes=self.all_strokes, restore_frame=True)
batch_reproject(context.object, proj_type=orient, all_strokes=self.all_strokes, restore_frame=True)
self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
@ -355,12 +408,14 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
### -- 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
if context.mode == 'EDIT_GREASE_PENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
self.layout.operator('gp.batch_reproject_all_frames', icon='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
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
classes = (
GPTB_OT_realign,
@ -371,12 +426,12 @@ 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)
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
bpy.types.VIEW3D_MT_edit_greasepencil_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)
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)

View File

@ -1,482 +0,0 @@
import bpy
import os
from os import listdir, scandir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from pathlib import Path
from time import strftime
C = bpy.context
D = bpy.data
from .utils import open_file, open_folder, get_addon_prefs
### render the png sequences
def initial_render_checks(context=None):
if not context:
context=bpy.context
if not bpy.data.is_saved:
return "File is not saved, render cancelled"
cam = context.scene.camera
if not cam:
return "No active Camera"
if cam.name == 'draw_cam':
if not cam.parent:
return "Camera is draw_cam but has no parent cam to render from..."
context.scene.camera = cam.parent
if cam.name == 'obj_cam':
if not cam.get('maincam_name'):
return "Cannot found main camera from obj_cam. Set main camera manually"
main_cam = context.scene.objects.get(cam['maincam_name'])
if not main_cam:
return f"Main camera not found with name: {cam['main_cam']}"
context.scene.camera = main_cam
return
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
)
"""
rd_keep = [
"resolution_percentage",
"resolution_x",
"resolution_y",
"filepath",
"use_stamp",
"stamp_font_size",
]
im_keep = [
'file_format',
'color_mode',
'quality',
'compression',
]
ff_keep = [
'codec',
'format',
'constant_rate_factor',
'ffmpeg_preset',
'gopsize',
'audio_codec',
'audio_bitrate',
]
"""
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
zones = [rd, ff, im]
obviz = {}
# layviz = []
# matviz = []
closeline = False
val_dic = {}
cam = bpy.context.scene.camera
enter_context = None
def __enter__(self):
self.enter_context = bpy.context
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
# cam
if self.cam and self.cam.name == 'draw_cam':
if self.cam.parent:
bpy.context.scene.camera = self.cam.parent
#case of obj cam
if self.cam.name == 'obj_cam':
bpy.context.scene.camera = bpy.context.scene.objects.get(self.cam['main_cam'])
for ob in bpy.context.scene.objects:
self.obviz[ob.name] = ob.hide_render
close_mat = bpy.data.materials.get('closeline')
if close_mat and not close_mat.grease_pencil.hide:
close_mat.grease_pencil.hide = True
self.closeline = True
# for gpo in bpy.context.scene.objects:
# if gpo.type != 'GPENCIL':
# continue
# if not gpo.materials.get('closeline'):
# continue
# self.closelines[gpo] = gpo.materials['closeline'].hide_render
def __exit__(self, type, value, traceback):
## reset header text
# self.enter_context.area.header_text_set(None)
### maybe keep render settings for custom output with right mode
"""
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.ietms():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
"""
if self.cam:
bpy.context.scene.camera = self.cam
for obname, val in self.obviz.items():
bpy.context.scene.objects[obname].hide_render = val
if self.closeline:
close_mat = bpy.data.materials.get('closeline')
if close_mat:
close_mat.grease_pencil.hide = False
return RenderFileRestorer()
def set_render_settings():
prefs = get_addon_prefs()
rd = bpy.context.scene.render
rd.use_sequencer = False
rd.use_compositing = False
rd.use_overwrite = True
rd.image_settings.file_format = 'PNG'
rd.image_settings.color_mode = 'RGBA'
rd.image_settings.color_depth = '16'
rd.image_settings.compression = 80 #maybe up the compression a bit...
rd.resolution_percentage = 100
rd.resolution_x, rd.resolution_y = prefs.render_res_x, prefs.render_res_y
rd.use_stamp = False
rd.film_transparent = True
def render_invididually(context, render_list):
'''Receive a list of object to render individually isolated'''
prefs = get_addon_prefs()
scn = context.scene
rd = scn.render
error_list = []
with render_with_restore():
set_render_settings()
# rd.filepath = join(dirname(bpy.data.filepath), basename(bpy.data.filepath))
# rd.frame_path(frame=0, preview=0, view="_sauce")## give absolute render filepath with some suffix
## set filepath
blend = Path(bpy.data.filepath)
### render by object in list
for obname in render_list:
the_obj = scn.objects.get(obname)
if not the_obj:
error_list.append(f'! Could not found {obname} in scene, skipped !')
continue
## Kill renderability of all
for o in scn.objects:
o.hide_render = True
the_obj.hide_render = False
# f'{blend.stem}_'
# fp = blend.parents[1] / "compo" / "base" / obname / (obname+'_')
fp = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve() / obname / (obname+'_')
rd.filepath = str(fp)
# Freeze so impossible to display advance
# context.area.header_text_set(f'rendering > {obname} ...')
### render
# bpy.ops.render.render_wrap(use_view=viewport)
bpy.ops.render.render(animation=True)
# print("render Done :", fp)#Dbg
return error_list
def render_grouped(context, render_list):
'''Receive a list of object to render grouped'''
scn = context.scene
rd = scn.render
error_list = []
with render_with_restore():
set_render_settings()
## Kill renderability of all
for o in scn.objects:
o.hide_render = True
### show all object of the list
for obname in render_list:
the_obj = scn.objects.get(obname)
if not the_obj:
error_list.append(f'! Could not found {obname} in scene, skipped !')
continue
the_obj.hide_render = False
## Use current file path of setup output path else following :
blend = Path(bpy.data.filepath)
outname = context.scene.gptoolprops.name_for_current_render
# fp = blend.parents[1] / "compo" / "base" / outname / (outname+'_')
fp = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve() / outname / (outname+'_')
rd.filepath = str(fp)
### render
# bpy.ops.render.render_wrap(use_view=viewport)
bpy.ops.render.render(animation=True)
# print("render Done :", fp)#Dbg
return error_list
class GPTRD_OT_render_anim(bpy.types.Operator):
bl_idname = "render.render_anim"
bl_label = "render anim"
bl_description = "Launch animation render"
bl_options = {"REGISTER"}
# use_view : bpy.props.BoolProperty(name='use_view', default=False)
to_render = []
mode : bpy.props.StringProperty(name="render mode",
description="change render mode for list rendering", default="INDIVIDUAL")
render_bool : bpy.props.BoolVectorProperty(name="render bools",
description="", default=tuple([True]*32), size=32, subtype='NONE')
def invoke(self, context, event):
# prefs = get_addons_prefs_and_set()
# if not prefs.local_folder:
# self.report({'ERROR'}, f'Project local folder is not specified in addon preferences')
# return {'CANCELLED'}
if self.mode == 'GROUP' and not context.scene.gptoolprops.name_for_current_render:
self.report({'ERROR'}, 'Need to set ouput name')
return {'CANCELLED'}
prefs = get_addon_prefs()
print('exclusions list ->', prefs.render_obj_exclusion)
exclusion_obj = [name.strip() for name in prefs.render_obj_exclusion.split(',')]
print('object exclusion list: ', exclusion_obj)
print('initial self.to_render: ', self.to_render)
self.to_render = []#reset
## check object to render with basic filter
for ob in context.scene.objects:
if ob.type != 'GPENCIL':
continue
if any(x in ob.name.lower() for x in exclusion_obj): #('old', 'rough', 'trash', 'test')
print('Skip', ob.name)
continue
self.to_render.append(ob.name)
if not self.to_render:
self.report({'ERROR'}, 'No GP to render')
return {'CANCELLED'}
## Reset at each render
# self.render_bool = tuple([True]*32)# reset all True
## disable for some name (ex: BG)
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.label(text='Tick objects to render')
for i, name in enumerate(self.to_render):
row = layout.row()
row.prop(self, 'render_bool', index = i, text = name)
# for i, set in enumerate(SETS):
# column.row().prop(context.scene.spritesheet, 'sets', index=i, text=set)
def execute(self, context):
prefs = get_addon_prefs()
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
render_list = []
for i, name in enumerate(self.to_render):
if self.render_bool[i]:
render_list.append(name)
if not render_list:
self.report({'ERROR'}, 'Nothing to render')
return {"CANCELLED"}
# self.report({'INFO'}, f'rendering {render_list}')#Dgb
# return {"FINISHED"}#Dgb
if self.mode == 'INDIVIDUAL':
errlist = render_invididually(context, render_list)
elif self.mode == 'GROUP':
errlist = render_grouped(context, render_list)
blend = Path(bpy.data.filepath)
# out = blend.parents[1] / "compo" / "base"
out = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve()
if out.exists():
open_folder(str(out))
else:
errlist.append('No compo/base folder created')
if errlist:
self.report({'ERROR'}, '\n'.join(errlist))
return {"FINISHED"}
### ---- Setup render path
class GPTRD_OT_setup_render_path(bpy.types.Operator):
bl_idname = "render.setup_render_path"
bl_label = "Setup render"
bl_description = "Setup render settings for normal render of the current state\nHint: F12 to check one frame, ctrl+F12 to render animation"
bl_options = {"REGISTER"}
def execute(self, context):
#get name and check
prefs = get_addon_prefs()
outname = context.scene.gptoolprops.name_for_current_render
if not outname:
self.report({'ERROR'}, 'No output name has been set')
return {"CANCELLED"}
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
set_render_settings()
blend = Path(bpy.data.filepath)
# out = blend.parents[1] / "compo" / "base"
out = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve()
fp = out / outname / (outname+'_')
context.scene.render.filepath = str(fp)
self.report({'INFO'}, f'output setup for "{outname}"')
return {"FINISHED"}
class GPTRD_OT_use_active_object_infos(bpy.types.Operator):
bl_idname = "render.use_active_object_name"
bl_label = "Use object Name"
bl_description = "Write active object name (active layer name with shift click on the button)"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object
def invoke(self, context, event):
# wm = context.window_manager
# return wm.invoke_props_dialog(self)
self.shift = event.shift
return self.execute(context)
def execute(self, context):
ob = context.object
#get name and check
if self.shift:
if ob.type != "GPENCIL":
self.report({'ERROR'}, 'Not a GP, no access to layers')
return {"CANCELLED"}
lay = ob.data.layers.active
if not lay:
self.report({'ERROR'}, 'No active layer found')
return {"CANCELLED"}
context.scene.gptoolprops.name_for_current_render = lay.info
else:
context.scene.gptoolprops.name_for_current_render = ob.name
# self.report({'INFO'}, 'Output Name changed')
return {"FINISHED"}
""" class GPTRD_OT_render_as_is(bpy.types.Operator):
bl_idname = "render.render_as_is"
bl_label = "render current"
bl_description = "Launch animation render with current setup"
bl_options = {"REGISTER"}
def execute(self, context):
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
return {"FINISHED"} """
### --- REGISTER
classes = (
GPTRD_OT_render_anim,
GPTRD_OT_setup_render_path,
GPTRD_OT_use_active_object_infos,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
def unregister():
for cl in classes:
bpy.utils.unregister_class(cl)
'''
## Potential cancelling method for image sequence rendering.
for cfra in range(start, end+1):
print("Baking frame " + str(cfra))
# update scene to new frame and bake to template image
scene.frame_set(cfra)
ret = bpy.ops.object.bake_image()
if 'CANCELLED' in ret:
return {'CANCELLED'}
'''
"""
class PBLAST_OT_render_wrap(bpy.types.Operator):
bl_idname = "render.render_wrap"
bl_label = "Render wraped"
bl_description = "render"
bl_options = {"REGISTER"}## need hide
use_view : bpy.props.BoolProperty(name='use_view', default=False)
def execute(self, context):
if self.use_view:## openGL
ret = bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, view_context=True)
else:## normal render
ret = bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
return {"FINISHED"}
"""
""" if __name__ == "__main__":
register() """

View File

@ -151,10 +151,6 @@ def register():
bpy.utils.register_class(GPTB_OT_sticky_cutter)
# register_keymaps()
def unregister():
if not bpy.app.background:
# unregister_keymaps()

View File

@ -2,11 +2,18 @@
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)**
### /!\ Main branch is currently broken, in migration to gpv3
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
**[Readme Doc in French (Documentation en Français et plus détaillée)](README_FR.md)**
**[Readme Doc in French](README_FR.md)**
It is recommended to enable _Grease Pencil Tools_ native Blender addon. Brings essentials features:
Canvas rotation, Box deform, Timeline scrubbing in viewport, Quick layer navigator.
---
@ -29,9 +36,38 @@ Set path to the palette folder (there is a json palette IO but you an also put a
Note about palette : For now the importer is not working with linked palette as it's not easy for animator (there are properties of the material you cannot access and the link grey-out fade the real color in UIlist preview)
### Environnement Variables
> Mainly for devellopers to set project environnement
Since 1.5.2, following _environnement variable_ can set the project properties in toolbox preferences at register launch:
`RENDER_WIDTH` : resolution x
`RENDER_HEIGHT` : resolution y
`FPS` : project frame rate
`PALETTES` : path to the blends (or json) containing materials palettes
`BRUSHES` : path to the blend containing brushes to load
`PREFIXES` : list of prefix (comma separated uppercase letters (2), an optional tooltip can be set after `:`, ex: 'LN:Line, CO:color, SH:Shadow') <!-- between 1 and 6 character -->
`SUFFIXES` : list of suffixes (comma separated uppercase letters of 2 character, ex: 'OL,UL')
`SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
### Expose native functionnality
The panel expose some attributes that are too "far" in the UI:
- Zoom 1:1 - Camera view take 100% zoom according to current scene resolution (ops `view3d.zoom_camera_1_to_1`)
- Zoom fit - Adjust view so camera frame takes full viewport spac (ops `view3d.view_center_camera`)
<!-- - autolock layer - tick layers'autolock -->
- In Front - the `In Front` property of the object to get an X-ray view
- passepartout camera - enable/disable + set opacity
- button and sliders to enable / disable / set opacity of single background camera images
**Edit line opacity** - Animators usually like to hide completely edit lines to have a better view of the drawing in edit/sculpt mode, lowering opacity also allows a better reading on what's selected.
This options is stored per layer per object but this apply on everything at once.
### Passive action
Add an "on save" Handler that trigger relative remap of all path (can be disabled in addon prefs).
An "on save" Handler that trigger relative remap of all path can be enabled in addon prefs (disabled by default).
### function
@ -71,7 +107,7 @@ Store strokes in os'clipboard (easier cross blend copy)
cutting is use a more user friendly (leave boundary points of left strokes untouched).
Also Possible to copy whole selected layers.
- Auto update : you have an updater in the addon preference tabs (use the [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater))
<!-- - Auto update : you have an updater in the addon preference tabs (use the [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater)) -->
**Palette management**
@ -84,6 +120,39 @@ In material submenu you have mutliple new entry:
- Load Color palette : same as the load above exept it loads directly from a blend file (all the material that the blend contains)
- Clean materials
**Shortcuts**
Viewport:
- Layer Picker from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
- Material Picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
- `F2` in Paint and Edit to rename active layer
- `Insert` add a new layer (same as Krita)
- `Shift + Insert` add a new layer and immediately pop-up a rename box
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
- `Shift + E` breakdown animation in object Mode
- `Ctrl + Shift + X/C/V` - Worldspace cut/copy/paste selected strokes/points:
Dopesheet:
- `Ctrl + Shift + X` Cut and send to layer
- `Ctrl + Shift + D` Duplicate and send to layer
Sculpt:
- point/stroke filter shortcut on `1`, `2`, `3` as toggles (similar to edit mode native shortcuts)
### Where ?
Panel in sidebar : 3D view > sidebar 'N' > Gpencil

View File

@ -2,16 +2,33 @@
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[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é)
**[English Readme Doc](README.md)**
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é d'activer l'addon natif _Grease pencil tools_ en parallèle qui ajoute des outils essentiels:
Rotation du Canvas, Boîte de déformation, Scrub dans la timeline, Navigation de calque facilité...
## Fonctionnalités et détails
### Variables d'environnement
> Cette partie s'adresse surtout aux dévellopeurs qui préparent les environement de projets.
Depuis la version 1.5.2, les variables d'environnement suivantes peuvent surcharger les préférences projet dans blender (au moment du register)
`RENDER_WIDTH` : résolution x
`RENDER_HEIGHT` : résolution y
`FPS` : Vitesse du projet
`PALETTES` : Chemin vers le dossier des blends (ou json) contenant des palettes de matériaux
`BRUSHES` : Chemin vers le dossier des blends contenant les brushes a charger
`PREFIXES` : Liste de prefixes du projet pour les noms de calques
`SUFFIXES` : Liste de suffixes (2 caractères majuscule, séparé par des virgule. ex: 'OL,UL')
`SEPARATOR` : Caractère de séparation pour determiner les prefixes, par défaut '_' (ne doit pas être un caractère spécial de regex !)
### Exposition dans l'UI de fonction native
@ -23,7 +40,7 @@ Expose les options suivantes:
- Zoom fit - Ajuste la vue pour que la cam soit vue en totalité dans l'écran (ops `view3d.view_center_camera`)
- Onion skin - coche des overlays
- autolock layer - coche du sous-menu de l'UI list des layers
- X-ray - Option `In Front` dans les propriété de l'objet
- In Front - met en avant l'option `In Front` des propriété de l'objet
- passepartout de caméra - active-désactive + opactité
- liste de boutons pour activer/désactiver les background de caméra (tous ou individuellement) avec icone par type.
@ -55,6 +72,9 @@ Le "couper" est également plus naturel (conserve les points d'extrémité sur l
Permet également de copier l'intégralité des layers selectionnés avec le bouton dédié (pas de raccourcis).
**check files** - série de check écris en dur dans le code. Pratique pour "fixer" rapidement sa scène:
la liste est visible et modifiable dans l'onglet "Check list" des preférences d'addons.
`Ctrl + Clic` sur le bonton permet de lister les changement sans les appliquer
Voilà quelques exemples:
- Lock main cam
- set scene res to def project res (specified in addon prefs)
- set scene percentage at 100:
@ -64,7 +84,7 @@ Permet également de copier l'intégralité des layers selectionnés avec le bou
- GP use additive drawing (else creating a frame in dopesheet makes it blank...)
- GP stroke placement/projection check (just warn if your not in 'Front')
- Warn if there are some disabled animation (and list datapath)
- Set onion skin filter to 'All type' (this became default in blender 2.91, guess who asked ;) )
- Set onion skin filter to 'All type'
EDIT: _rotate canvas_ et _box deform_ ont été retiré dans la version 1.0 car déja intégré à l'addon natif **grease pencil tools** depuis la 2.91 (activez simplement cet addon)
> **Rotate canvas** (`ctrl + alt + clic-droit`) - Différence avec celui intégré a grease pencil tools : la rotation en vue cam n'est possible que si on est dans une caméra de manipulation (`manip_cam`) pour éviter de casser l'anim le roll de la cam principale.
@ -88,67 +108,71 @@ Souci connu: Il y a un décalage d'une frame une fois activé sur un nouvel obje
### À potentiellement mettre de côté (peu utilisé)
**Tint layer** - Permet de mettre une teinte aléatoire par calque afin de les différencier rapidement. Souci de cet opérateurm, il ne faut pas l'utiliser si on utilise les tints de layer car il en change la couleur.
**Tint layer** - Permet de mettre une teinte aléatoire par calque afin de les différencier rapidement. Souci de cet opérateurm, il ne faut pas l'utiliser si la paropriétée `tints` des layers est utile aux projet car il modifie cette couleur.
<!-- **Colorize** (gros WIP) - un sous ensemble d'outils qui était censé permettre de faire du remplissage bitmap via des color spots en envoyant deux set d'images rendu automatiquement à GMIC en ligne de commande et recalé la séquence de résultat en BG de Cam. Finalement abandonné, pas eu le temps de finir la mise au point (malgré des résultats préliminaires intéressant).
Mais trop long a mettre en place, trop hackeu, et surtout c'est dommage de basculer sur du bitmap, la source de couleur doit rester au maximum GP/vecto. -->
## colorisation
**Line stopper** - Extension des lignes pour améliorer la fermeture des formes (génère un matériaux à part pour les masquer/supprimer facilement plus tard).
Le hack est très simple mais aide beaucoup à fermer les contour pour éviter le leak de l'outils pot de peinture.
Permet de diminuer le `leak_size` de ce dernier ce qui corrige certains pins de colo dans les angles obtus.
**Create empty frame** - Permet de créer des frames vides sur un calques partout où il y a des frames sur les calques supérieur (permet de faire un key to key sur le calque actif ensuite sur les key pré-créée pour faire sa colo).
En réalité pour quelque chose de plus pratique pour la colo, il suffit d'ajouter un autre `screen.gp_keyframe_jump` operator en activant le filtre (all layers)
TODO: faire un "all layer _above_" ou se baser sur le nouveau filtre natif existant pour cet usage depuis blender 2.91.
**Render** - chemin de sorties + 2 boutons:
- layers individually (popup pour selectionner des calques a rendre individuellement)
- layers grouped (popup pour selectionner des layers a rendre ensemble)
- utilise le chemin relatif spécifié dans les preférences d'addon pour créer un chemin par groupe comme suit : `pref.location/name/name_####`
TODO: profiter du système de render layers (per layer) pour faire un meilleur batch renderer.
**Create empty frame** - Permet de créer des frames vides sur un calques en se basant sur les frames des calques choisis (permet de faire un key to key sur le calque actif ensuite sur les key pré-créée pour faire sa colo).
Sinon pratique pour la colo, on peut aussi ajouter un autre `screen.gp_keyframe_jump` operator en activant le filtre (all layers) dans les options
## tools supplémentaires
**Check links** (pop une fenêtre) - Permet de lister les liens de la scène, voir si il y en a des cassés et ouvrir le dossier d'un lien existant.
**Check links** (pop une fenêtre) - Permet de lister les liens de la scène, voir si il y en a des cassés et ouvrir le dossier d'un lien existant et copier un lien.
**Auto update** - Un système de mise à jour est diponible dans les addon prefs, autocheck possible (désactivé par défaut). Utilise [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater))
<!-- **Auto update** - Un système de mise à jour est diponible dans les addon prefs, autocheck possible (désactivé par défaut). Utilise [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater)) -->
## raccourci supplémentaires
## raccourcis supplémentaires
Viewport:
- `W` (stroke) et `Alt + W` (fill) Sélectionne un calque d'après le trait le plus proche du curseur (en paint mode)
- `S` (fill) et `Alt + S` (stroke) selectionne un matériaux d'après le trait le plus proche du curseur. (en paint fill mode) (la touche doit pressé/relaché en moins de 200ms)
- `F2` Pop-up pour renommer le calque actif (en paint et en edit mode)
- `Insert` Ajoute un nouveau layer (comme dans Krita)
- `Shift + Insert` Ajoute un nouveau layer et immédiatement apelle le pop-up pour renommer le calque
- `page up / page down` change le calque actif en estompant temporairement les autres calques (la force d'estompe est personalisable dans les prefs de l'addon)
- `Shift + E` breakdown sur l'animation d'objet en mode Objet (Fonctionne comme celui disponible sur des bones en pose mode).
- `Ctrl + Shift + X/C/V` - Couper/Copier/Coller en World Space (indépendamment de la position de l'objet)
Dopesheet:
- `Ctrl + Shift + X` Couper une clé et l'envoyer sur un autre calque
- `Ctrl + Shift + D` Dupliquer une clé et l'envoyer sur un autre calque
Sculpt mode:
- point/stroke filter shortcut sur `1`, `2`, `3` en toggle (similaire a l'edit mode)
- `1`, `2`, `3` (rangée au-dessus des lettres) Bascule les filtres de selection par Points/Strokes, comme en edit mode.
Grease pencil 3D cursor: Surcharge du raccourci curseur 3D pour le snapper à la surface du grease pencil. (Raccourci à remplacer manuellement dans la keymap, idname:`view3d.cusor_snap`, idname de l'opérateur de curseur natif `view3d.cursor3d`. Pas forcément utile si il n'y a pas de mix de 2D/3D.)
Le mieux reste d'avoir un raccourci dédié, séparé de celui d'origine...
<!-- Grease pencil 3D cursor: Surcharge du raccourci curseur 3D pour le snapper à la surface du grease pencil. (Raccourci à remplacer manuellement dans la keymap, idname:`view3d.cusor_snap`, idname de l'opérateur de curseur natif `view3d.cursor3d`. Pas forcément utile si il n'y a pas de mix de 2D/3D.)
Le mieux reste d'avoir un raccourci dédié, séparé de celui d'origine... -->
---
### TODO:
### Idées:
- Permettre de rendre avec la résolution spécifié dans le nom de la caméra active
(utile dans les projet rendu a la résolution du BG mais ou la résolution finale peut être utilisé pour un bout-a-bout)
- Update du système de "passes" de rendu:
- utiliser des render layers + file outputs au lieux de faire des batchs par opacité
- BG Playblast enhancement:
- Tester davantage le playblast BG
- Éventuellement mettre une coche de fallback vers le playblast classique (utile en cas de pépin.
- Faire un import-export des réglage généraux en json (Déjà une bonne partie du code dans Pipe sync)
pour set : Résolution du film, dossier palette, render settings
pour set : Résolution du film, dossier palette, render settings...
- opt: exposer les "tool setting" de placement de canvas en permanence dans la sidebar (visible seulement en draw)
- Déplacer automatiquement la vue "Face" au GP (en fonction des Gpencil view 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)
- Meilleure table lumineuse (grosse réflexion et travail en perspective)
- 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 les repos d'[Andarta](https://gitlab.com/andarta-pictures)
---

View File

@ -7,7 +7,7 @@ from .utils import get_addon_prefs
class GPTB_WT_eraser(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'PAINT_GPENCIL'
bl_context_mode = 'PAINT_GREASE_PENCIL'
# The prefix of the idname should be your add-on name.
bl_idname = "gp.eraser_tool"

View File

@ -1,14 +1,18 @@
from . import addon_updater_ops
from .utils import get_addon_prefs
# from . import addon_updater_ops
from .utils import (get_addon_prefs,
anim_status,
gp_modifier_status,
)
import bpy
from pathlib import Path
from bpy.types import Panel
## UI in properties
### dataprop_panel not used --> transferred to sidebar
"""
class GPTB_PT_dataprop_panel(bpy.types.Panel):
class GPTB_PT_dataprop_panel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
# bl_space_type = 'VIEW_3D'
@ -16,7 +20,7 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
# bl_category = "Tool"
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
bl_label = "Pseudo color"# title
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
@ -35,8 +39,8 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
## UI in Gpencil sidebar menu
class GPTB_PT_sidebar_panel(bpy.types.Panel):
bl_label = "Toolbox"
class GPTB_PT_sidebar_panel(Panel):
bl_label = "GP Toolbox"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
@ -44,23 +48,28 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
def draw(self, context):
layout = self.layout
# layout.use_property_split = True
prefs = get_addon_prefs()
rd = context.scene.render
# check for update
addon_updater_ops.check_for_update_background()
# addon_updater_ops.check_for_update_background()
# layout.label(text='View options:')
## flip X cam
if context.scene.camera and context.scene.camera.scale.x < 0:
# layout.label(text='! Flipped !')
row = layout.row(align=True)
col = layout.column()
row.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
## flip X cam
# layout.label(text='! Flipped !')
row = col.row(align=True)
row.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
row = col.row(align=True)
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
if context.scene.camera and context.scene.camera.scale.x < 0:
row.label(text='',icon='LOOP_BACK')
else:
layout.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
## draw/manipulation camera
col = layout.column()
if context.scene.camera and context.scene.camera.name.startswith(('draw', 'obj')):
row = col.row(align=True)
row.operator('gp.draw_cam_switch', text = 'Main cam', icon = 'OUTLINER_OB_CAMERA')
@ -86,6 +95,9 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
else:
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
# row = layout.row(align=True)
# row.operator('view3d.view_camera_frame_fit', text = 'Custom fit', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
row = layout.row(align=True)
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER')
@ -122,21 +134,19 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
## 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")
layout.operator(bpy.types.GP_OT_straight_stroke.bl_idname, icon ="CURVE_PATH")
## Options
layout.separator()
layout.label(text = 'Options:')
col = layout.column()
col.label(text = 'Options:')
## Kf Jump filter
layout.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
col = layout.column()
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
# 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 == 'GREASEPENCIL':
# col.prop(context.object.data, 'use_autolock_layers') # not often used
col.prop(context.object, 'show_in_front', text='X-ray') # default text "In Front"
col.prop(context.object, 'show_in_front') # text='In Front'
## rename datablock temporary layout
if context.object.name != context.object.data.name:
@ -147,6 +157,7 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True
## Check base palette
if prefs.warn_base_palette and prefs.palette_path:
if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
box = col.box()
box.label(text='Missing base material setup', icon='INFO')
@ -156,37 +167,19 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
col.label(text='No GP object selected')
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 )
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
## File checker
row = col.row(align=True)
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
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)
## Gpv3: not more edit line (use Curve lines)
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
# Mention update as notice
addon_updater_ops.update_notice_box_ui(self, context)
# addon_updater_ops.update_notice_box_ui(self, context)
# row = layout.row(align=False)
# row.label(text='arrow choice')
# row.operator("my_operator.multi_op", text='', icon='TRIA_LEFT').left = 1
# row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0
class GPTB_PT_anim_manager(bpy.types.Panel):
bl_label = "Animation manager"
class GPTB_PT_anim_manager(Panel):
bl_label = "Animation Manager"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
@ -196,38 +189,103 @@ class GPTB_PT_anim_manager(bpy.types.Panel):
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def get_object_by_types(self, context) -> dict:
# import time
# t0 = time.perf_counter()
# objs = [o for o in context.scene.objects if o.type not in ('GREASEPENCIL', 'CAMERA')]
# gps = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
# cams = [o for o in context.scene.objects if o.type == 'CAMERA']
objs = []
gps = []
cams = []
for o in context.scene.objects:
if o.type not in ('GREASEPENCIL', 'CAMERA'):
objs.append(o)
elif o.type == 'GREASEPENCIL':
gps.append(o)
elif o.type == 'CAMERA':
cams.append(o)
# print(f'{time.perf_counter() - t0:.8f}s')
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
## Animation enable disable anim (shift click to select) OP_helpers.GPTB_OT_toggle_mute_animation
layout.operator('gp.list_disabled_anims')
## Objs ()
row = layout.row(align=True)
row.label(text='Obj anims:')
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
ops.skip_gp = True
ops.skip_obj = False
obj_types = self.get_object_by_types(context)
col.operator('gp.list_disabled_anims')
## Show Enable / Disable anims
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]:
on_icon, off_icon = anim_status(obj_types[cat_type])
subcol = col.column()
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
row = subcol.row(align=True)
row.label(text=cat)
ops = row.operator('gp.toggle_mute_animation', text='ON', icon=on_icon)
ops.mode = cat_type
ops.mute = False
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
ops.skip_gp = True
ops.skip_obj = False
ops.mute = True
## Gps
row = layout.row(align=True)
row.label(text='Gp anims:')
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
ops.skip_gp = False
ops.skip_obj = True
ops.mute = False
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
ops.skip_gp = False
ops.skip_obj = True
ops = row.operator('gp.toggle_mute_animation', text='OFF', icon=off_icon)
ops.mode = cat_type
ops.mute = True
class GPTB_PT_toolbox_playblast(bpy.types.Panel):
## GP modifiers
subcol = col.column()
row = subcol.row(align=True)
row.label(text='Gp modifiers:')
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
## Step Select Frames
col.operator('gptb.step_select_frames')
## Follow curve path
col = col.column()
row = col.row(align=True)
if context.object:
if context.object.type == 'CURVE' and context.mode in ('OBJECT', 'EDIT_CURVE'):
row.operator('object.object_from_curve', text='Back To Object', icon='LOOP_BACK')
elif (follow_const := context.object.constraints.get('Follow Path')) and follow_const.target:
row.operator('object.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE')
row.operator('object.remove_follow_path', text='', icon='X')
col.label(text=f'{context.object.name} -> {follow_const.target.name}', icon='CON_FOLLOWPATH')
if follow_const.use_fixed_location:
col.prop(follow_const, 'offset_factor')
else:
col.prop(follow_const, 'offset')
if context.object.location.length != 0: # context.object.location[:] != (0,0,0):
col.operator('object.location_clear', text='Offseted Location! Reset', icon='ERROR')
# ? Check if object location is animated ? (can be intentional...)
else:
col.operator('object.create_follow_path_curve', text='Create Follow Curve', icon='CURVE_BEZCURVE')
## This can go in an extra category...
col = layout.column()
col.use_property_split = False
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)
if context.scene.gptoolprops.cursor_follow:
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
class GPTB_PT_toolbox_playblast(Panel):
bl_label = "Playblast"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -249,18 +307,14 @@ class GPTB_PT_toolbox_playblast(bpy.types.Panel):
# 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):
bl_label = "Tint layers"
class GPTB_PT_tint_layers(Panel):
bl_label = "Tint Layers"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
@ -270,51 +324,82 @@ class GPTB_PT_tint_layers(bpy.types.Panel):
## pseudo color layers
# layout.separator()
col = layout.column(align = True)
row = col.split(align=False, factor=0.63)
row.prop(context.scene.gptoolprops, 'autotint_offset')
row.prop(context.scene.gptoolprops, 'autotint_namespace')
# row = col.split(align=False, factor=0.63)
# row = col.row()
col.prop(context.scene.gptoolprops, 'autotint_offset', text='Hue Offset')
col.prop(context.scene.gptoolprops, 'autotint_namespace')
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
class GPTB_PT_render(bpy.types.Panel):
bl_label = "Render"
class GPTB_PT_checker(Panel):
bl_label = "Checker"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context):
layout = self.layout
layout.operator('render.render_anim', text = 'Render invividually', icon = 'RENDERLAYERS').mode = 'INDIVIDUAL'#RENDER_STILL #RESTRICT_RENDER_OFF
layout.operator('render.render_anim', text = 'Render grouped', icon = 'IMAGE_RGB').mode = 'GROUP'
col = layout.column()
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')
layout.separator()
row = layout.row()
row.prop(context.scene.gptoolprops, 'name_for_current_render', text = 'Output name')#icon = 'OUTPUT'
row.operator('render.use_active_object_name', text = '', icon='OUTLINER_DATA_GP_LAYER')#icon = 'OUTPUT'
## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
## check drawing alignement
col.operator('gp.check_canvas_alignement', icon='DRIVER_ROTATIONAL_DIFFERENCE')
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS
blend = bpy.data.filepath
if blend:
blend = Path(blend)
out = blend.parents[1] / "compo" / "base"
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
## File checker
row = col.row(align=True)
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
class GPTB_PT_color(Panel):
bl_label = "Color"
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
col = layout.column()
## Create empty frame on layer
# Material panel as a pop-up (would work if palette dir is separated)
# col.operator("wm.call_panel", text="Link Materials Palette", icon='COLOR').name = "GPTB_PT_palettes_linker_ui"
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
# col.operator("wm.call_panel", text="Link Material Palette", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
""" # unused : added in Animation Manager
class GPTB_PT_extra(Panel):
bl_label = "Extra"
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
col = layout.column()
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)
"""
"""
## unused -- (integrated in sidebar_panel)
class GPTB_PT_cam_ref_panel(bpy.types.Panel):
class GPTB_PT_cam_ref_panel(Panel):
bl_label = "Background imgs"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -340,38 +425,375 @@ class GPTB_PT_cam_ref_panel(bpy.types.Panel):
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
"""
def palette_manager_menu(self, context):
"""Palette menu to append in existing menu"""
# GPENCIL_MT_material_context_menu
layout = self.layout
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
layout.separator()
prefs = get_addon_prefs()
layout.operator("gp.copy_active_to_selected_palette", text='Append Materials To Selected', icon='MATERIAL')
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
layout.separator()
layout.operator("wm.call_panel", text="Pop Palette Linker", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
layout.operator("gp.load_blend_palette", text='Load Mats From Single Blend', icon='RESTRICT_COLOR_ON').filepath = prefs.palette_path
layout.separator()
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.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')
layout.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context):
# add in GreasePencilLayerDisplayPanel (gp dopesheet View > Display)
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.label(text='Use Channel Colors (User preferences):')
layout.prop(context.preferences.edit, 'use_anim_channel_group_colors')
#--- Palette Linker Panels
def palettes_path_ui(self, context):
layout = self.layout
scn = bpy.context.scene
pl_prop = scn.bl_palettes_props
col= layout.column()
prefs = get_addon_prefs()
## Here put the path thing (only to use a non-library)
# maybe in submenu...
row = col.row()
# expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT'
# row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False)
row.prop(pl_prop, 'use_project_path', text='Use Project Palettes')
# row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
if pl_prop.use_project_path:
## gp toolbox addon prefs path
if not prefs.palette_path:
col.label(text='GP toolbox Palette Directory Needed', icon='INFO')
col.operator('gptb.open_addon_prefs', icon='PREFERENCES')
# if not prefs.palette_path: # or pl_prop.show_path
# col.prop(prefs, 'palette_path', text='Project Dir')
#col.label(text='(saved with preferences)')
else:
## local path
if not pl_prop.custom_dir:
col.label(text='Need to specify directory')
col.prop(pl_prop, 'custom_dir', text='Custom Dir')
# if not pl_prop.custom_dir or pl_prop.show_path:
# col.prop(pl_prop, 'custom_dir', text='Custom Dir')
# col.operator('gptb.palette_version_update', text='Update Palette Version') # when update is ready
def palettes_lists_ui(self, context, popup=False):
layout = self.layout
scn = bpy.context.scene
pl_prop = scn.bl_palettes_props
col= layout.column()
row=col.row()
# refresh button
txt = 'Project Palettes' if pl_prop.use_project_path else 'Custom Palettes'
row.label(text=txt)
row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
col= layout.column()
row = col.row()
if popup:
blends_minimum_row = 5
objects_minimum_row = 25
else:
blends_minimum_row = 2
objects_minimum_row = 4
row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx",
rows=blends_minimum_row)
# side panel
# subcol = row.column(align=True)
# subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
## Show object UI list only once blend Uilist is filled ?
if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)):
col.label(text='Select blend refresh available objects')
row = col.row()
row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx",
rows=objects_minimum_row)
## Show link button in the border of the UI list ?
# col.prop(pl_prop, 'import_type')
split = col.split(align=True, factor=0.4)
split.prop(pl_prop, 'import_type', text='')
split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path)
split.operator('gp.import_obj_palette', text='Palette')
# button to launch link with combined props (active only if the two items are valids)
# str(Path(self.blends) / 'Object' / self.objects
class GPTB_PT_palettes_linker_main_ui(Panel):
bl_space_type = 'TOPBAR' # dummy
bl_region_type = 'HEADER'
# bl_space_type = "VIEW_3D"
# bl_region_type = "UI"
# bl_category = "Gpencil"
bl_label = "Palettes Mat Linker"
def draw(self, context):
layout = self.layout
## link button for tests
# layout.operator('gp.import_obj_palette', text='Palette')
# Subpanel are appended to this main UI
## Or just as One fat panel
# palettes_path_ui(self, context)
# palettes_lists_ui(self, context)
class GPTB_PT_palettes_path_ui(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_label = "Palettes Source" # Source Path
# bl_parent_id = "GPTB_PT_palettes_linker_main_ui"
bl_parent_id = "GPTB_PT_color"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
palettes_path_ui(self, context)
# layout.label()
# pop-up version of object lists
class GPTB_PT_palettes_list_popup(Panel):
bl_space_type = 'TOPBAR' # dummy
bl_region_type = 'HEADER'
bl_category = "Gpencil"
bl_label = "Palettes Lists"
bl_ui_units_x = 18
def draw(self, context):
palettes_lists_ui(self, context, popup=True)
class GPTB_PT_palettes_list_ui(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_label = "Palettes Lists"
bl_parent_id = "GPTB_PT_color"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, context):
layout = self.layout
# layout.label(text="My Select Panel")
layout.operator("wm.call_panel", text="", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
def draw(self, context):
palettes_lists_ui(self, context, popup=False)
## bl 3+ UI
def asset_browser_ui(self, context):
'''Only shows in blender >= 3.0.0'''
layout = self.layout
asset_file_handle = context.asset_file_handle
if asset_file_handle is None:
# layout.label(text="No asset selected", icon='INFO')
layout.label(text='No object/material selected', icon='INFO')
return
if asset_file_handle.id_type not in ('OBJECT', 'MATERIAL'):
layout.label(text='No object/material selected', icon='INFO')
return
layout.use_property_split = True
layout.use_property_decorate = False
asset_library_ref = context.asset_library_ref
## Path to blend
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
path_to_obj = Path(asset_lib_path) / 'Objects' / asset_file_handle.name
## respect header choice ?
## import_type in (LINK, APPEND, APPEND_REUSE)
imp_type = context.space_data.params.import_type
if imp_type == 'APPEND':
imp_txt = 'Append'
elif imp_type == 'APPEND_REUSE':
imp_txt = 'Append (Reuse)'
else:
imp_txt = 'Link'
if asset_file_handle.id_type == 'MATERIAL':
layout.label(text=f'From Mat: {asset_file_handle.name}')
if asset_file_handle.id_type == 'OBJECT':
layout.label(text=f'From Obj: {asset_file_handle.name}')
layout.label(text=f'{imp_txt} Materials To GP Object')
layout.operator('gp.palette_linker', text=f'{imp_txt} Materials To GP Object') ## ops
# layout.label(text='Link Materials to GP Object')
# Put back pop-over UI for Grease Pencil stroke interpolation tools native pop hover panel from 2.92
class GPTB_PT_tools_grease_pencil_interpolate(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_label = "Interpolate"
@classmethod
def poll(cls, context):
if context.gpencil_data is None:
return False
gpd = context.gpencil_data
valid_mode = bool(gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode)
return bool(context.editable_gpencil_strokes) and valid_mode
def draw(self, context):
layout = self.layout
layout.use_property_split = True
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
## access active tool settings
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate')
## custom curve access (still in gp interpolate tools)
interpolate_settings = context.tool_settings.gpencil_interpolate
# ex : interpolate_settings.interpolation_curve.curves[0].points[1].location
col = layout.column(align=True)
col.label(text="Interpolate Strokes")
col.operator("gpencil.interpolate", text="Interpolate")
col.operator("gpencil.interpolate_sequence", text="Sequence")
col.operator("gpencil.interpolate_reverse", text="Remove Breakdowns")
col = layout.column(align=True)
col.label(text="Options:")
# col.prop(settings, "interpolate_all_layers") # now the enum "layers"
gpd = context.gpencil_data
if gpd.use_stroke_edit_mode:
col.prop(settings, "interpolate_selected_only")
col.prop(settings, "layers")
col.prop(settings, "flip")
col.prop(settings, "smooth_factor")
col.prop(settings, "smooth_steps")
'''## Sequence Options
seq_settings = context.window_manager.operators.get('GPENCIL_OT_interpolate_sequence')
col = layout.column(align=True)
col.label(text="Sequence Options:")
if not seq_settings:
# col.label(text='Launch Interpolate Sequence Once')
# col.operator('gpencil.interpolate_sequence',text='Interpolate Sequence Once')
col.label(text='Interpolate sequence', icon='INFO')
col.label(text='must be launched')
col.label(text="once per session")
col.label(text="to expose it's properties")
return
col.prop(seq_settings, "step")
col.prop(seq_settings, "layers")
col.prop(seq_settings, "interpolate_selected_only")
col.prop(seq_settings, "flip")
col.prop(seq_settings, "smooth_factor")
col.prop(seq_settings, "smooth_steps")
col.prop(seq_settings, "type")
if seq_settings.type == 'CUSTOM':
# TODO: Options for loading/saving curve presets?
col.template_curve_mapping(interpolate_settings, "interpolation_curve", brush=True,
use_negative_slope=True)
elif seq_settings.type != 'LINEAR':
col.prop(seq_settings, "easing")
if seq_settings.type == 'BACK':
layout.prop(seq_settings, "back")
elif seq_settings.type == 'ELASTIC':
sub = layout.column(align=True)
sub.prop(seq_settings, "amplitude")
sub.prop(seq_settings, "period")
'''
## recreate property group from operator options
# inspect context.window_manager.operators['GPENCIL_OT_interpolate_sequence']
# separate options from single interpolation and sequence interpolation
# class GPTB_PG_interpolate_sequence_prop(bpy.types.PropertyGroup):
# interpolate_selected_only : BoolProperty(
# name="Selected Only",
# description="",
# default=True,
# options={'HIDDEN'})
def interpolate_header_ui(self, context):
layout = self.layout
obj = context.active_object
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data:
gpd = context.gpencil_data
else:
return
if gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode:
row = layout.row(align=True)
row.popover(
panel="GPTB_PT_tools_grease_pencil_interpolate",
text="Interpolate",
)
classes = (
GPTB_PT_sidebar_panel,
GPTB_PT_checker,
GPTB_PT_anim_manager,
GPTB_PT_toolbox_playblast,
GPTB_PT_color,
GPTB_PT_tint_layers,
GPTB_PT_render,
## GPTB_PT_cam_ref_panel,
GPTB_PT_toolbox_playblast,
# GPTB_PT_tools_grease_pencil_interpolate, # WIP
# palettes linker
GPTB_PT_palettes_linker_main_ui, # main panel
GPTB_PT_palettes_list_popup, # popup (dummy region)
GPTB_PT_palettes_path_ui, # subpanels
GPTB_PT_palettes_list_ui, # subpanels
# GPTB_PT_extra,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
# if bpy.app.version >= (3,0,0):
# bpy.types.ASSETBROWSER_PT_metadata.append(asset_browser_ui)
def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# if bpy.app.version >= (3,0,0):
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
@ -392,33 +814,3 @@ def GPdata_toolbox_panel(self, context):
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
"""
### old
"""
col = layout.column(align = True)
col.operator("gpencil.stroke_change_color", text="Move to Color",icon = "COLOR")
col.operator("transform.shear", text="Shear")
col.operator("gpencil.stroke_cyclical_set", text="Toggle Cyclic").type = 'TOGGLE'
col.operator("gpencil.stroke_subdivide", text="Subdivide",icon = "OUTLINER_DATA_MESH")
row = layout.row(align = True)
row.operator("gpencil.stroke_join", text="Join").type = 'JOIN'
row.operator("grease_pencil.stroke_separate", text = "Separate")
col.operator("gpencil.stroke_flip", text="Flip Direction",icon = "ARROW_LEFTRIGHT")
col = layout.column(align = True)
col.operator("gptools.randomise",icon = 'RNDCURVE')
col.operator("gptools.thickness",icon = 'LINE_DATA')
col.operator("gptools.angle_split",icon = 'MOD_BEVEL',text='Angle Splitting')
col.operator("gptools.stroke_uniform_density",icon = 'MESH_DATA',text = 'Density')
row = layout.row(align = True)
row.prop(settings,"extra_tools",text='',icon = "DOWNARROW_HLT" if settings.extra_tools else "RIGHTARROW",emboss = False)
row.label("Extra tools")
if settings.extra_tools :
layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction")
"""

View File

@ -1,33 +1,22 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# SPDX-License-Identifier: GPL-2.0-or-later
bl_info = {
"name": "GP toolbox",
"description": "Set of tools for Grease Pencil in animation production",
"description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux",
"version": (1, 5, 1),
"blender": (2, 91, 0),
"version": (4, 0, 4),
"blender": (4, 3, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
"category": "3D View",
}
from . import addon_updater_ops
from .utils import *
from .functions import *
from pathlib import Path
from shutil import which
from sys import modules
from .utils import get_addon_prefs, draw_kmi
## GMIC
from .GP_guided_colorize import GP_colorize
@ -41,31 +30,47 @@ from . import OP_helpers
from . import OP_keyframe_jump
from . import OP_cursor_snap_canvas
from . import OP_palettes
from . import OP_palettes_linker
from . import OP_brushes
from . import OP_file_checker
from . import OP_render
from . import OP_copy_paste
from . import OP_realign
# from . import OP_flat_reproject # Disabled
from . import OP_depth_move
from . import OP_key_duplicate_send
from . import OP_eraser_brush
from . import TOOL_eraser_brush
from . import OP_layer_manager
from . import OP_layer_picker
from . import OP_layer_nav
from . import OP_material_picker
from . import OP_git_update
from . import OP_layer_namespace
from . import OP_pseudo_tint
from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush
# from . import TOOL_eraser_brush
from . import handler_draw_cam
from . import keymaps
from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers
from . import UI_tools
from .properties import GP_PG_ToolsSettings, GP_PG_FixSettings
from .properties import (
GP_PG_ToolsSettings,
GP_PG_FixSettings,
GP_PG_namespaces,
)
from bpy.props import (FloatProperty,
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
IntProperty,
PointerProperty
)
import bpy
import os
from bpy.app.handlers import persistent
from pathlib import Path
# from .eyedrop import EyeDropper
@ -100,75 +105,42 @@ def remap_on_save_update(self, context):
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
## precise eraser
# 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):
bl_idname = __name__
use_precise_eraser : BoolProperty(
name='Precise Eraser',
default=False,
update=update_use_precise_eraser
)
## precise eraser
# use_precise_eraser : BoolProperty(
# name='Precise Eraser',
# default=False,
# update=update_use_precise_eraser)
## tabs
pref_tabs : EnumProperty(
items=(('PREF', "Preferences", "Change some preferences of the modal"),
('MAN_OPS', "Operator", "Operator to add Manually"),
# ('TUTO', "Tutorial", "How to use the tool"),
('KEYS', "Shortcuts", "Customize addon shortcuts"),
('MAN_OPS', "Operators", "Operator to add Manually"),
('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"),
('UPDATE', "Update", "Check and apply updates"),
# ('UPDATE', "Update", "Check and apply updates"),
# ('TUTO', "Tutorial", "How to use the tool"),
# ('KEYMAP', "Keymap", "customise the default keymap"),
),
default='PREF')
## addon pref updater props
auto_check_update : BoolProperty(
name="Auto-check for Update",
description="If enabled, auto-check for updates using an interval",
default=False,
)
updater_intrval_months : IntProperty(
name='Months',
description="Number of months between checking for updates",
default=0,
min=0
)
updater_intrval_days : IntProperty(
name='Days',
description="Number of days between checking for updates",
default=7,
min=0,
max=31
)
updater_intrval_hours : IntProperty(
name='Hours',
description="Number of hours between checking for updates",
default=0,
min=0,
max=23
)
updater_intrval_minutes : IntProperty(
name='Minutes',
description="Number of minutes between checking for updates",
default=0,
min=0,
max=59
)
## addon prefs
## Project preferences
#--# PROJECT PREFERENCES #--#
# subtype (string) Enumerator in ['FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'].
# update variables
is_git_repo : BoolProperty(default=False)
has_git : BoolProperty(default=False)
## fps
use_relative_remap_on_save : BoolProperty(
@ -200,21 +172,77 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
separator : StringProperty(
name="Namespace separator",
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified",
default="_", maxlen=0, subtype='NONE')
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH')
use_env_palettes : BoolProperty(
name="Use Project Palettes",
description="Load the palette path in environnement at startup (key 'PALETTES')",
default=True,
)
palette_path : StringProperty(
name="Palettes directory",
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
warn_base_palette : BoolProperty(
name="Warn if base palette isn't loaded",
description="Display a button to load palette base.json if current grease pencil has a no 'line' and 'invisible' materials",
default=True,
)
mat_link_exclude : StringProperty(
name="Materials Link Exclude",
description="List of material name to exclude when using palette linker (separate multiple value with comma, ex: line, rough)",
default="line,", maxlen=0)
use_env_brushes : BoolProperty(
name="Use Project Brushes",
description="Load the brushes path in environnement at startup (key 'BRUSHES')",
default=True,
)
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
## namespace
separator : StringProperty(
name="Separator",
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified",
default="_", maxlen=0, subtype='NONE')
## Old one string comma separated prefix/suffix list
# prefixes : StringProperty(
# name="Layers Prefixes",
# description="List of prefixes (two capital letters) available for layers(ex: AN,CO,CL)",
# default="", maxlen=0)
# suffixes : StringProperty(
# name="Layers Suffixes",
# description="List of suffixes (two capital letters) available for layers(ex: OL,UL)",
# default="", maxlen=0)
prefixes : PointerProperty(type=GP_PG_namespaces)
suffixes : PointerProperty(type=GP_PG_namespaces)
# use_env_namespace : BoolProperty(
# name="Use Project namespace",
# description="Ovewrite prefix/suffix with Project values defined in environnement at startup\n(key 'PREFIXES and SUFFIXES')",
# default=True,
# )
show_prefix_buttons : BoolProperty(
name="Show Prefix Buttons",
description="Show prefix and suffix buttons above layer stack",
default=True,
)
## Playblast prefs
playblast_auto_play : BoolProperty(
name="Playblast auto play",
@ -228,19 +256,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=False,
)
## default active tool to use
select_active_tool : EnumProperty(
name="Default selection tool", description="Active tool to set when launching check fix scene",
default='builtin.select_lasso',
items=(
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
))
## render settings
render_obj_exclusion : StringProperty(
name="GP obj exclude filter",
@ -311,12 +326,65 @@ class GPTB_prefs(bpy.types.AddonPreferences):
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings)
## GP Layer navigator
nav_use_fade : BoolProperty(
name='Fade Inactive Layers',
description='Fade Inactive layers to determine active layer in a glimpse',
default=True)
nav_fade_val : FloatProperty(
name='Fade Value',
description='Fade value for other layers when navigating (0=invisible)',
default=0.1, min=0.0, max=0.95, step=1, precision=2)
nav_limit : FloatProperty(
name='Fade Duration',
description='Time of other layer faded when using layer navigation',
default=1.4, min=0.1, max=5, step=2, precision=1, subtype='TIME', unit='TIME')
nav_use_fade_in : BoolProperty(
name='Progressive Fade Back',
description='Use a fade on other layer when navigating',
default=True)
nav_fade_in_time : FloatProperty(
name='Fade-In Time',
description='Duration of the fade',
default=0.5, min=0.1, max=5, step=2, precision=2, subtype='TIME', unit='TIME')
nav_interval : FloatProperty(
name='Refresh Rate',
description='Refresh rate for fade updating (upper value means stepped fade)',
default=0.04, min=0.01, max=0.5, step=3, precision=2, subtype='TIME', unit='TIME')
## Temp cutter
# temp_cutter_use_shortcut: BoolProperty(
# name = "Use temp cutter Shortcut",
# description = "Auto assign shortcut for temp_cutter",
# default = True)
def draw_namespaces_list(self, layout, template_list, pg_name, rows=4):
'''Get layout, property group to draw and default row number'''
pg = getattr(self, pg_name)
row = layout.row(align=True)
row.template_list(template_list, "", pg, "namespaces", pg, "idx", rows=rows)
subcol = row.column(align=True) # Lateral right
subcol.operator("gptb.add_namespace_entry", icon="ADD", text="").propname=pg_name
subcol.operator("gptb.remove_namespace_entry", icon="REMOVE", text="").propname=pg_name
subcol.separator()
op_move = subcol.operator("gptb.move_item", icon="TRIA_UP", text="")
op_move.propname = pg_name
op_move.direction = 'UP'
op_move = subcol.operator("gptb.move_item", icon="TRIA_DOWN", text="")
op_move.propname = pg_name
op_move.direction = 'DOWN'
## Reset entry (Not needed anymore)
# subcol.separator()
# subcol.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'PREFIXES'
def draw(self, context):
layout = self.layout## random color
# layout.use_property_split = True
@ -336,14 +404,53 @@ class GPTB_prefs(bpy.types.AddonPreferences):
row.label(text='Render Resolution')
row.prop(self, 'render_res_x', text='Width')
row.prop(self, 'render_res_y', text='Height')
box.prop(self, 'use_relative_remap_on_save')
box.prop(self, "render_obj_exclusion", icon='FILTER')#
subbox = box.box()
subbox.label(text='Project folders:')
## Palette
box.label(text='Project folders:')
box.prop(self, 'palette_path')
box.prop(self, 'brush_path')
subbox.prop(self, 'use_env_palettes', text='Use Palettes Environnement Path')
subbox.prop(self, 'palette_path')
subbox.prop(self, 'warn_base_palette')
subbox.prop(self, 'mat_link_exclude')
## Brushes
subbox.prop(self, 'use_env_brushes', text='Use Brushes Environnement Path')
subbox.prop(self, 'brush_path')
## render output
box.prop(self, 'output_path')
box.prop(self, 'use_relative_remap_on_save')
subbox.prop(self, 'output_path')
## namespace
subbox = box.box()
subbox.label(text='Namespace:')
subbox.prop(self, 'separator')
subrow = subbox.row()
subrow.prop(self, 'show_prefix_buttons', text='Use Prefixes Toggles')
if self.show_prefix_buttons:
rowrow = subrow.row()
# Reset Names From Projects
rowrow.alignment = 'RIGHT'
rowrow.operator('gptb.reset_project_namespaces', text='', icon='BRUSH_DATA')
"""
row = subbox.row()
row.prop(self, 'prefixes')
row.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'PREFIXES'
row = subbox.row(align=True)
row.prop(self, 'suffixes')
row.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'SUFFIXES'
"""
## Collection UI list version
self.draw_namespaces_list(subbox, 'GPTB_UL_namespace_list', 'prefixes', rows=4)
subbox.separator()
self.draw_namespaces_list(subbox, 'GPTB_UL_namespace_list_suffix', 'suffixes', rows=2)
### TODO add render settings
@ -352,58 +459,131 @@ class GPTB_prefs(bpy.types.AddonPreferences):
box.label(text='Playblast options:')
box.prop(self, 'playblast_auto_play')
box.prop(self, 'playblast_auto_open_folder')
box.prop(self, 'playblast_path')
# box.separator()## Keyframe jumper
box = layout.box()
box.label(text='Keyframe Jump options:')
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
if self.kfj_use_shortcut:
prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
if self.kfj_prev_keycode:
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_prev_ctrl), ('Shift', self.kfj_prev_shift), ('Alt', self.kfj_prev_alt)] if b])
text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
text = f'Jump Keyframe Prev: {text} (Click to change)'
else:
text = prompt
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
ops.s_keycode = 'kfj_prev_keycode'
ops.s_ctrl = 'kfj_prev_ctrl'
ops.s_shift = 'kfj_prev_shift'
ops.s_alt = 'kfj_prev_alt'
## Keyframe jump now displayed in Shortcut Tab
# box = layout.box()
# box.label(text='Keyframe Jump options:')
if self.kfj_next_keycode:
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
text = f'Jump Keyframe Next: {text} (Click to change)'
else:
text = prompt
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
ops.s_keycode = 'kfj_next_keycode'
ops.s_ctrl = 'kfj_next_ctrl'
ops.s_shift = 'kfj_next_shift'
ops.s_alt = 'kfj_next_alt'
# box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
# if self.kfj_use_shortcut:
# prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
# if self.kfj_prev_keycode:
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_prev_ctrl), ('Shift', self.kfj_prev_shift), ('Alt', self.kfj_prev_alt)] if b])
# text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
# text = f'Jump Keyframe Prev: {text} (Click to change)'
# else:
# text = prompt
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
# ops.s_keycode = 'kfj_prev_keycode'
# ops.s_ctrl = 'kfj_prev_ctrl'
# ops.s_shift = 'kfj_prev_shift'
# ops.s_alt = 'kfj_prev_alt'
else:
box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
# if self.kfj_next_keycode:
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
# text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
# text = f'Jump Keyframe Next: {text} (Click to change)'
# else:
# text = prompt
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
# ops.s_keycode = 'kfj_next_keycode'
# ops.s_ctrl = 'kfj_next_ctrl'
# ops.s_shift = 'kfj_next_shift'
# ops.s_alt = 'kfj_next_alt'
## Active tool
box = layout.box()
box.label(text='Autofix check button options:')
box.prop(self, "select_active_tool", icon='RESTRICT_SELECT_OFF')
box.prop(self, "render_obj_exclusion", icon='FILTER')#
## random color character separator
box = layout.box()
box.label(text='Random color options:')
box.prop(self, 'separator')
# else:
# box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
# box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
box = layout.box()
box.label(text='Tools options:')
box.prop(self, 'use_precise_eraser')
subbox= box.box()
subbox.label(text='Layer Navigation')
col = subbox.column()
col.prop(self, 'nav_use_fade')
if self.nav_use_fade:
row = col.row()
row.prop(self, 'nav_fade_val')
row.prop(self, 'nav_limit')
row = subbox.row(align=False)
row.prop(self, 'nav_use_fade_in')
if self.nav_use_fade_in:
row.prop(self, 'nav_fade_in_time', text='Fade Back Time')
# row.prop(self, 'nav_interval') # Do not expose refresh rate for now, not usefull to user...
# box.prop(self, 'use_precise_eraser') # precise eraser
if self.is_git_repo:
box = layout.box()
box.label(text='Addon Update')
if self.is_git_repo and self.has_git:
box.operator('gptb.git_pull', text='Check / Get Last Update', icon='PLUGIN')
else:
box.label(text='Toolbox can be updated using git')
row = box.row()
row.operator('wm.url_open', text='Download and install git here', icon='URL').url = 'https://git-scm.com/download/'
row.label(text='then restart blender')
if self.pref_tabs == 'KEYS':
# layout.label(text='Shortcuts :')
box = layout.box()
box.label(text='Shortcuts added by GP toolbox with context scope:')
## not available directly :
## keymaps.addon_keymaps <<- one two three on sculpt, not exposed
## OP_temp_cutter # not active by defaut
## TOOL_eraser_brush.addon_keymaps # has a checkbox in
prev_key_category = ''
for kms in [
OP_keyframe_jump.addon_keymaps,
OP_copy_paste.addon_keymaps,
OP_breakdowner.addon_keymaps,
OP_key_duplicate_send.addon_keymaps,
OP_layer_picker.addon_keymaps,
OP_material_picker.addon_keymaps,
OP_layer_nav.addon_keymaps,
# OP_layer_manager.addon_keymaps, # Do not display, wm.call_panel call panel ops mixed with natives shortcut (F2)
]:
ct = 0
for akm, akmi in kms:
km = bpy.context.window_manager.keyconfigs.user.keymaps.get(akm.name)
if not km:
continue
key_category = km.name
# kmi = km.keymap_items.get(akmi.idname) # get only first idname when multiple entry
kmi = None
## numbering hack, need a better way to find multi idname user keymaps
id_ct = 0
for km_item in km.keymap_items:
if km_item.idname == akmi.idname:
if ct > id_ct:
id_ct +=1
continue
kmi = km_item
ct += 1
break
if not kmi:
continue
## show keymap category (ideally grouped by category)
if not prev_key_category:
if key_category:
box.label(text=key_category)
elif key_category and key_category != prev_key_category: # check if has changed singe
box.label(text=key_category)
draw_kmi(km, kmi, box)
prev_key_category = key_category
box.separator()
if self.pref_tabs == 'MAN_OPS':
# layout.separator()## notes
@ -444,25 +624,147 @@ class GPTB_prefs(bpy.types.AddonPreferences):
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.use_property_split = True
# col.prop(self.fixprops, 'check_only')
col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO')
# col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
col.separator()
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_gp_use_lights_off')
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, 'list_obj_vis_conflict')
col.prop(self.fixprops, 'list_gp_mod_vis_conflict')
col.prop(self.fixprops, 'list_broken_mod_targets')
col.prop(self.fixprops, 'autokey_add_n_replace')
col.prop(self.fixprops, 'remove_redundant_strokes')
#-# col.prop(self.fixprops, 'set_cursor_type')
# col = layout.column()
# col.use_property_split = True
col.prop(self.fixprops, "select_active_tool", icon='RESTRICT_SELECT_OFF')
col.prop(self.fixprops, "file_path_type")
col.prop(self.fixprops, "lock_object_mode")
# row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)')
# if self.pref_tabs == 'UPDATE':
# addon_updater_ops.update_settings_ui(self, context)
if self.pref_tabs == 'UPDATE':
addon_updater_ops.update_settings_ui(self, context)
### --- ENV_PROP ---
def set_namespace_env(name_env, prop_group):
tag_list = os.getenv(name_env)
current_pfix = []
project_pfix = []
if tag_list and tag_list.strip():
## Force clear (clear also hide): prop_group.namespaces.clear()
## Get current tag list
tag_list = tag_list.strip(', ').split(',')
current_pfix = [n.tag for n in prop_group.namespaces if n.tag]
# for n in prop_group.namespaces:
# print(n.tag, n.name)
for p in tag_list:
tag = p.split(':')[0].strip()
project_pfix.append(tag)
name = '' if not ':' in p else p.split(':')[1].strip()
item = None
if tag not in current_pfix:
item = prop_group.namespaces.add()
item.tag = tag
item.name = name
# print('Loaded project tag:', tag, name)
elif name:
# get the tag and apply name
item = next((n for n in prop_group.namespaces if n.tag == tag), None)
if item: # and not item.name.strip()
item.name = name
# print('Loaded name:', name)
if item:
item.is_project = True
else:
tag_list = []
# "release" suffix that are not in project anymore
for n in prop_group.namespaces:
n.is_project = n.tag in project_pfix
def set_env_properties():
prefs = get_addon_prefs()
fps = os.getenv('FPS')
prefs.fps = int(fps) if fps else prefs.fps
render_width = os.getenv('RENDER_WIDTH')
prefs.render_res_x = int(render_width) if render_width else prefs.render_res_x
render_height = os.getenv('RENDER_HEIGHT')
prefs.render_res_y = int(render_height) if render_height else prefs.render_res_y
palettes = os.getenv('PALETTES')
if prefs.use_env_palettes:
prefs.palette_path = palettes if palettes else prefs.palette_path
brushes = os.getenv('BRUSHES')
if prefs.use_env_brushes:
prefs.brush_path = brushes if brushes else prefs.brush_path
# if prefs.use_env_namespace:
## Old method with direct string assignment (now a property group)
# prefix_list = os.getenv('PREFIXES')
# prefs.prefixes = prefix_list if prefix_list else prefs.prefixes
# suffix_list = os.getenv('SUFFIXES')
# prefs.suffixes = suffix_list if suffix_list else prefs.suffixes
set_namespace_env('PREFIXES', prefs.prefixes)
set_namespace_env('SUFFIXES', prefs.suffixes)
separator = os.getenv('SEPARATOR')
prefs.separator = separator if separator else prefs.separator
class GPTB_set_env_settings(bpy.types.Operator):
"""manually reset prefs from project environnement setttings"""
bl_idname = "prefs.reset_gp_toolbox_env"
bl_label = "Reset prefs from project environnement settings (if any)"
mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) # 'HIDDEN',
def execute(self, context):
prefs = get_addon_prefs()
if self.mode == 'ALL':
set_env_properties()
elif self.mode == 'PREFIXES':
prefix_list = os.getenv('PREFIXES')
if not prefix_list:
self.report({'ERROR'}, 'No prefix preset to load from project environnement')
return {'CANCELLED'}
set_namespace_env('PREFIXES', prefs.prefixes)
elif self.mode == 'SUFFIXES':
suffix_list = os.getenv('SUFFIXES')
if not suffix_list:
self.report({'ERROR'}, 'No suffix preset to load from project environnement')
return {'CANCELLED'}
set_namespace_env('SUFFIXES', prefs.suffixes)
return {'FINISHED'}
### --- REGISTER ---
@ -472,80 +774,81 @@ class GPTB_prefs(bpy.types.AddonPreferences):
classes = (
GP_PG_FixSettings,
GP_PG_ToolsSettings,
GPTB_set_env_settings,
GPTB_prefs,
GPT_OT_auto_tint_gp_layers,
)
# register, unregister = bpy.utils.register_classes_factory(classes)
addon_modules = (
OP_helpers,
OP_pseudo_tint,
OP_keyframe_jump,
OP_file_checker,
OP_breakdowner,
OP_temp_cutter,
GP_colorize,
OP_playblast_bg,
OP_playblast,
OP_palettes,
OP_palettes_linker,
OP_brushes,
OP_cursor_snap_canvas,
OP_copy_paste,
# OP_flat_reproject # Disabled,
OP_realign,
OP_depth_move,
OP_key_duplicate_send,
OP_layer_namespace,
OP_layer_manager,
OP_material_picker,
OP_git_update,
OP_layer_picker,
OP_layer_nav,
OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam,
UI_tools,
keymaps,
)
def register():
addon_updater_ops.register(bl_info)
# bpy.types.Scene.gpfixprops = bpy.props.PointerProperty(type = GP_PG_FixSettings) # used in prefs
# Register property group first
properties.register()
for cls in classes:
bpy.utils.register_class(cls)
OP_helpers.register()
OP_keyframe_jump.register()
OP_file_checker.register()
OP_breakdowner.register()
OP_temp_cutter.register()
GP_colorize.register()## GP_guided_colorize.
OP_playblast_bg.register()
OP_playblast.register()
OP_palettes.register()
OP_brushes.register()
OP_cursor_snap_canvas.register()
OP_render.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()
keymaps.register()
for mod in addon_modules:
mod.register()
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings)
# add handler (if option is on)
set_env_properties()
if get_addon_prefs().use_relative_remap_on_save:
## add handler (if option is on)
prefs = get_addon_prefs()
if prefs.use_relative_remap_on_save:
if not 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.append(remap_relative)
## Change a variable in prefs if a '.git is detected'
prefs.is_git_repo = (Path(__file__).parent / '.git').exists()
prefs.has_git = bool(which('git'))
def unregister():
# remove handler
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.remove(remap_relative)
keymaps.unregister()
addon_updater_ops.unregister()
for mod in reversed(addon_modules):
mod.unregister()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
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_render.unregister()
OP_cursor_snap_canvas.unregister()
OP_brushes.unregister()
OP_palettes.unregister()
OP_file_checker.unregister()
OP_helpers.unregister()
OP_keyframe_jump.unregister()
OP_breakdowner.unregister()
OP_temp_cutter.unregister()
GP_colorize.unregister()## GP_guided_colorize.
OP_playblast_bg.unregister()
OP_playblast.unregister()
# del bpy.types.Scene.gpfixprops
properties.unregister()
del bpy.types.Scene.gptoolprops

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,7 @@ from random import random as rand
import numpy as np
from bpy_extras.object_utils import world_to_camera_view as cam_space
import bmesh
from .utils import link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
def get_view_origin_position():
#method 1
from bpy_extras import view3d_utils
region = bpy.context.region
rv3d = bpy.context.region_data
view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
print("view_loc1", view_loc)#Dbg
#method 2
r3d = bpy.context.space_data.region_3d
view_loc2 = r3d.view_matrix.inverted().translation
print("view_loc2", view_loc2)#Dbg
if view_loc != view_loc2: print('there might be an errror when finding view coordinate')
return view_loc
from .utils import get_gp_draw_plane, link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
def to_bl_image(array, img):
@ -116,7 +99,8 @@ def gp_stroke_angle_split (frame,strokes,angle):
splitted_loops = bm_angle_split(bm,angle)
frame.strokes.remove(stroke_info['stroke'])
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
frame.drawing.strokes.remove(stroke_info['stroke'])
for loop in splitted_loops :
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
@ -140,6 +124,7 @@ def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
bm_uniform_density(bm,cam,max_spacing)
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
frame.strokes.remove(stroke_info['stroke'])
bm.verts.ensure_lookup_table()
@ -166,7 +151,7 @@ def along_stroke(stroke,attr,length,min,max) :
def randomise_points(mat, points, attr, strength) :
for point in points :
if attr is 'co' :
if attr == 'co' :
random_x = (rand()-0.5)
random_y = (rand()-0.5)
@ -182,7 +167,6 @@ def randomise_points(mat,points,attr,strength) :
setattr(point,attr,value+random*strength)
def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0]
max_x= box[1]
@ -233,27 +217,6 @@ def zoom_to_object(cam,resolution,box,margin=0.01) :
#print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,resolution
def set_viewport_matrix(width,height,mat):
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
glViewport(0,0,width,height)
#glLoadIdentity()
glMatrixMode(GL_PROJECTION)
projection = [mat[j][i] for i in range(4) for j in range(4)]
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
#glMatrixMode( GL_MODELVIEW )
#glLoadIdentity()
# get object info
def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene
@ -379,3 +342,9 @@ def get_object_info(mesh_groups,order_list = []) :
return mesh_info, convert_table
def redraw_ui() -> None:
"""Forces blender to redraw the UI."""
for screen in bpy.data.screens:
for area in screen.areas:
area.tag_redraw()

View File

@ -1,6 +1,5 @@
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
@ -30,22 +29,37 @@ def view3d_camera_border_2d(context, cam):
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px
def vertices_to_line_loop(v_list, closed=True) -> list:
'''Take a sequence of vertices
return a position lists of segments to create a line loop passing in all points
the result is usable with gpu_shader 'LINES'
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
'''
loop = []
for i in range(len(v_list) - 1):
loop += [v_list[i], v_list[i + 1]]
if closed:
# Add segment between last and first to close loop
loop += [v_list[-1], v_list[0]]
return loop
def draw_cam_frame_callback_2d():
context = bpy.context
if context.region_data.view_perspective != 'CAMERA':
return
if context.scene.camera.name != 'draw_cam':
if not context.scene.camera or context.scene.camera.name != 'draw_cam':
return
main_cam = context.scene.camera.parent
if not main_cam:
return
bgl.glEnable(bgl.GL_BLEND)
gpu.state.blend_set('ALPHA')
frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT
@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gpu.state.line_width_set(1.0)
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
"""
## need to accurately detect viewport background color (difficult)
@ -135,15 +149,14 @@ def draw_cam_frame_callback_2d():
frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": frame_point})
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
shader_2d.bind()
shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d)
# bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_LINE_SMOOTH)
bgl.glDisable(bgl.GL_BLEND)
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
gpu.state.blend_set('NONE')
draw_handle = None

8
keymaps.py Normal file → Executable file
View File

@ -8,6 +8,8 @@ def register_keymaps():
# km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
## Sculpt mode toggles
km = addon.keymaps.new(name = "Grease Pencil Stroke Sculpt Mode", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new('wm.context_toggle', type='ONE', value='PRESS')
@ -22,6 +24,12 @@ def register_keymaps():
kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_segment'
addon_keymaps.append((km, kmi))
## T temp cutter (need disabling of native T shortcut, maybe expose a button to set the shortcut as user ?)
# km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY")
# kmi = km.keymap_items.new('gpencil.stroke_cutter', type='LEFTMOUSE', value='PRESS', key_modifier='T')
# kmi.properties.flat_caps=False
# addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)

View File

@ -1,4 +1,5 @@
import bpy
from bpy.types import PropertyGroup
from bpy.props import (
IntProperty,
BoolProperty,
@ -8,7 +9,7 @@ from bpy.props import (
)
from .OP_cursor_snap_canvas import cursor_follow_update
from .OP_layer_manager import layer_name_build
def change_edit_lines_opacity(self, context):
for gp in bpy.data.grease_pencils:
@ -16,7 +17,21 @@ def change_edit_lines_opacity(self, context):
gp.edit_line_color[3]=self.edit_lines_opacity
class GP_PG_FixSettings(bpy.types.PropertyGroup):
def update_layer_name(self, context):
if not self.layer_name:
# never replace by nothing (since there should be prefix/suffix)
return
if not context.object or context.object.type != 'GREASEPENCIL':
return
if not context.object.data.layers.active:
return
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
# context.object.data.layers.active.name = self.layer_name
class GP_PG_FixSettings(PropertyGroup):
lock_main_cam : BoolProperty(
name="Lock Main Cam",
description="Lock the main camera (works only if 'layout' is not in name)",
@ -25,7 +40,7 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
set_scene_res : BoolProperty(
name="Reset Scene Resolution",
description="Set the scene resolution to current prefs project settings",
default=True, options={'HIDDEN'})
default=False, options={'HIDDEN'})
set_res_percentage : BoolProperty(
name="Reset Resolution Percentage To 100%",
@ -42,9 +57,9 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
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)",
set_gp_use_lights_off : BoolProperty(
name="Set Off Use lights On All Gpencil Objects",
description="Uncheck Use lights on all grease pencil objects\nAt object level, not layer level (Object properties > Visibility > GP uselight)",
default=True, options={'HIDDEN'})
check_front_axis : BoolProperty(
@ -65,19 +80,76 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
disable_guide : BoolProperty(
name="Disable Drawing Guide",
description="Disable constrained guide in draw mode",
default=False, options={'HIDDEN'})
default=True, options={'HIDDEN'})
list_disabled_anim : BoolProperty(
name="List Disabled Animation",
description="Alert if there are disabled animations",
default=True, options={'HIDDEN'})
list_obj_vis_conflict : BoolProperty(
name="List Object Visibility Conflicts",
description="Alert if some objects have different hide viewport and hide render values",
default=True, options={'HIDDEN'})
list_gp_mod_vis_conflict : BoolProperty(
name="List GP Object Modifiers Visibility Conflicts",
description="Alert if some GP modifier have different show viewport and show render values",
default=True, options={'HIDDEN'})
list_broken_mod_targets : BoolProperty(
name="List GP Object modifiers broken target",
description="Alert if some GP modifier have a target layer not existing in layer stack",
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):
remove_redundant_strokes : BoolProperty(
name="Remove Redundant Strokes",
description="Remove GP strokes duplication. When points are exactly identical within the same frame",
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'})
## default active tool to use
select_active_tool : EnumProperty(
name="Set Default Selection Tool", description="Active tool to set when launching check fix scene",
default='builtin.select_lasso',
items=(
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
))
## check file path mapping type
file_path_type : EnumProperty(
name="Check File Path Mapping", description="Check if all path in scene are relative, absolute or disable the check",
default='RELATIVE',
items=(
('none', 'No Check', 'Do not check for path type', 0),
('RELATIVE', 'Relative', 'Check if all path are relative', 1),
('ABSOLUTE', 'Absolute', 'Check if all path are absolute', 2),
))
## check set lock object mode
lock_object_mode : EnumProperty(
name="Set Lock Object Mode", description="Set 'Lock object mode' parameter check'",
default='none',
items=(
('none', 'Do nothing', 'Do not set', 0),
('LOCK', 'Lock object mode', 'Toggle lock object mode On', 1),
('UNLOCK', 'Unlock object mode', 'Toggle lock object mode Off', 2),
))
class GP_PG_ToolsSettings(PropertyGroup):
eraser_radius : IntProperty(
name="Eraser Radius", description="Radius of eraser brush",
default=20, min=0, max=500, subtype='PIXEL')
@ -106,9 +178,15 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update)
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)
cursor_follow_target : bpy.props.PointerProperty(
name='Cursor Follow Target',
description="Optional target object to follow for cursor instead of active object",
type=bpy.types.Object, update=cursor_follow_update)
## gpv3 : no edit line color anymore
# 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)
## render
name_for_current_render : StringProperty(
@ -127,6 +205,12 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
))
layer_name : StringProperty(
name="Layer Name",
description="The layer name, should describe the content of the layer",
default="",
update=update_layer_name)# update=None, get=None, set=None
"""
reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)
render_settings = bpy.props.BoolProperty(default = False)
@ -147,3 +231,41 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
stroke_select = bpy.props.EnumProperty(items = [("POINT","Point",""),("STROKE","Stroke","")],update = update_selection_mode)
"""
class GP_PG_namespace_props(PropertyGroup):
tag : StringProperty(
name="Tag", description="Layer namespace tag (prefix/suffix)",
default="")
name : StringProperty(
name="Name", description="Name that represent this prefix (used as hint and tooltip)",
default="")
hide : BoolProperty(
name="Hide", description="Hide this prefix from layer prefix management",
default=False)
is_project : BoolProperty(
name="Project", description="Show this propery was set by project environnement (not deletable if that's the case)",
default=False)
class GP_PG_namespaces(PropertyGroup):
idx : IntProperty(default=-1)
namespaces : bpy.props.CollectionProperty(type=GP_PG_namespace_props)
classes = (
# Prefix/suiffix prefs prop group
GP_PG_namespace_props,
GP_PG_namespaces,
## General toolbox settings
GP_PG_FixSettings,
GP_PG_ToolsSettings,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

1048
utils.py

File diff suppressed because it is too large Load Diff

237
view3d_utils.py Normal file
View File

@ -0,0 +1,237 @@
import bpy
from mathutils import Vector
class Rect:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
@property
def top_left(self):
return Vector((self.x, self.y))
@property
def bottom_left(self):
return Vector((self.x, self.y - self.height))
@property
def bottom_right(self):
return Vector((self.x + self.width, self.y - self.height))
@property
def top_right(self):
return Vector((self.x + self.width, self.y))
def __str__(self):
return f'Rect(x={self.x}, y={self.y}, width={self.width}, height={self.height})'
class View3D:
def __init__(self, area=None):
if area is None:
area = bpy.context.area
if area.type != 'VIEW_3D':
area = next((area for area in bpy.context.screen.areas if area.type == 'VIEW_3D'), None)
self.area = area
@property
def sidebar(self):
return self.area.regions[3]
@property
def toolbar(self):
return self.area.regions[2]
@property
def tool_header(self):
return self.area.regions[1]
@property
def header(self):
return self.area.regions[0]
@property
def space(self):
return self.area.spaces.active
@property
def region(self):
return self.area.regions[5]
@property
def region_3d(self):
return self.space.region_3d
@property
def x(self):
return 0
@property
def y(self):
return 0
@property
def width(self):
return self.region.width
@property
def height(self):
return self.region.height
@property
def rect(self):
return Rect(self.x, self.y, self.width, self.height)
@property
def reduced_rect(self):
w, h = self.region.width, self.region.height
if not bpy.context.preferences.system.use_region_overlap:
return self.rect
## Minus tool leftbar + sidebar right
# top_margin = bottom_margin = 0
# if self.tool_header.alignment == 'TOP':
# top_margin += self.tool_header.height
# else:
# bottom_margin += self.tool_header.height
## Set corner values
# top_left = (self.toolbar.width, h - top_margin - 1)
# top_right = (w - self.sidebar.width, h - top_margin - 1)
# bottom_right = (w - self.sidebar.width, bottom_margin + 2)
# bottom_left = (self.toolbar.width, bottom_margin + 2)
reduced_y = 0
if self.tool_header.alignment == 'TOP':
reduced_y = self.tool_header.height
reduced_width = w - self.sidebar.width - self.toolbar.width
reduced_height = h - self.tool_header.height - 1
return Rect(self.toolbar.width, h - reduced_y - 1, reduced_width, reduced_height)
# return Rect(self.toolbar.width, h - reduced_y - 1, right_down, left_down)
def to_2d(self, coord):
from bpy_extras.view3d_utils import location_3d_to_region_2d
return location_3d_to_region_2d(self.region, self.region_3d, coord)
def to_3d(self, coord, depth_coord=None):
from bpy_extras.view3d_utils import region_2d_to_location_3d
if depth_coord is None:
depth_coord = bpy.context.scene.cursor.location
return region_2d_to_location_3d(self.region, self.region_3d, coord, depth_coord)
def get_camera_frame_3d(self, scene=None, camera=None):
if scene is None:
scene = bpy.context.scene
if camera is None:
camera = scene.camera
frame = camera.data.view_frame()
mat = camera.matrix_world
return [mat @ v for v in frame]
def get_camera_frame_2d(self, scene=None, camera=None):
'''View frame Top_right-CW'''
if scene is None:
scene = bpy.context.scene
frame_3d = self.get_camera_frame_3d(scene=scene, camera=camera)
frame_2d = [self.to_2d(v) for v in frame_3d]
rd = scene.render
resolution_x = rd.resolution_x * rd.pixel_aspect_x
resolution_y = rd.resolution_y * rd.pixel_aspect_y
ratio_x = min(resolution_x / resolution_y, 1.0)
ratio_y = min(resolution_y / resolution_x, 1.0)
## Top right - CW
frame_width = (frame_2d[1].x - frame_2d[2].x) # same size (square)
frame_height = (frame_2d[0].y - frame_2d[1].y) # same size (square)
cam_width = (frame_2d[1].x - frame_2d[2].x) * ratio_x
cam_height = (frame_2d[0].y - frame_2d[1].y) * ratio_y
cam_x = frame_2d[3].x - ((frame_width - cam_width) / 2)
cam_y = frame_2d[3].y - ((frame_height - cam_height) / 2)
return Rect(cam_x, cam_y, cam_width, cam_height)
def zoom_from_fac(self, zoomfac):
from math import sqrt
return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0
def fit_camera_view(self):
## CENTER
self.region_3d.view_camera_offset = (0,0)
## ZOOM
# rect = self.reduced_rect
rect = self.rect
cam_frame = self.get_camera_frame_2d()
# print('width: ', rect.width)
# print('height: ', rect.height)
# xfac = rect.width / (cam_frame.width + 4)
# yfac = rect.height / (cam_frame.height + 4)
# # xfac = rect.width / (rect.width - 4)
# # yfac = rect.height / (rect.height - 4)
scene = bpy.context.scene
rd = scene.render
# resolution_x = rd.resolution_x * rd.pixel_aspect_x
# resolution_y = rd.resolution_y * rd.pixel_aspect_y
# xfac = min(resolution_x / resolution_y, 1.0)
# yfac = min(resolution_y / resolution_x, 1.0)
# xfac = rect.width / (cam_frame.width * rect.width + 4)
# yfac = rect.height / (cam_frame.height * rect.height + 4)
# xfac = rect.width / ((rect.width - cam_frame.width) * 2 + 4)
# yfac = rect.height / (rect.height - cam_frame.height + 4)
# xfac = rect.width / ((rect.width - cam_frame.width * rd.resolution_x) * 2 + 4)
# xfac = rect.width / ((rect.width / cam_frame.width) * rd.resolution_x)
# xfac = rect.width / rd.resolution_x * 2
# xfac = rect.width / ((cam_frame.width / rect.width) * rd.resolution_x)
# xfac = self.region.width / (rect.width - cam_frame.width)
# xfac = self.region.width / (rect.width - cam_frame.width)
# xfac = ((cam_frame.width - rect.width) / rd.resolution_x) * self.region.width
xfac = rect.width / cam_frame.width
# xfac = 0.8
# xfac = yfac = 0.8
# xfac = yfac = 1.0
# print('xfac: ', xfac) # Dbg
# print('yfac: ', yfac) # Dbg
# fac = min([xfac, yfac]) # Dbg
fac = xfac
# fac = rd.resolution_x / self.width
print('fac: ', fac)
print('zoom before', self.region_3d.view_camera_zoom) # Dbg
self.region_3d.view_camera_zoom = self.zoom_from_fac(fac)
print('zoom after', self.region_3d.view_camera_zoom) # Dbg
if __name__ == "__main__":
## construct view 3d class
view3d = View3D()
print(view3d.rect)
print(view3d.rect.bottom_left)
print(view3d.get_camera_frame_3d())
## construct
rect_frame = view3d.get_camera_frame_2d()
view3d.reduced_rect.bottom_left
bpy.context.scene.cursor.location = view3d.to_3d(view3d.reduced_rect.bottom_right)