93 Commits
Author SHA1 Message Date
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
40 changed files with 6738 additions and 4536 deletions
+418 -55
View File
@@ -1,5 +1,368 @@
# Changelog # Changelog
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 1.5.1
- fix: eraser brush - fix: eraser brush
@@ -38,29 +401,29 @@
- feat: show main cam frame when in draw_cam - feat: show main cam frame when in draw_cam
1.3.2: 1.3.2
- change: disable manip cam name drawing - change: disable manip cam name drawing
- code: add initial support for main cam frame draw within camera view - 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. - 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. - 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 - feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
1.2.2: 1.2.2
- fix: realign anim return error - fix: realign anim return error
1.2.1: 1.2.1
- fix: Breakdowner initial error check - fix: Breakdowner initial error check
1.2.0: 1.2.0
- feat: New depth move operator that handle both perspective and orthographic cam - feat: New depth move operator that handle both perspective and orthographic cam
- feat: Realign, added drawing plane checkbox to autoset to Front after realign - feat: Realign, added drawing plane checkbox to autoset to Front after realign
@@ -69,67 +432,67 @@
- doc: Added changelog file (moved list from readme) - doc: Added changelog file (moved list from readme)
- doc: relative link to changelog and FR_readme in main 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 - Important change : Remap relative is now disabled by default in addon preferences
- feat: Add realign operator in sidebar with reproject as true by default - 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 - 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 - 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 - 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) - 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) - 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 - UI: Better cam ref exposition in Toolbox panel
- Access to opacity - Access to opacity
- merge activation bool with source type icon - merge activation bool with source type icon
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps) - 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. - 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 - pref: Added option to disable always remap relative on save in addon-preference
1.0.1: 1.0.1
- fix: copy paste problems - fix: copy paste problems
- Get points uv_properties (used for brushed points) - 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+) - 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 - Compatible with official grease pencil tools
- removed box deform and rotate canvas that existed in other - removed box deform and rotate canvas that existed in other
0.9.3: 0.9.3
- feat: keyframe jump keys are now auto-binded - feat: keyframe jump keys are now auto-binded
- UI: added keyframe jump customisation in addon pref - UI: added keyframe jump customisation in addon pref
- code: split keyframe jump in a separate file with his new key updater - 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 - doc: Correct download link (important, bugged the addon install) + update
- code: added tracker url - code: added tracker url
- updater: remove updater temp file, reset minimum version, turn off verbose mode - updater: remove updater temp file, reset minimum version, turn off verbose mode
0.9.1: 0.9.1
- Public release - Public release
- prefs: added fps as part of project settings - prefs: added fps as part of project settings
@@ -139,38 +502,38 @@
- doc: Added fully-detailed french readme - doc: Added fully-detailed french readme
0.8.0: 0.8.0
- feat: Added background_rendering playblast, derivating from Tonton's playblaster - feat: Added background_rendering playblast, derivating from Tonton's playblaster
- stripped associated properties from properties.py and passed as wm props. - stripped associated properties from properties.py and passed as wm props.
0.7.2: 0.7.2
- fix: Palette importer bug - fix: Palette importer bug
0.7.0: 0.7.0
- feat: auto create empty frame on color layer - 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) - 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 - 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` - Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
- Minor refactor for submodule register - 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 - 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 - feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
0.5.9: 0.5.9
- feat: render exporter - feat: render exporter
- Render a selection of GP object isolated from the rest - Render a selection of GP object isolated from the rest
@@ -180,21 +543,21 @@
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock - 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) - 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 - feat: GP material append on active object from single blend file
0.5.7: 0.5.7
- Added warning message for cursor snapping - 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. - 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 - 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) - 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: - feat: anim manager in his own GP_toolbox submenu:
- button to list disabled anim (allow to quickly check state of the scene) - button to list disabled anim (allow to quickly check state of the scene)
@@ -202,19 +565,19 @@
- shift clic to target selection only - shift clic to target selection only
- check file: added disabled fcurved counter alert with detail in console - 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) - 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) - 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) - Revert back obj_cam operator for following object (native lock view follow only translation)
- Changed method for canvas rotation to more robust rotate axis. - Changed method for canvas rotation to more robust rotate axis.
- Add operators on link checker to open containing folder/file of link - Add operators on link checker to open containing folder/file of link
- Refactor: file checkers in their own file - Refactor: file checkers in their own file
0.5.1: 0.5.1
- fix: error when empty material slot on GP object. - fix: error when empty material slot on GP object.
- fix: cursor snap on GP canvas when GP is parented - fix: cursor snap on GP canvas when GP is parented
@@ -229,11 +592,11 @@
- set show slider and sync range - set show slider and sync range
- set fps to 24 - set fps to 24
0.4.6: 0.4.6
- feat: basic Palette manager with base material check and warning - feat: basic Palette manager with base material check and warning
0.4.5: 0.4.5
- open blender config folder from addon preference - open blender config folder from addon preference
- fix: obj cam parent on selected object - fix: obj cam parent on selected object
@@ -242,29 +605,29 @@
- camview: potential bug when cam is parented with some specific angle (could not reproduce) - 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 - 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 - 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 - feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
- fix: canvas rotate works with parented camera ! - fix: canvas rotate works with parented camera !
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck. - 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) - 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) - 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 onion skin in interface
- UI: expose autolock in interface - UI: expose autolock in interface
@@ -272,47 +635,47 @@
- code: refactor, pushed most of class register in their owner file - 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 - 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: new interface with tabs for addon preferences
- UI: possible to disable color panel from preference (might be deleted if unusable) - UI: possible to disable color panel from preference (might be deleted if unusable)
- docs: change readme changelog format and correct doc - docs: change readme changelog format and correct doc
0.3.6: 0.3.6
- UI: Stoplines : add a button for quickly set stoplines visibility. - 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 : 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. - 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 - 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 - version 1 beta of gmic colorize
- variant of `screen.gp_keyframe_jump` through keymap seetings - 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 - 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) - 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 operator to `screen.gp_keyframe_jump`
- add shortcut to rotate canvas - add shortcut to rotate canvas
- fix duplicate class - fix duplicate class
0.2.2: 0.2.2
- separated props resolution_percentage parameter - separated props resolution_percentage parameter
- playblast options for launching folder and opening folder - playblast options for launching folder and opening folder
0.2.1: 0.2.1
- playblast feature - playblast feature
- Button to go zoom 100% or fit screen - Button to go zoom 100% or fit screen
@@ -320,22 +683,22 @@
- Fix reference panel : works with video and display in a box layout. - 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) - 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: Toggle camera background images from Toolbox panel
- UI: quick access to passepartout - UI: quick access to passepartout
- Feature: option to use namespace for pseudo color - Feature: option to use namespace for pseudo color
0.1.5: 0.1.5
- added CGC-auto-updater - added CGC-auto-updater
0.1.3: 0.1.3
- flip cam x - flip cam x
- inital stage of overlay toggle (need pref/multiple pref) - inital stage of overlay toggle (need pref/multiple pref)
0.1.2: 0.1.2
- subpanel of GP data (instead of direct append) - subpanel of GP data (instead of direct append)
- initial commit with GP pseudo color - initial commit with GP pseudo color
+3 -3
View File
@@ -13,7 +13,7 @@
import bpy import bpy
from . import OP_line_closer # from . import OP_line_closer
from . import OP_create_empty_frames from . import OP_create_empty_frames
@@ -45,11 +45,11 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
OP_create_empty_frames.register() 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) bpy.types.Scene.gpcolor_props = bpy.props.PointerProperty(type = GPCOLOR_PG_settings)
def unregister(): def unregister():
OP_line_closer.unregister() # OP_line_closer.unregister()
OP_create_empty_frames.unregister() OP_create_empty_frames.unregister()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
+134 -12
View File
@@ -1,33 +1,156 @@
## Create empty keyframe where keyframe exists in layers above. ## Create empty keyframe where keyframe exists in layers above.
import bpy import bpy
from bpy.props import (FloatProperty,
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
## copied from OP_key_duplicate_send
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]
class GP_OT_create_empty_frames(bpy.types.Operator): class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames" bl_idname = "gp.create_empty_frames"
bl_label = "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_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_options = {'REGISTER','UNDO'} bl_options = {'REGISTER','UNDO'}
layers_enum : EnumProperty(
name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer",
items=get_layer_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', ''),
)
)
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 @classmethod
def poll(cls, context): 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 == 'GPENCIL'
def invoke(self, context, event):
# Possible preset with shortcut
# if event.alt:
# self.targeted_layers = 'ALL_VISIBLE'
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')
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): def execute(self, context):
obj = context.object obj = context.object
gpl = obj.data.layers gpl = obj.data.layers
gpl.active_index
## Only possible on 'fill' layer ?? print(self.targeted_layers)
# if not 'fill' in gpl.active.info.lower(): if self.targeted_layers == 'ALL_ABOVE':
# self.report({'ERROR'}, f"There must be 'fill' text in layer name") tgt_layers = [l for i, l in enumerate(gpl) if i > gpl.active_index]
# return {'CANCELLED'}
elif self.targeted_layers == 'ALL_BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i < gpl.active_index]
elif self.targeted_layers == 'ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index + 1]
elif self.targeted_layers == 'BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index - 1]
elif self.targeted_layers == 'ALL_VISIBLE':
tgt_layers = [l for l in gpl if not l.hide 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.info == self.layers_enum]
elif self.targeted_layers == 'NUMBER':
if self.number == 0:
self.report({'ERROR'}, f"Can't have 0 as value")
return {'CANCELLED'}
l_range = gpl.active_index + self.number
print('l_range: ', l_range)
if self.number > 0: # positive
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index < i <= l_range]
else:
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index > i >= l_range]
if not tgt_layers:
self.report({'ERROR'}, f"No layers found with chosen Targets")
return {'CANCELLED'}
frame_id_list = [] frame_id_list = []
for i, l in enumerate(gpl): for l in tgt_layers:
# don't list layer below
if i <= gpl.active_index:
continue
# print(l.info, "index:", i)
for f in l.frames: 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.append(f.frame_number)
frame_id_list = list(set(frame_id_list)) frame_id_list = list(set(frame_id_list))
@@ -50,7 +173,6 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
def register(): def register():
bpy.utils.register_class(GP_OT_create_empty_frames) bpy.utils.register_class(GP_OT_create_empty_frames)
+8 -7
View File
@@ -302,8 +302,10 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
### --- KEYMAP --- ### --- KEYMAP ---
breakdowner_addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
if bpy.app.background:
return
# pref = get_addon_prefs() # pref = get_addon_prefs()
# if not pref.breakdowner_use_shortcut: # if not pref.breakdowner_use_shortcut:
# return # return
@@ -320,24 +322,23 @@ def register_keymaps():
if ops_id not in km.keymap_items: if ops_id not in km.keymap_items:
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY 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) 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(): 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) km.keymap_items.remove(kmi)
breakdowner_addon_keymaps.clear() addon_keymaps.clear()
# del breakdowner_addon_keymaps[:]
### --- REGISTER --- ### --- REGISTER ---
def register(): def register():
# if not bpy.app.background:
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim) bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
register_keymaps() register_keymaps()
def unregister(): def unregister():
# if not bpy.app.background:
unregister_keymaps() unregister_keymaps()
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim) bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)
+25 -2
View File
@@ -48,18 +48,41 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
return {"FINISHED"} 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_GPENCIL'
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 -- ### -- MENU ENTRY --
def load_brush_ui(self, context): def load_brush_ui(self, context):
prefs = get_addon_prefs()
if context.mode == 'PAINT_GPENCIL': if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
def load_brush_top_bar_ui(self, context): def load_brush_top_bar_ui(self, context):
prefs = get_addon_prefs()
if context.mode == 'PAINT_GPENCIL': if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes') self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
classes = ( classes = (
GPTB_OT_load_brushes, GPTB_OT_load_brushes,
GPTB_OT_brush_set,
) )
def register(): def register():
+93 -37
View File
@@ -17,7 +17,7 @@ bl_info = {
"name": "GP clipboard", "name": "GP clipboard",
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends", "description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (1, 3, 2), "version": (1, 3, 3),
"blender": (2, 83, 0), "blender": (2, 83, 0),
"location": "View3D > Toolbar > Gpencil > GP clipboard", "location": "View3D > Toolbar > Gpencil > GP clipboard",
"warning": "", "warning": "",
@@ -61,7 +61,8 @@ def getMatrix (layer) :
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,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
'''add properties of a given points to a dic and return it''' '''add properties of a given points to a dic and return it'''
pdic = {} pdic = {}
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type' #point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
@@ -72,26 +73,30 @@ def dump_gp_point(p, l, obj):
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co')) pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
else: else:
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co')) pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
# pdic['select'] = convertAttr(getattr(p,'select')) # need selection ? # pdic['select'] = convertAttr(getattr(p,'select')) # need selection ?
if pressure and p.pressure != 1.0:
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
if strength and p.strength != 1.0:
pdic['strength'] = convertAttr(getattr(p,'strength')) pdic['strength'] = convertAttr(getattr(p,'strength'))
## get vertex color (long...) ## get vertex color (long...)
if p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0): if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
pdic['vertex_color'] = convertAttr(p.vertex_color) pdic['vertex_color'] = convertAttr(p.vertex_color)
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...) ## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
if p.uv_fill != default_pt_uv_fill: if uv_fill and p.uv_fill != default_pt_uv_fill:
pdic['uv_fill'] = convertAttr(p.uv_fill) pdic['uv_fill'] = convertAttr(p.uv_fill)
if p.uv_factor != 0.0: if uv_factor and p.uv_factor != 0.0:
pdic['uv_factor'] = convertAttr(p.uv_factor) pdic['uv_factor'] = convertAttr(p.uv_factor)
if p.uv_rotation != 0.0: if uv_rotation and p.uv_rotation != 0.0:
pdic['uv_rotation'] = convertAttr(p.uv_rotation) pdic['uv_rotation'] = convertAttr(p.uv_rotation)
return pdic return pdic
def dump_gp_stroke_range(s, sid, l, obj): def dump_gp_stroke_range(s, sid, l, obj,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
'''Get a grease pencil stroke and return a dic with attribute '''Get a grease pencil stroke and return a dic with attribute
(points attribute being a dic of dics to store points and their attributes) (points attribute being a dic of dics to store points and their attributes)
''' '''
@@ -129,16 +134,18 @@ def dump_gp_stroke_range(s, sid, l, obj):
points = [] points = []
if sid is None: # no ids, just full points... if sid is None: # no ids, just full points...
for p in s.points: for p in s.points:
points.append(dump_gp_point(p,l,obj)) points.append(dump_gp_point(p,l,obj,
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
else: else:
for pid in sid: for pid in sid:
points.append(dump_gp_point(s.points[pid],l,obj)) points.append(dump_gp_point(s.points[pid],l,obj,
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
sdic['points'] = points sdic['points'] = points
return sdic return sdic
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 copy all visibles selected strokes on active frame
layers can be None, a single layer object or list of layer object as filter layers can be None, a single layer object or list of layer object as filter
@@ -184,7 +191,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
# Cutting operation # Cutting operation
if not copy: if not copy:
maxindex = len(s.points)-1 maxindex = len(s.points)-1
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
f.strokes.remove(s) f.strokes.remove(s)
else: else:
neg = [i for i, p in enumerate(s.points) if not p.select] neg = [i for i, p in enumerate(s.points) if not p.select]
@@ -276,11 +283,11 @@ def copy_all_strokes(layers=None):
return stroke_list 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,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
''' '''
copy all stroke, not affected by selection on active frame 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 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() t0 = time()
scene = bpy.context.scene scene = bpy.context.scene
@@ -310,10 +317,10 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
## full stroke version ## full stroke version
# if s.select: # if s.select:
# send index of all points to get the whole stroke with "range" # 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,
pressure=pressure,strength=strength,vertex_color=vertex_color,uv_fill=uv_fill,uv_factor=uv_factor,uv_rotation=uv_rotation))
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds') # print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
return stroke_list return stroke_list
def add_stroke(s, frame, layer, obj, select=False): def add_stroke(s, frame, layer, obj, select=False):
@@ -368,7 +375,8 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
add a list of strokes to active frame of given layer add a list of strokes to active frame of given layer
if no layer specified, active layer is used if no layer specified, active layer is used
if use_current_frame is True, a new frame will be created only if needed if use_current_frame is True, a new frame will be created only if needed
if select is True, newly added strokes are selected 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 scene = bpy.context.scene
obj = bpy.context.object obj = bpy.context.object
@@ -382,7 +390,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
fnum = scene.frame_current fnum = scene.frame_current
target_frame = False target_frame = False
act = layer.active_frame act = layer.active_frame
for s in stroke_list: ## set frame if needed
if act: if act:
if use_current_frame or act.frame_number == fnum: if use_current_frame or act.frame_number == fnum:
#work on current frame if exists #work on current frame if exists
@@ -394,12 +402,14 @@ 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 #or active exists but not aligned scene.current with use_current_frame disabled
target_frame = layer.frames.new(fnum) target_frame = layer.frames.new(fnum)
for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select) add_stroke(s, target_frame, layer, obj, select=select)
''' '''
for s in stroke_data: for s in stroke_data:
add_stroke(s, target_frame) add_stroke(s, target_frame)
''' '''
print(len(stroke_list), 'strokes pasted')
# print(len(stroke_list), 'strokes pasted')
### OPERATORS ### OPERATORS
@@ -407,7 +417,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
class GPCLIP_OT_copy_strokes(bpy.types.Operator): class GPCLIP_OT_copy_strokes(bpy.types.Operator):
bl_idname = "gp.copy_strokes" bl_idname = "gp.copy_strokes"
bl_label = "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"} bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@@ -424,7 +434,7 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
#ct = check_pressure() #ct = check_pressure()
strokelist = copycut_strokes(copy=True, keep_empty=True) strokelist = copycut_strokes(copy=True, keep_empty=True)
if not strokelist: if not strokelist:
self.report({'ERROR'},'rien a copier') self.report({'ERROR'}, 'Nothing to copy')
return {"CANCELLED"} return {"CANCELLED"}
bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
#if ct: #if ct:
@@ -437,7 +447,7 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
class GPCLIP_OT_cut_strokes(bpy.types.Operator): class GPCLIP_OT_cut_strokes(bpy.types.Operator):
bl_idname = "gp.cut_strokes" bl_idname = "gp.cut_strokes"
bl_label = "GP Cut strokes" bl_label = "GP Cut strokes"
bl_description = "Cut strokes to str in paperclip" bl_description = "Cut strokes to text in paperclip"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@classmethod @classmethod
@@ -493,8 +503,8 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator): class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
bl_idname = "gp.copy_multi_strokes" bl_idname = "gp.copy_multi_strokes"
bl_label = "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_description = "Copy multiple layers>frames>strokes from selected layers to str in paperclip"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@@ -502,6 +512,37 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
pressure : bpy.props.BoolProperty(name='pressure', default=True,
description='Dump point pressure attribute (already skipped if at default value)')
strength : bpy.props.BoolProperty(name='strength', default=True,
description='Dump point strength 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)')
uv_fill : bpy.props.BoolProperty(name='uv fill', default=True,
description='Dump point uv_fill 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)')
uv_rotation : bpy.props.BoolProperty(name='uv rotation', default=True,
description='Dump point uv_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, 'pressure')
col.prop(self, 'strength')
col.prop(self, 'vertex_color')
col.prop(self, 'uv_fill')
col.prop(self, 'uv_factor')
col.prop(self, 'uv_rotation')
return
def execute(self, context): def execute(self, context):
bake_moves = True bake_moves = True
skip_empty_frame = False skip_empty_frame = False
@@ -528,7 +569,9 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
if skip_empty_frame and not len(f.strokes): if skip_empty_frame and not len(f.strokes):
continue continue
context.scene.frame_set(f.frame_number) # use matrix of this frame 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,
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
frame_dic[f.frame_number] = strokelist frame_dic[f.frame_number] = strokelist
@@ -536,6 +579,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
else: # bake position: copy frame where object as moved even if frame is unchanged else: # bake position: copy frame where object as moved even if frame is unchanged
for l in layerpool: for l in layerpool:
print('dump layer:', l.info)
if not l.frames: if not l.frames:
continue# skip empty layers continue# skip empty layers
@@ -562,7 +606,10 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
if skip_empty_frame and not len(f.strokes): if skip_empty_frame and not len(f.strokes):
continue 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,
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
frame_dic[i] = strokelist frame_dic[i] = strokelist
prevmat = curmat prevmat = curmat
@@ -579,8 +626,8 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator): class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
bl_idname = "gp.paste_multi_strokes" bl_idname = "gp.paste_multi_strokes"
bl_label = "GP paste multi strokes" bl_label = "GP Paste Multi Strokes"
bl_description = "Paste multiple layers>frames>strokes from paperclip" bl_description = "Paste multiple layers>frames>strokes from paperclip on active layer"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@@ -615,7 +662,7 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
layer = gpl.new(layname) layer = gpl.new(layname)
for fnum, fstrokes in allframes.items(): 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 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) print('total_time', time() - t0)
@@ -637,13 +684,16 @@ class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row(align=True)
row.operator('gp.copy_strokes', text='Copy strokes', icon='COPYDOWN') col = layout.column(align=True)
row.operator('gp.cut_strokes', text='Cut strokes', icon='PASTEFLIPUP') row = col.row(align=True)
layout.operator('gp.paste_strokes', text='Paste strokes', icon='PASTEDOWN') row.operator('gp.copy_strokes', text='Copy Strokes', icon='COPYDOWN')
layout.separator() row.operator('gp.cut_strokes', text='Cut Strokes', icon='PASTEFLIPUP')
layout.operator('gp.copy_multi_strokes', text='Copy layers', icon='COPYDOWN') col.operator('gp.paste_strokes', text='Paste Strokes', icon='PASTEDOWN')
layout.operator('gp.paste_multi_strokes', text='Paste layers', 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 ###---TEST zone
@@ -712,6 +762,9 @@ GPCLIP_PT_clipboard_ui,
) )
def register(): def register():
if bpy.app.background:
return
for cl in classes: for cl in classes:
bpy.utils.register_class(cl) bpy.utils.register_class(cl)
@@ -719,6 +772,9 @@ def register():
register_keymaps() register_keymaps()
def unregister(): def unregister():
if bpy.app.background:
return
unregister_keymaps() unregister_keymaps()
for cl in reversed(classes): for cl in reversed(classes):
bpy.utils.unregister_class(cl) bpy.utils.unregister_class(cl)
+1 -1
View File
@@ -48,7 +48,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
if warning: if warning:
self.report({'WARNING'}, ', '.join(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 if not plane_co:#default to object location
plane_co = context.object.matrix_world.to_translation()#context.object.location plane_co = context.object.matrix_world.to_translation()#context.object.location
+465 -126
View File
@@ -1,20 +1,41 @@
import bpy import bpy
import os import os
from pathlib import Path from pathlib import Path
from .utils import show_message_box, get_addon_prefs import numpy as np
from . import utils
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.strokes):
point_list = [p.co for p in s.points]
if point_list in stroke_list:
ct += 1
if apply:
# Remove redundancy
f.strokes.remove(s)
else:
stroke_list.append(point_list)
return ct
class GPTB_OT_file_checker(bpy.types.Operator): class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker" 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_description = "Check / correct some aspect of the file, properties and such and report"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
# @classmethod ## list of actions :
# def poll(cls, context): # Lock main cam
# return context.region_data.view_perspective == 'CAMERA'
## list of action :
# Lock main cam:
# set scene res # set scene res
# set scene percentage at 100: # set scene percentage at 100:
# set show slider and sync range # set show slider and sync range
@@ -23,13 +44,30 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# GP use additive drawing (else creating a frame in dopesheet makes it blank...) # GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# GP stroke placement/projection check # GP stroke placement/projection check
# Disabled animation # Disabled animation
# Objects visibility conflict
# Objects modifiers visibility conflict
# GP modifiers broken target check
# Set onion skin filter to 'All type' # 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
def invoke(self, context, event):
# need some self-control (I had to...)
self.ctrl = event.ctrl
return self.execute(context)
def execute(self, context): def execute(self, context):
prefs = get_addon_prefs() prefs = utils.get_addon_prefs()
fix = prefs.fixprops fix = prefs.fixprops
problems = [] problems = []
apply = not fix.check_only
# If Ctrl is pressed, invert behavior (invert boolean)
apply ^= self.ctrl
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
@@ -44,6 +82,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
triple = (True,True,True) triple = (True,True,True)
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple: if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
problems.append('Lock main camera') problems.append('Lock main camera')
if apply:
cam.lock_location = cam.lock_rotation = triple cam.lock_location = cam.lock_rotation = triple
## set scene res at pref res according to addon pref ## set scene res at pref res according to addon pref
@@ -52,18 +91,21 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# TODO set (rx, ry) to camera resolution if specified in camera name # TODO set (rx, ry) to camera resolution if specified in camera name
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry: if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{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 context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
## set scene percentage at 100: ## set scene percentage at 100:
if fix.set_res_percentage: if fix.set_res_percentage:
if context.scene.render.resolution_percentage != 100: if context.scene.render.resolution_percentage != 100:
problems.append('Resolution output to 100%') problems.append('Resolution output to 100%')
if apply:
context.scene.render.resolution_percentage = 100 context.scene.render.resolution_percentage = 100
## set fps according to preferences settings ## set fps according to preferences settings
if fix.set_fps: if fix.set_fps:
if context.scene.render.fps != prefs.fps: if context.scene.render.fps != prefs.fps:
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') ) problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
if apply:
context.scene.render.fps = prefs.fps context.scene.render.fps = prefs.fps
## set show slider and sync range ## set show slider and sync range
@@ -74,18 +116,17 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if area.type == 'DOPESHEET_EDITOR': if area.type == 'DOPESHEET_EDITOR':
if hasattr(area.spaces[0], 'show_sliders'): if hasattr(area.spaces[0], 'show_sliders'):
setattr(area.spaces[0], 'show_sliders', True) setattr(area.spaces[0], 'show_sliders', True)
if hasattr(area.spaces[0], 'show_locked_time'): if hasattr(area.spaces[0], 'show_locked_time'):
setattr(area.spaces[0], 'show_locked_time', True) setattr(area.spaces[0], 'show_locked_time', True)
## set cursor type (according to prefs ?) ## set cursor type
if fix.set_cursor_type:
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"): if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = prefs.select_active_tool tool = fix.select_active_tool
if tool != 'none': if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool: 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]}') 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...) # ## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# if not context.scene.tool_settings.use_gpencil_draw_additive: # if not context.scene.tool_settings.use_gpencil_draw_additive:
@@ -102,6 +143,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN': if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)') problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
## GP Use light disable
if fix.set_gp_use_lights_off:
gp_with_lights = [o for o in context.scene.objects if o.type == 'GPENCIL' 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 ## Disabled animation
if fix.list_disabled_anim: if fix.list_disabled_anim:
@@ -114,24 +163,109 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fcu_ct += 1 fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}") print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct: if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details -> console)') problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict
if fix.list_obj_vis_conflict:
viz_ct = 0
for o in context.scene.objects:
if o.hide_viewport != o.hide_render:
vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}')
if viz_ct:
problems.append(['gp.list_object_visibility', 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:
if o.type == 'GPENCIL':
for m in o.grease_pencil_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} - GP modifier {m.name}: viewport {vp} != render {rd}')
else:
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 == 'GPENCIL']:
lay_name_list = [l.info for l in o.data.layers]
for m in o.grease_pencil_modifiers:
if not hasattr(m, 'layer'):
continue
if m.layer != '' and not m.layer in lay_name_list:
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer}'
print(mess)
problems.append(mess)
## Use median point ## Use median point
if fix.set_pivot_median_point: if fix.set_pivot_median_point:
if context.scene.tool_settings.transform_pivot_point != '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'") 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' context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
if fix.disable_guide: if fix.disable_guide:
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True: if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
problems.append(f"Disabled Draw Guide") problems.append(f"Disabled Draw Guide")
if apply:
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
if fix.autokey_add_n_replace: if fix.autokey_add_n_replace:
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS': if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
problems.append(f"Autokey mode reset to 'Add & Replace'") problems.append(f"Autokey mode reset to 'Add & Replace'")
if apply:
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS' 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' # ## Set onion skin filter to 'All type'
# fix_kf_type = 0 # fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data # for gp in bpy.data.grease_pencils:#from data
@@ -154,13 +288,208 @@ class GPTB_OT_file_checker(bpy.types.Operator):
print(p) print(p)
else: else:
print(p[0]) print(p[0])
# Show in viewport # Show in viewport
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO') title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
return {"FINISHED"} return {"FINISHED"}
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"}
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
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):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text=self.title)
if self.broke_ct:
layout.label(text="You can try to scan for missing files:")
## How to launch directly without filebrowser ?
# in Shot folder
layout.operator('file.find_missing_files', text='in parent hierarchy').directory = Path(bpy.data.filepath).parents[1].as_posix()
if self.proj:
# In Library
layout.operator('file.find_missing_files', text='in library').directory = (Path(self.proj)/'library').as_posix()
# In all project
layout.operator('file.find_missing_files', text='in all project (last resort)').directory = self.proj
layout.separator()
# layout = layout.column() # thinner linespace
for l in self.all_lnks:
if l[1] == 'CANCEL':
layout.label(text=l[0], icon=l[1])
continue
if l[1] == 'LIBRARY_DATA_BROKEN':
split=layout.split(factor=0.85)
split.label(text=l[0], icon=l[1])
# 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 = []
self.title = ''
self.broke_ct = 0
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)):
# 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') )
else:
if realib.as_posix().startswith('//'):
rel_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )
else:
abs_ct += 1
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')
return {"FINISHED"}
bct = f"{self.broke_ct} broken " if self.broke_ct else ''
act = f"{abs_ct} absolute " if abs_ct else ''
rct = f"{rel_ct} clean " if rel_ct else ''
self.title = f"{bct}{act}{rct}"
self.all_lnks.sort(key=lambda x: x[1], reverse=True)
if self.all_lnks:
print('===File check===')
for p in self.all_lnks:
if isinstance(p, str):
print(p)
else:
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"}
popup_width = 800
if maxlen < 50:
popup_width = 500
elif maxlen > 100:
popup_width = 1000
self.proj = os.environ.get('PROJECT_ROOT')
return context.window_manager.invoke_props_dialog(self, width=popup_width)
""" OLD links checker with show_message_box """ OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator): class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker" bl_idname = "gp.links_checker"
@@ -193,130 +522,140 @@ class GPTB_OT_links_checker(bpy.types.Operator):
else: else:
print(p[0]) print(p[0])
# Show in viewport # Show in viewport
show_message_box(all_lnks, _title = "Links", _icon = 'INFO') utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """ return {"FINISHED"} """
class GPTB_OT_links_checker(bpy.types.Operator): class GPTB_OT_list_object_visibility(bpy.types.Operator):
bl_idname = "gp.links_checker" bl_idname = "gp.list_object_visibility"
bl_label = "Links check" bl_label = "List Object Visibility Conflicts"
bl_description = "Check states of file direct links" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
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): def execute(self, context):
return {"FINISHED"} return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = []
# viz_ct = 0
# for o in context.scene.objects:
# if o.hide_viewport != o.hide_render:
# vp = 'No' if o.hide_viewport else 'Yes'
# rd = 'No' if o.hide_render else 'Yes'
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}')
# if li:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found')
# else:
# self.report({'INFO'}, f"No Object visibility conflict on current scene")
# return {'FINISHED'}
## Only GP modifier
'''
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List GP 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 o.type != 'GPENCIL':
continue
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods])
mods.append(m)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.label(text=self.title) for o in self.ob_list:
if self.broke_ct: layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
layout.label(text="You can try to scan for missing files:") for m in o[1]:
row = layout.row()
## How to launch directly without filebrowser ? row.label(text='')
# in Shot folder row.label(text=m.name, icon='MODIFIER_ON')
layout.operator('file.find_missing_files', text='in parent hierarchy').directory = Path(bpy.data.filepath).parents[1].as_posix() row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
if self.proj: row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
# In Library
layout.operator('file.find_missing_files', text='in library').directory = (Path(self.proj)/'library').as_posix()
# In all project
layout.operator('file.find_missing_files', text='in all project (last resort)').directory = self.proj
layout.separator()
for l in self.all_lnks:
if l[1] == 'LIBRARY_DATA_BROKEN':
layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.75)
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])))
def invoke(self, context, event):
self.all_lnks = []
self.title = ''
self.broke_ct = 0
abs_ct = 0
rel_ct = 0
## 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():
self.broke_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
rel_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
abs_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
if not self.all_lnks:
self.report({'INFO'}, 'No external links in files')
return {"FINISHED"}
bct = f"{self.broke_ct} broken " if self.broke_ct else ''
act = f"{abs_ct} absolute " if abs_ct else ''
rct = f"{rel_ct} clean " if rel_ct else ''
self.title = f"{bct}{act}{rct}"
self.all_lnks.sort(key=lambda x: x[1], reverse=True)
if self.all_lnks:
print('===File check===')
for p in self.all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
# 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)
'''### 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"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context): def execute(self, context):
## check scene resolution / 100% / framerate return {'FINISHED'}
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 ?
## 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
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
## disable autolock
context.scene.tool_settings.lock_object_mode = False
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 o.type == 'GPENCIL':
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, 'OUTLINER_OB_GREASEPENCIL'])
mods.append(m)
else:
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 = ( classes = (
# GPTB_OT_check_scene, GPTB_OT_list_object_visibility,
GPTB_OT_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard,
GPTB_OT_file_checker, GPTB_OT_file_checker,
GPTB_OT_links_checker, GPTB_OT_links_checker,
) )
+160
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)
"""
## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject
:all_stroke: affect hided, 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.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.info} : {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.strokes:
for p in s.points:
p.co = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.co), 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.strokes:
points_co = [obj.matrix_world @ p.co 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.co)
# p.co = 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.active_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.strokes:
points_co = [obj.matrix_world @ p.co 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 == 'GPENCIL'
def execute(self, context):
for o in context.selected_objects:
if o.type != 'GPENCIL' 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_GPENCIL':
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_GPENCIL' 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_gpencil_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_gpencil_edit_context_menu.remove(flat_reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+215
View File
@@ -0,0 +1,215 @@
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 == 'GPENCIL':
bpy.ops.object.mode_set(mode='PAINT_GPENCIL', 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
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)
+353 -49
View File
@@ -1,12 +1,19 @@
import bpy import bpy
import mathutils
import math
from time import ctime
from mathutils import Vector #, Matrix from mathutils import Vector #, Matrix
from pathlib import Path from pathlib import Path
from math import radians 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_idname = "wm.copytext"
bl_label = "Copy to clipboard" bl_label = "Copy To Clipboard"
bl_description = "Insert passed text to clipboard" bl_description = "Insert passed text to clipboard"
bl_options = {"REGISTER", "INTERNAL"} bl_options = {"REGISTER", "INTERNAL"}
@@ -18,23 +25,53 @@ class GPTB_OT_copy_text(bpy.types.Operator):
self.report({'INFO'}, mess) self.report({'INFO'}, mess)
return {"FINISHED"} return {"FINISHED"}
class GPTB_OT_flipx_view(bpy.types.Operator): class GPTB_OT_flipx_view(Operator):
bl_idname = "gp.mirror_flipx" bl_idname = "view3d.camera_mirror_flipx"
bl_label = "cam mirror flipx" bl_label = "Cam Mirror Flipx"
bl_description = "Invert X scale on camera to flip image horizontally" bl_description = "Invert X scale on camera to flip image horizontally"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@classmethod @classmethod
def poll(cls, context): 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): def execute(self, context):
context.scene.camera.scale.x *= -1 context.scene.camera.scale.x *= -1
return {"FINISHED"} 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_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_description = "Rename the GP datablock with the same name as the object"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@@ -113,9 +150,9 @@ def get_gp_alignement_vector(context):
elif orient == 'CURSOR': elif orient == 'CURSOR':
return Vector((0,0,1))#.rotate(context.scene.cursor.matrix) 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_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_description = "switch between main camera and draw (manipulate) camera"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@@ -170,7 +207,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
# dcam_col = bpy.data.collections.get(camcol_name) # dcam_col = bpy.data.collections.get(camcol_name)
# if not dcam_col: # if not dcam_col:
set_collection(drawcam, camcol_name) utils.set_collection(drawcam, camcol_name)
# Swap to it, unhide if necessary and hide previous # Swap to it, unhide if necessary and hide previous
context.scene.camera = maincam context.scene.camera = maincam
@@ -188,7 +225,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
if not drawcam: if not drawcam:
created=True created=True
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data) drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
set_collection(drawcam, 'manip_cams') utils.set_collection(drawcam, 'manip_cams')
if dcam_name == 'draw_cam': if dcam_name == 'draw_cam':
drawcam.parent = maincam drawcam.parent = maincam
@@ -197,10 +234,13 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
drawcam.lock_location = (True,True,True) drawcam.lock_location = (True,True,True)
# drawcam.hide_viewport = True # drawcam.hide_viewport = True
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout 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 maincam.data.show_passepartout = False
else: else:
# object cam
if created: if created:
drawcam['maincam_name'] = context.scene.camera.name drawcam['maincam_name'] = context.scene.camera.name
drawcam.parent = act drawcam.parent = act
@@ -238,15 +278,16 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
return {"FINISHED"} 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_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_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@classmethod @classmethod
def poll(cls, context): 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') # return context.scene.camera and not context.scene.camera.name.startswith('Cam')
def execute(self, context): def execute(self, context):
@@ -279,9 +320,9 @@ class GPTB_OT_set_view_as_cam(bpy.types.Operator):
return {"FINISHED"} 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_idname = "gp.reset_cam_rot"
bl_label = "Reset rotation" bl_label = "Reset Rotation"
bl_description = "Reset rotation of the draw manipulation camera" bl_description = "Reset rotation of the draw manipulation camera"
bl_options = {"REGISTER"} 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.scene.camera and not context.scene.camera.name.startswith('Cam')
# return context.region_data.view_perspective == 'CAMERA'# check if in camera # return context.region_data.view_perspective == 'CAMERA'# check if in camera
def execute(self, context): def get_center_view(self, context, cam):
# dcam_name = 'draw_cam' from bpy_extras.view3d_utils import location_3d_to_region_2d
# camcol_name = 'manip_cams' frame = cam.data.view_frame()
drawcam = context.scene.camera mat = cam.matrix_world
if drawcam.parent.type == 'CAMERA': frame = [mat @ v for v in frame]
## align to parent camera frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
# drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
elif drawcam.parent: return mathutils.Vector((center_x, center_y))
## there is a parent, so align the Y of the camera to object's Z
# drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong def get_ui_ratio(self, context):
pass '''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: 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") self.report({'ERROR'}, "No parents to refer to for rotation reset")
return {"CANCELLED"} 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"} 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_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_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False) mute : bpy.props.BoolProperty(default=False)
skip_gp : bpy.props.BoolProperty(default=False) mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL
skip_obj : bpy.props.BoolProperty(default=False)
def invoke(self, context, event): def invoke(self, context, event):
self.selection = event.shift self.selection = event.shift
return self.execute(context) 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: if self.selection:
pool = context.selected_objects pool = context.selected_objects
else: else:
pool = context.scene.objects pool = context.scene.objects
for o in pool: for o in pool:
if self.skip_gp and o.type == 'GPENCIL': if self.mode == 'GPENCIL' and o.type != 'GPENCIL':
continue continue
if self.skip_obj and o.type != 'GPENCIL': if self.mode == 'OBJECT' and o.type in ('GPENCIL', 'CAMERA'):
continue continue
if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue
# mute attribute animation for GP and cameras
if o.type in ('GPENCIL', '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: if not o.animation_data:
continue continue
@@ -343,15 +447,45 @@ class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
if not act: if not act:
continue continue
for i, fcu in enumerate(act.fcurves): print(f'\n---{o.name}:')
print(i, fcu.data_path, fcu.array_index) self.set_action_mute(act)
fcu.mute = self.mute
return {'FINISHED'} 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 != 'GPENCIL':
continue
for m in o.grease_pencil_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_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_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@@ -376,21 +510,37 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
# continue # continue
# if self.skip_obj and o.type != 'GPENCIL': # if self.skip_obj and o.type != 'GPENCIL':
# continue # continue
if o.type == 'GPENCIL':
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: if not o.animation_data:
continue continue
act = o.animation_data.action act = o.animation_data.action
if not act: if not act:
continue continue
for g in act.groups:
if g.mute:
li.append(f'{o.name} - group: {g.name}')
for i, fcu in enumerate(act.fcurves): for i, fcu in enumerate(act.fcurves):
# print(i, fcu.data_path, fcu.array_index) # print(i, fcu.data_path, fcu.array_index)
if fcu.mute: if fcu.mute:
if o not in oblist: if o not in oblist:
oblist.append(o) oblist.append(o)
li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}') li.append(f'{o.name}:')
else: li.append(f' - {fcu.data_path} {fcu.array_index}')
li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}')
if li: if li:
show_message_box(li) utils.show_message_box(li)
else: else:
self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}") self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}")
return {'FINISHED'} 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 ?) ## 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_idname = "gp.overlay_presets"
bl_label = "Overlay presets" bl_label = "Overlay presets"
bl_description = "Overlay save/load presets for showing only whats needed" 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'} 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_idname = "gp.clear_active_frame"
bl_label = "Clear Active Frame" bl_label = "Clear Active Frame"
bl_description = "Delete all strokes in active frames" bl_description = "Delete all strokes in active frames"
@@ -485,16 +635,170 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator):
return {'FINISHED'} 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 == 'GPENCIL'# 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 == 'GPENCIL'
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 = ( classes = (
GPTB_OT_copy_text, GPTB_OT_copy_text,
GPTB_OT_flipx_view, GPTB_OT_flipx_view,
GPTB_OT_view_camera_frame_fit,
GPTB_OT_rename_data_from_obj, GPTB_OT_rename_data_from_obj,
GPTB_OT_draw_cam, GPTB_OT_draw_cam,
GPTB_OT_set_view_as_cam, GPTB_OT_set_view_as_cam,
GPTB_OT_reset_cam_rot, GPTB_OT_reset_cam_rot,
GPTB_OT_toggle_mute_animation, GPTB_OT_toggle_mute_animation,
GPTB_OT_toggle_hide_gp_modifier,
GPTB_OT_list_disabled_anims, GPTB_OT_list_disabled_anims,
GPTB_OT_clear_active_frame, GPTB_OT_clear_active_frame,
GPTB_OT_check_canvas_alignement,
GPTB_OT_step_select_frames,
GPTB_OT_open_addon_prefs,
) )
def register(): def register():
+18 -7
View File
@@ -4,6 +4,10 @@ from bpy.types import Operator
def get_layer_list(self, context): def get_layer_list(self, context):
'''return (identifier, name, description) of enum content''' '''return (identifier, name, description) of enum content'''
if not context:
return [('None', 'None','None')]
if not context.object:
return [('None', 'None','None')]
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active] return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
# try: # try:
# except: # except:
@@ -21,14 +25,16 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
layers_enum : bpy.props.EnumProperty( layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers", name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer", description="Duplicate selected keys in active layer and send them to choosen layer",
items=get_layer_list items=get_layer_list,
options={'HIDDEN'},
) )
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod @classmethod
def poll(cls, context): 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 == 'GPENCIL'\
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) # history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
@@ -107,11 +113,12 @@ def register_keymaps():
# if not pref.kfj_use_shortcut: # if not pref.kfj_use_shortcut:
# return # return
addon = bpy.context.window_manager.keyconfigs.addon 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") 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)) 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 kmi.properties.delete_source = True
addon_keymaps.append((km,kmi)) addon_keymaps.append((km,kmi))
@@ -127,13 +134,17 @@ GPTB_OT_duplicate_send_to_layer,
) )
def register(): def register():
if not bpy.app.background: if bpy.app.background:
return
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
register_keymaps() register_keymaps()
def unregister(): def unregister():
if not bpy.app.background: if bpy.app.background:
return
unregister_keymaps() unregister_keymaps()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
+4 -3
View File
@@ -171,13 +171,14 @@ def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Screen", space_type = "EMPTY") 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 = 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 kmi.properties.next = False
addon_keymaps.append((km, kmi)) 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(): def unregister_keymaps():
# print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg # print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg
for km, kmi in addon_keymaps: for km, kmi in addon_keymaps:
+840
View File
@@ -0,0 +1,840 @@
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
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.info
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.info = 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.grease_pencil_modifiers:
if not hasattr(mod, 'layer'):
continue
if mod.layer == old:
mod.layer = 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.info
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.info = 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:
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"}
def grp_toggle(l, mode='TOGGLE'):
'''take mode in (TOGGLE, GROUP, UNGROUP) '''
grp_item_id = ' - '
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info)
if not res and mode in ('TOGGLE', 'GROUP'):
# No gpr : add group prefix after stripping all space and dash
l.info = grp_item_id + l.info.lstrip(' -')
elif res and mode in ('TOGGLE', 'UNGROUP'):
# found : delete group prefix
l.info = res.group(2)
class GPTB_OT_layer_group_toggle(Operator):
bl_idname = "gp.layer_group_toggle"
bl_label = "Group Toggle"
bl_description = "Group or ungroup a layer"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
# group : StringProperty(default='', options={'SKIP_SAVE'})
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
for l in gpl:
if l.select or l == act:
grp_toggle(l)
return {"FINISHED"}
class GPTB_OT_layer_new_group(Operator):
bl_idname = "gp.layer_new_group"
bl_label = "New Group"
bl_description = "Create a group from active layer"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
res = re.search(PATTERN, act.info)
if not res:
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
return {"CANCELLED"}
name = res.group('name').strip(' -')
if not name:
self.report({'ERROR'}, f'No name found in {act.info}')
return {"CANCELLED"}
if name in [l.info.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.info}')
return {"FINISHED"}
grp_toggle(act, mode='GROUP')
n = gpl.new(name, set_active=False)
n.use_onion_skinning = n.use_lights = False
n.hide = True
n.opacity = 0
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 == 'GPENCIL']
else:
pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
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.info.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 == 'GPENCIL'
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.info}')
res = re.search(PATTERN, act.info)
if not res:
self.report({'ERROR'}, f'Error scanning {act.info}')
return {"CANCELLED"}
namespace = res.group('tag')
if not namespace:
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
return {"CANCELLED"}
if self.mode == 'SELECT':
## with split
# namespace = act.info.split(sep,1)[0]
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
## with reg # only active
# namespace_bool_list = [l.info.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.info.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.info.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 == 'GPENCIL'
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.info}')
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.info, 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.info != 'background'
else:
lays = [l for l in gpl] # exclude : if l.info != 'background'
ct = 0
for l in lays:
old = l.info
if regex:
new = re.sub(target, replacement, l.info)
if old != new:
l.info = new
print('rename:', old, '-->', new)
ct += 1
continue
if prefix_only:
if not sep in l.info:
# only if separator exists
continue
splited = l.info.split(sep)
prefix = splited[0]
new_prefix = prefix.replace(target, replacement)
if prefix != new_prefix:
splited[0] = new_prefix
l.info = sep.join(splited)
print('rename:', old, '-->', l.info)
ct += 1
else:
new = l.info.replace(target, replacement)
if old != new:
l.info = 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 == 'GPENCIL'
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_gpencil_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_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', '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 != 'GPENCIL':
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.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 != 'GPENCIL':
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.info.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')
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'},
)
@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, 'info', 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 == 'GPENCIL'
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 == 'GPENCIL'
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 Stroke 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_layer_group_toggle,
GPTB_OT_layer_new_group,
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_gpencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GPENCIL_MT_layer_context_menu.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.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# delete layer index trigger
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
+205
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
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_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
if self.direction == 'UP':
utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info')
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_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
trigger = True
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
_val = utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info')
# utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'info')#layers
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 Stroke 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
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
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 == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
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)
lid = self.point_pair[index][1]
return lid
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.info}')
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 gp.use_multiedit:
for layer_id, l in enumerate(gp.layers):
if l.hide:# l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.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.co), 0)), layer_id) for p in s.points]
else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for layer_id, l in enumerate(gp.layers):
if l.hide or not l.active_frame:# l.lock or
continue
for s in l.active_frame.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.co), 0)), layer_id) for p in s.points]
if not self.point_pair:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'}
lid = self.filter_stroke(context)
if isinstance(lid, str):
self.report({'ERROR'}, lid)
return {'CANCELLED'}
del self.point_pair # auto garbage collected ?
self.ob.data.layers.active_index = lid
## 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.info}')
return {'FINISHED'}
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Stroke 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)
+342
View File
@@ -0,0 +1,342 @@
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
### 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 == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def filter_stroke(self, context):
# get stroke under mouse using kdtree
point_pair = [(p.co, 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.co == 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 self.gp.use_multiedit:
for l in self.gp.layers:
if l.hide:# l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.strokes:
self.stroke_list.append(s)
else:
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for l in self.gp.layers:
if l.hide or not l.active_frame:# l.lock or
continue
for s in l.active_frame.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].co
self.init_pos = [p.co.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].co = 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 == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
# 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.co, 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.co == 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 gp.use_multiedit:
for l in gp.layers:
if l.hide:# l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.strokes:
self.stroke_list.append(s)
else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for l in gp.layers:
if l.hide or not l.active_frame:# l.lock or
continue
for s in l.active_frame.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].co
# self.init_pos = [p.co.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 Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", 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)
+4 -5
View File
@@ -5,7 +5,6 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path from pathlib import Path
from .utils import convert_attr, get_addon_prefs from .utils import convert_attr, get_addon_prefs
### --- Json serialized material load/save ### --- Json serialized material load/save
def load_palette(context, filepath): def load_palette(context, filepath):
@@ -130,6 +129,8 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
dic = {} dic = {}
allmat=[] allmat=[]
for mat in ob.data.materials: for mat in ob.data.materials:
if not mat:
continue
if not mat.is_grease_pencil: if not mat.is_grease_pencil:
continue continue
if mat in allmat: if mat in allmat:
@@ -165,9 +166,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
def load_blend_palette(context, filepath): def load_blend_palette(context, filepath):
'''Load materials on current active object from current chosen blend''' '''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} --') print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects: for ob in context.selected_objects:
if ob.type != 'GPENCIL': if ob.type != 'GPENCIL':
@@ -460,7 +459,6 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
classes = ( classes = (
GPTB_OT_load_palette, GPTB_OT_load_palette,
GPTB_OT_save_palette, GPTB_OT_save_palette,
@@ -468,6 +466,7 @@ GPTB_OT_load_default_palette,
GPTB_OT_load_blend_palette, GPTB_OT_load_blend_palette,
GPTB_OT_copy_active_to_selected_palette, GPTB_OT_copy_active_to_selected_palette,
GPTB_OT_clean_material_stack, GPTB_OT_clean_material_stack,
) )
def register(): def register():
+581
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 == 'GPENCIL']
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 != 'GPENCIL':
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
+49 -2
View File
@@ -40,6 +40,8 @@ exclude = (
'audio_bitrate', 'audio_bitrate',
] ]
""" """
'''
def render_with_restore(): def render_with_restore():
class RenderFileRestorer: class RenderFileRestorer:
rd = bpy.context.scene.render rd = bpy.context.scene.render
@@ -80,11 +82,51 @@ def render_with_restore():
return RenderFileRestorer() 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): def playblast(viewport = False, stamping = True):
scn = bpy.context.scene scn = bpy.context.scene
res_factor = scn.gptoolprops.resolution_percentage res_factor = scn.gptoolprops.resolution_percentage
playblast_path = get_addon_prefs().playblast_path
rd = scn.render rd = scn.render
ff = rd.ffmpeg ff = rd.ffmpeg
with render_with_restore(): 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) # mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
blend = Path(bpy.data.filepath) blend = Path(bpy.data.filepath)
date_format = "%Y-%m-%d_%H-%M-%S" 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) #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.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options #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) # bpy.ops.render.render_wrap(use_view=viewport)
### render ### render
+52 -12
View File
@@ -22,13 +22,18 @@ exclude = (
) )
def delete_file(filepath): def delete_file(filepath):
fp = Path(filepath)
if fp.exists() and fp.is_file():
try: try:
if os.path.isfile(filepath) : print('removing', fp)
print('removing', filepath) fp.unlink(missing_ok=False)
os.remove(filepath) # os.remove(fp)
return True return True
except PermissionError: 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 return False
# render function # render function
@@ -58,7 +63,7 @@ def render_function(cmd, total_frame, scene) :
frame_count += 1 frame_count += 1
try : try :
# print('frame_count: ', frame_count, 'total_frame: ', total_frame) # 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 : except AttributeError :
#debug #debug
if debug : print("AttributeError avoided") if debug : print("AttributeError avoided")
@@ -231,9 +236,7 @@ class BGBLAST_OT_playblast_modal_check(bpy.types.Operator):
self.report({'INFO'}, "Render Finished") self.report({'INFO'}, "Render Finished")
"""
### classic sauce
def render_with_restore(): def render_with_restore():
class RenderFileRestorer: class RenderFileRestorer:
rd = bpy.context.scene.render rd = bpy.context.scene.render
@@ -266,6 +269,38 @@ def render_with_restore():
print(e) print(e)
return RenderFileRestorer() 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): def playblast(context, viewport = False, stamping = True):
@@ -274,6 +309,7 @@ def playblast(context, viewport = False, stamping = True):
rd = scn.render rd = scn.render
ff = rd.ffmpeg ff = rd.ffmpeg
playblast_path = get_addon_prefs().playblast_path
prefix = 'tempblast_' prefix = 'tempblast_'
# delete old playblast and blend files # 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 # rd.is_movie_format# check if its movie mode
## set filepath ## set filepath
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name) # mode incremental 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" 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) #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.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options #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 # get total number of frames
+8 -3
View File
@@ -1,8 +1,6 @@
from .utils import get_gp_objects, get_gp_datas, get_addon_prefs
import bpy 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): def get_hue_by_name(name, offset=0):
''' '''
@@ -127,3 +125,10 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
def invoke(self, context, event): def invoke(self, context, event):
self.autotint_offset = context.scene.gptoolprops.autotint_offset self.autotint_offset = context.scene.gptoolprops.autotint_offset
return self.execute(context) 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)
+75 -18
View File
@@ -4,7 +4,8 @@ from mathutils import Matrix, Vector
from math import pi from math import pi
import numpy as np import numpy as np
from time import time from time import time
from . import utils
from mathutils.geometry import intersect_line_plane
def get_scale_matrix(scale): def get_scale_matrix(scale):
# recreate a neutral mat scale # recreate a neutral mat scale
@@ -21,27 +22,75 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
if restore_frame: if restore_frame:
oframe = bpy.context.scene.frame_current oframe = bpy.context.scene.frame_current
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
frame_list = list(set(frame_list))
frame_list.sort()
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 l in obj.data.layers:
if not all_strokes:
if not l.select:
continue
if l.hide or l.lock:
continue
f = next((f for f in l.frames if f.frame_number == i), None)
if f is None:
continue
for s in f.strokes:
## 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.co 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)
# for i, p in enumerate(s.points):
# p.co = obj.matrix_world.inverted() @ new_world_co_3d[i]
## Ravel new coordinate on the fly
new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
## Set points in obj local space (apply matrix slower)
# new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
s.points.foreach_set('co', new_local_coords)
bpy.context.area.tag_redraw()
'''
## Old method using Operators:
omode = bpy.context.mode omode = bpy.context.mode
# FIXME : if all_stroke is False, might be better to still store>set>restore "lock_frame"
if all_strokes: if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers] layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers: for l in obj.data.layers:
l.hide = False l.hide = False
l.lock = False l.lock = False
l.lock_frame = False l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT_GPENCIL') bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
for l in obj.data.layers:
for f in l.frames:
if not len(f.strokes):
continue
bpy.context.scene.frame_set(f.frame_number)
# switch to edit to reproject through ops for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT') bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT') bpy.ops.gpencil.select_all(action='DESELECT')
# restore # restore
@@ -52,6 +101,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
layer.lock_frame = lock_frame layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode) bpy.ops.object.mode_set(mode=omode)
'''
if restore_frame: if restore_frame:
bpy.context.scene.frame_current = oframe bpy.context.scene.frame_current = oframe
@@ -127,7 +177,7 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler' chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
## double list keys ## 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 ## normal iter
# for fcu in o.animation_data.action.fcurves: # for fcu in o.animation_data.action.fcurves:
@@ -232,7 +282,7 @@ class GPTB_OT_realign(bpy.types.Operator):
def invoke(self, context, event): def invoke(self, context, event):
if context.object.data.use_multiedit: if context.object.data.use_multiedit:
self.report({'ERROR'}, 'Does not work in Multi-edit') self.report({'ERROR'}, 'Does not work in Multiframe mode')
return {"CANCELLED"} return {"CANCELLED"}
self.alert = '' self.alert = ''
@@ -320,14 +370,15 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
description='Hided and locked layer will also be reprojected') description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type', type: bpy.props.EnumProperty(name='Type',
items=(('FRONT', "Front", ""), items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""),
('SIDE', "Side", ""), ('SIDE', "Side", ""),
('TOP', "Top", ""), ('TOP', "Top", ""),
('VIEW', "View", ""), ('VIEW', "View", ""),
('SURFACE', "Surface", ""), ('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""), ('CURSOR', "Cursor", ""),
), ),
default='FRONT') default='CURRENT')
def invoke(self, context, event): def invoke(self, context, event):
if context.object.data.use_multiedit: if context.object.data.use_multiedit:
@@ -338,15 +389,19 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
if not context.region_data.view_perspective == 'CAMERA': if not context.region_data.view_perspective == 'CAMERA':
layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') # layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera, not current view', icon='ERROR')
layout.prop(self, "all_strokes") layout.prop(self, "all_strokes")
layout.prop(self, "type") layout.prop(self, "type")
def execute(self, context): def execute(self, context):
t0 = time() 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)' ) self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
@@ -356,11 +411,13 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
def reproject_clean_menu(self, context): def reproject_clean_menu(self, context):
if context.mode == 'EDIT_GPENCIL': if context.mode == 'EDIT_GPENCIL':
self.layout.operator('gp.batch_reproject_all_frames', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC 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): def reproject_context_menu(self, context):
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE': 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 self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
classes = ( classes = (
GPTB_OT_realign, GPTB_OT_realign,
-482
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() """
-4
View File
@@ -151,10 +151,6 @@ def register():
bpy.utils.register_class(GPTB_OT_sticky_cutter) bpy.utils.register_class(GPTB_OT_sticky_cutter)
# register_keymaps() # register_keymaps()
def unregister(): def unregister():
if not bpy.app.background: if not bpy.app.background:
# unregister_keymaps() # unregister_keymaps()
+68 -3
View File
@@ -6,7 +6,10 @@ Blender addon - Various tool to help with grease pencil in animation productions
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[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 +32,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) 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 ### 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 ### function
@@ -71,7 +103,7 @@ Store strokes in os'clipboard (easier cross blend copy)
cutting is use a more user friendly (leave boundary points of left strokes untouched). cutting is use a more user friendly (leave boundary points of left strokes untouched).
Also Possible to copy whole selected layers. 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** **Palette management**
@@ -84,6 +116,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) - 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 ? ### Where ?
Panel in sidebar : 3D view > sidebar 'N' > Gpencil Panel in sidebar : 3D view > sidebar 'N' > Gpencil
+58 -36
View File
@@ -6,12 +6,27 @@ Blender addon - Boîte à outils de grease pencil pour la production d'animation
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[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 ## 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 ### Exposition dans l'UI de fonction native
@@ -23,7 +38,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`) - 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 - Onion skin - coche des overlays
- autolock layer - coche du sous-menu de l'UI list des layers - 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é - 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. - liste de boutons pour activer/désactiver les background de caméra (tous ou individuellement) avec icone par type.
@@ -55,6 +70,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). 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: **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 - Lock main cam
- set scene res to def project res (specified in addon prefs) - set scene res to def project res (specified in addon prefs)
- set scene percentage at 100: - set scene percentage at 100:
@@ -64,7 +82,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 use additive drawing (else creating a frame in dopesheet makes it blank...)
- GP stroke placement/projection check (just warn if your not in 'Front') - GP stroke placement/projection check (just warn if your not in 'Front')
- Warn if there are some disabled animation (and list datapath) - 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) 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. > **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 +106,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é) ### À 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). <!-- **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. --> 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 ## 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). **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).
Le hack est très simple mais aide beaucoup à fermer les contour pour éviter le leak de l'outils pot de peinture. 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
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.
## tools supplémentaires ## 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: 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.) <!-- 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... 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 - 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) (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) - 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) - 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 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) - 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)
- Meilleure table lumineuse (grosse réflexion et travail en perspective)
--- ---
+505 -132
View File
@@ -1,14 +1,18 @@
from . import addon_updater_ops # from . import addon_updater_ops
from .utils import get_addon_prefs from .utils import (get_addon_prefs,
anim_status,
gp_modifier_status,
)
import bpy import bpy
from pathlib import Path from pathlib import Path
from bpy.types import Panel
## UI in properties ## UI in properties
### dataprop_panel not used --> transferred to sidebar ### 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_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
# bl_space_type = 'VIEW_3D' # bl_space_type = 'VIEW_3D'
@@ -35,8 +39,8 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
## UI in Gpencil sidebar menu ## UI in Gpencil sidebar menu
class GPTB_PT_sidebar_panel(bpy.types.Panel): class GPTB_PT_sidebar_panel(Panel):
bl_label = "Toolbox" bl_label = "GP Toolbox"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Gpencil" bl_category = "Gpencil"
@@ -44,23 +48,23 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
# layout.use_property_split = True # layout.use_property_split = True
prefs = get_addon_prefs()
rd = context.scene.render rd = context.scene.render
# check for update # check for update
addon_updater_ops.check_for_update_background() # addon_updater_ops.check_for_update_background()
# layout.label(text='View options:') # layout.label(text='View options:')
## flip X cam col = layout.column()
if context.scene.camera and context.scene.camera.scale.x < 0:
# layout.label(text='! Flipped !')
row = layout.row(align=True)
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.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') row.label(text='',icon='LOOP_BACK')
else:
layout.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
## draw/manipulation camera ## draw/manipulation camera
col = layout.column()
if context.scene.camera and context.scene.camera.name.startswith(('draw', 'obj')): if context.scene.camera and context.scene.camera.name.startswith(('draw', 'obj')):
row = col.row(align=True) row = col.row(align=True)
row.operator('gp.draw_cam_switch', text = 'Main cam', icon = 'OUTLINER_OB_CAMERA') row.operator('gp.draw_cam_switch', text = 'Main cam', icon = 'OUTLINER_OB_CAMERA')
@@ -86,6 +90,9 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
else: else:
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN') row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
row.prop(context.scene.camera.data, 'passepartout_alpha', text='') row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
# row = layout.row(align=True)
# row.operator('view3d.view_camera_frame_fit', text = 'Custom fit', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
row = layout.row(align=True) row = layout.row(align=True)
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER') row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER')
@@ -122,21 +129,19 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
## Straight line ops from official greasepencil_tools addon if enabled. ## 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 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() 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 ## Options
layout.separator() col = layout.column()
layout.label(text = 'Options:') col.label(text = 'Options:')
## Kf Jump filter ## Kf Jump filter
layout.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
col = layout.column()
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used # col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
if context.object and context.object.type == 'GPENCIL': if context.object and context.object.type == 'GPENCIL':
# col.prop(context.object.data, 'use_autolock_layers') # not often used # 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 ## rename datablock temporary layout
if context.object.name != context.object.data.name: if context.object.name != context.object.data.name:
@@ -147,6 +152,7 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True
## Check base palette ## 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")): 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 = col.box()
box.label(text='Missing base material setup', icon='INFO') box.label(text='Missing base material setup', icon='INFO')
@@ -155,38 +161,18 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
else: else:
col.label(text='No GP object selected') col.label(text='No GP object selected')
col.prop(context.scene.gptoolprops, 'edit_lines_opacity') col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
row = col.row(align=True)
## realign / reproject
row.operator('gp.realign', icon='AXIS_FRONT')
## move in depth
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
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)
# Mention update as notice # Mention update as notice
addon_updater_ops.update_notice_box_ui(self, context) # addon_updater_ops.update_notice_box_ui(self, context)
# row = layout.row(align=False) # row = layout.row(align=False)
# row.label(text='arrow choice') # 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_LEFT').left = 1
# row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0 # row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0
class GPTB_PT_anim_manager(bpy.types.Panel): class GPTB_PT_anim_manager(Panel):
bl_label = "Animation manager" bl_label = "Animation Manager"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Gpencil" bl_category = "Gpencil"
@@ -196,38 +182,101 @@ class GPTB_PT_anim_manager(bpy.types.Panel):
# def draw_header(self,context): # def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="") # 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 ('GPENCIL', 'CAMERA')]
# gps = [o for o in context.scene.objects if o.type == 'GPENCIL']
# 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 ('GPENCIL', 'CAMERA'):
objs.append(o)
elif o.type == 'GPENCIL':
gps.append(o)
elif o.type == 'CAMERA':
cams.append(o)
# print(f'{time.perf_counter() - t0:.8f}s')
return {'OBJECT': objs, 'GPENCIL': gps, 'CAMERA': cams}
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
col = layout.column()
## Animation enable disable anim (shift click to select) OP_helpers.GPTB_OT_toggle_mute_animation ## Animation enable disable anim (shift click to select) OP_helpers.GPTB_OT_toggle_mute_animation
layout.operator('gp.list_disabled_anims') obj_types = self.get_object_by_types(context)
## Objs ()
row = layout.row(align=True) col.operator('gp.list_disabled_anims')
row.label(text='Obj anims:')
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH' ## Show Enable / Disable anims
ops.skip_gp = True for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GPENCIL')]:
ops.skip_obj = False 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.mute = False
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH' ops = row.operator('gp.toggle_mute_animation', text='OFF', icon=off_icon)
ops.skip_gp = True ops.mode = cat_type
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.mute = True 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['GPENCIL'])
# 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)
class GPTB_PT_toolbox_playblast(Panel):
bl_label = "Playblast" bl_label = "Playblast"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
@@ -249,18 +298,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 = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
row.operator('render.playblast_anim', text = 'Viewport').use_view = True row.operator('render.playblast_anim', text = 'Viewport').use_view = True
class GPTB_PT_tint_layers(bpy.types.Panel): class GPTB_PT_tint_layers(Panel):
bl_label = "Tint layers" bl_label = "Tint Layers"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Gpencil" bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel" bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context): # def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="") # self.layout.prop(context.scene.camera.data, "show_background_images", text="")
@@ -270,51 +315,82 @@ class GPTB_PT_tint_layers(bpy.types.Panel):
## pseudo color layers ## pseudo color layers
# layout.separator() # layout.separator()
col = layout.column(align = True) col = layout.column(align = True)
row = col.split(align=False, factor=0.63) # row = col.split(align=False, factor=0.63)
row.prop(context.scene.gptoolprops, 'autotint_offset') # row = col.row()
row.prop(context.scene.gptoolprops, 'autotint_namespace') 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", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True 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_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Gpencil" bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel" bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context): # def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="") # self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator('render.render_anim', text = 'Render invividually', icon = 'RENDERLAYERS').mode = 'INDIVIDUAL'#RENDER_STILL #RESTRICT_RENDER_OFF col = layout.column()
layout.operator('render.render_anim', text = 'Render grouped', icon = 'IMAGE_RGB').mode = 'GROUP' 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() ## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
row = layout.row() ## check drawing alignement
row.prop(context.scene.gptoolprops, 'name_for_current_render', text = 'Output name')#icon = 'OUTPUT' col.operator('gp.check_canvas_alignement', icon='DRIVER_ROTATIONAL_DIFFERENCE')
row.operator('render.use_active_object_name', text = '', icon='OUTLINER_DATA_GP_LAYER')#icon = 'OUTPUT'
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS ## File checker
row = col.row(align=True)
blend = bpy.data.filepath row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
if blend: row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
blend = Path(blend)
out = blend.parents[1] / "compo" / "base"
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
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) ## 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_label = "Background imgs"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
@@ -340,7 +416,6 @@ class GPTB_PT_cam_ref_panel(bpy.types.Panel):
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'} row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
""" """
def palette_manager_menu(self, context): def palette_manager_menu(self, context):
"""Palette menu to append in existing menu""" """Palette menu to append in existing menu"""
# GPENCIL_MT_material_context_menu # GPENCIL_MT_material_context_menu
@@ -350,28 +425,356 @@ def palette_manager_menu(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
layout.operator("gp.copy_active_to_selected_palette", text='Append Materials To Selected', icon='MATERIAL') 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.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_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_GPENCIL').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 == 'GPENCIL' 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 = ( classes = (
GPTB_PT_sidebar_panel, GPTB_PT_sidebar_panel,
GPTB_PT_checker,
GPTB_PT_anim_manager, GPTB_PT_anim_manager,
GPTB_PT_toolbox_playblast, GPTB_PT_color,
GPTB_PT_tint_layers, GPTB_PT_tint_layers,
GPTB_PT_render, GPTB_PT_toolbox_playblast,
## GPTB_PT_cam_ref_panel, # 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(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu) 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(): def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu) 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): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
@@ -392,33 +795,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", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True 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")
"""
+502 -201
View File
@@ -1,22 +1,11 @@
# This program is free software; you can redistribute it and/or modify # SPDX-License-Identifier: GPL-2.0-or-later
# 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/>.
bl_info = { bl_info = {
"name": "GP toolbox", "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", "author": "Samuel Bernou, Christophe Seux",
"version": (1, 5, 1), "version": (2, 5, 0),
"blender": (2, 91, 0), "blender": (3, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox", "doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
@@ -24,10 +13,10 @@ bl_info = {
"category": "3D View", "category": "3D View",
} }
from . import addon_updater_ops from pathlib import Path
from shutil import which
from .utils import * from sys import modules
from .functions import * from .utils import get_addon_prefs, draw_kmi
## GMIC ## GMIC
from .GP_guided_colorize import GP_colorize from .GP_guided_colorize import GP_colorize
@@ -41,31 +30,46 @@ from . import OP_helpers
from . import OP_keyframe_jump from . import OP_keyframe_jump
from . import OP_cursor_snap_canvas from . import OP_cursor_snap_canvas
from . import OP_palettes from . import OP_palettes
from . import OP_palettes_linker
from . import OP_brushes from . import OP_brushes
from . import OP_file_checker from . import OP_file_checker
from . import OP_render
from . import OP_copy_paste from . import OP_copy_paste
from . import OP_realign from . import OP_realign
from . import OP_flat_reproject
from . import OP_depth_move from . import OP_depth_move
from . import OP_key_duplicate_send from . import OP_key_duplicate_send
from . import OP_eraser_brush from . import OP_layer_manager
from . import TOOL_eraser_brush 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_eraser_brush
# from . import TOOL_eraser_brush
from . import handler_draw_cam from . import handler_draw_cam
from . import keymaps from . import keymaps
from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers
from . import UI_tools from . import UI_tools
from .properties import GP_PG_ToolsSettings, GP_PG_FixSettings from .properties import (
GP_PG_ToolsSettings,
GP_PG_FixSettings,
GP_PG_namespaces,
)
from bpy.props import (FloatProperty, from bpy.props import (FloatProperty,
BoolProperty, BoolProperty,
EnumProperty, EnumProperty,
StringProperty, StringProperty,
IntProperty) IntProperty,
PointerProperty
)
import bpy import bpy
import os
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from pathlib import Path from pathlib import Path
# from .eyedrop import EyeDropper # from .eyedrop import EyeDropper
@@ -100,75 +104,42 @@ def remap_on_save_update(self, context):
bpy.app.handlers.save_pre.remove(remap_relative) bpy.app.handlers.save_pre.remove(remap_relative)
## precise eraser
def update_use_precise_eraser(self, context): # def update_use_precise_eraser(self, context):
# km, kmi = TOOL_eraser_brush.addon_keymaps[0]
km, kmi = TOOL_eraser_brush.addon_keymaps[0] # kmi.active = self.use_precise_eraser
kmi.active = self.use_precise_eraser
class GPTB_prefs(bpy.types.AddonPreferences): class GPTB_prefs(bpy.types.AddonPreferences):
bl_idname = __name__ bl_idname = __name__
use_precise_eraser : BoolProperty( ## precise eraser
name='Precise Eraser', # use_precise_eraser : BoolProperty(
default=False, # name='Precise Eraser',
update=update_use_precise_eraser # default=False,
) # update=update_use_precise_eraser)
## tabs ## tabs
pref_tabs : EnumProperty( pref_tabs : EnumProperty(
items=(('PREF', "Preferences", "Change some preferences of the modal"), items=(('PREF', "Preferences", "Change some preferences of the modal"),
('MAN_OPS', "Operator", "Operator to add Manually"), ('KEYS', "Shortcuts", "Customize addon shortcuts"),
# ('TUTO', "Tutorial", "How to use the tool"), ('MAN_OPS', "Operators", "Operator to add Manually"),
('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"), ('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"), # ('KEYMAP', "Keymap", "customise the default keymap"),
), ),
default='PREF') 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 ## addon prefs
## Project preferences #--# PROJECT PREFERENCES #--#
# subtype (string) Enumerator in ['FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE']. # 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 ## fps
use_relative_remap_on_save : BoolProperty( use_relative_remap_on_save : BoolProperty(
@@ -200,21 +171,77 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Path relative to blend to place render", description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH') default="//render", maxlen=0, subtype='DIR_PATH')
separator : StringProperty( playblast_path : StringProperty(
name="Namespace separator", name="Playblast Path",
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified", description="Path to folder for playblasts output",
default="_", maxlen=0, subtype='NONE') 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( palette_path : StringProperty(
name="Palettes directory", name="Palettes directory",
description="Path to palette containing palette.json files to save and load", description="Path to palette containing palette.json files to save and load",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path default="", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
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( brush_path : StringProperty(
name="Brushes directory", name="Brushes directory",
description="Path to brushes containing the blends holding the brushes", description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path 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 prefs
playblast_auto_play : BoolProperty( playblast_auto_play : BoolProperty(
name="Playblast auto play", name="Playblast auto play",
@@ -228,19 +255,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=False, 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 settings
render_obj_exclusion : StringProperty( render_obj_exclusion : StringProperty(
name="GP obj exclude filter", name="GP obj exclude filter",
@@ -311,12 +325,65 @@ class GPTB_prefs(bpy.types.AddonPreferences):
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings) 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.35, 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
# temp_cutter_use_shortcut: BoolProperty( # temp_cutter_use_shortcut: BoolProperty(
# name = "Use temp cutter Shortcut", # name = "Use temp cutter Shortcut",
# description = "Auto assign shortcut for temp_cutter", # description = "Auto assign shortcut for temp_cutter",
# default = True) # 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): def draw(self, context):
layout = self.layout## random color layout = self.layout## random color
# layout.use_property_split = True # layout.use_property_split = True
@@ -336,14 +403,53 @@ class GPTB_prefs(bpy.types.AddonPreferences):
row.label(text='Render Resolution') row.label(text='Render Resolution')
row.prop(self, 'render_res_x', text='Width') row.prop(self, 'render_res_x', text='Width')
row.prop(self, 'render_res_y', text='Height') row.prop(self, 'render_res_y', text='Height')
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 ## Palette
box.label(text='Project folders:') subbox.prop(self, 'use_env_palettes', text='Use Palettes Environnement Path')
box.prop(self, 'palette_path') subbox.prop(self, 'palette_path')
box.prop(self, 'brush_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 ## render output
box.prop(self, 'output_path') subbox.prop(self, 'output_path')
box.prop(self, 'use_relative_remap_on_save')
## 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 ### TODO add render settings
@@ -352,58 +458,131 @@ class GPTB_prefs(bpy.types.AddonPreferences):
box.label(text='Playblast options:') box.label(text='Playblast options:')
box.prop(self, 'playblast_auto_play') box.prop(self, 'playblast_auto_play')
box.prop(self, 'playblast_auto_open_folder') box.prop(self, 'playblast_auto_open_folder')
box.prop(self, 'playblast_path')
# box.separator()## Keyframe jumper # box.separator()## Keyframe jumper
box = layout.box()
box.label(text='Keyframe Jump options:')
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts') ## Keyframe jump now displayed in Shortcut Tab
if self.kfj_use_shortcut: # box = layout.box()
prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]' # box.label(text='Keyframe Jump options:')
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'
if self.kfj_next_keycode: # box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b]) # if self.kfj_use_shortcut:
text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode # prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
text = f'Jump Keyframe Next: {text} (Click to change)' # if self.kfj_prev_keycode:
else: # 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 = prompt # text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH') # text = f'Jump Keyframe Prev: {text} (Click to change)'
ops.s_keycode = 'kfj_next_keycode' # else:
ops.s_ctrl = 'kfj_next_ctrl' # text = prompt
ops.s_shift = 'kfj_next_shift' # ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
ops.s_alt = 'kfj_next_alt' # 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: # if self.kfj_next_keycode:
box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR") # mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor") # 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 # else:
box = layout.box() # box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
box.label(text='Autofix check button options:') # box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
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')
box = layout.box() box = layout.box()
box.label(text='Tools options:') 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': if self.pref_tabs == 'MAN_OPS':
# layout.separator()## notes # layout.separator()## notes
@@ -444,25 +623,147 @@ class GPTB_prefs(bpy.types.AddonPreferences):
if self.pref_tabs == 'CHECKS': if self.pref_tabs == 'CHECKS':
layout.label(text='Following checks will be made when clicking "Check File" button:') layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column() col = layout.column()
# row = col.row() col.use_property_split = True
col.prop(self.fixprops, 'check_only')
col.label(text='If dry run is checked, no modification is done', icon='INFO')
col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1')
col.separator()
col.prop(self.fixprops, 'lock_main_cam') 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_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_res_percentage')
col.prop(self.fixprops, 'set_fps', text=f'Reset FPS (to {self.fps})') 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_slider_n_sync')
col.prop(self.fixprops, 'set_cursor_type')
col.prop(self.fixprops, 'check_front_axis') col.prop(self.fixprops, 'check_front_axis')
col.prop(self.fixprops, 'check_placement') 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, 'set_pivot_median_point')
col.prop(self.fixprops, 'disable_guide') col.prop(self.fixprops, 'disable_guide')
col.prop(self.fixprops, 'list_disabled_anim') 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, '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)') # 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 --- ### --- REGISTER ---
@@ -472,80 +773,80 @@ class GPTB_prefs(bpy.types.AddonPreferences):
classes = ( classes = (
GP_PG_FixSettings, GPTB_set_env_settings,
GP_PG_ToolsSettings,
GPTB_prefs, 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,
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_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam,
UI_tools,
keymaps,
)
def register(): def register():
addon_updater_ops.register(bl_info) # Register property group first
# bpy.types.Scene.gpfixprops = bpy.props.PointerProperty(type = GP_PG_FixSettings) # used in prefs properties.register()
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
OP_helpers.register()
OP_keyframe_jump.register() for mod in addon_modules:
OP_file_checker.register() mod.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()
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings) 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]: if not 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.append(remap_relative) 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(): def unregister():
# remove handler
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]: if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.remove(remap_relative) bpy.app.handlers.save_pre.remove(remap_relative)
keymaps.unregister() for mod in reversed(addon_modules):
addon_updater_ops.unregister() mod.unregister()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
UI_tools.unregister()
handler_draw_cam.unregister() properties.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
del bpy.types.Scene.gptoolprops del bpy.types.Scene.gptoolprops
-1673
View File
File diff suppressed because it is too large Load Diff
-1503
View File
File diff suppressed because it is too large Load Diff
+8 -21
View File
@@ -6,24 +6,7 @@ from random import random as rand
import numpy as np import numpy as np
from bpy_extras.object_utils import world_to_camera_view as cam_space from bpy_extras.object_utils import world_to_camera_view as cam_space
import bmesh import bmesh
from .utils import link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping from .utils import get_gp_draw_plane, 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
def to_bl_image(array, img): def to_bl_image(array, img):
@@ -166,7 +149,7 @@ def along_stroke(stroke,attr,length,min,max) :
def randomise_points(mat, points, attr, strength) : def randomise_points(mat, points, attr, strength) :
for point in points : for point in points :
if attr is 'co' : if attr == 'co' :
random_x = (rand()-0.5) random_x = (rand()-0.5)
random_y = (rand()-0.5) random_y = (rand()-0.5)
@@ -252,8 +235,6 @@ def set_viewport_matrix(width,height,mat):
# get object info # get object info
def get_object_info(mesh_groups, order_list = []) : def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene scene = bpy.context.scene
@@ -379,3 +360,9 @@ def get_object_info(mesh_groups,order_list = []) :
return mesh_info, convert_table 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()
+1 -1
View File
@@ -34,7 +34,7 @@ def draw_cam_frame_callback_2d():
context = bpy.context context = bpy.context
if context.region_data.view_perspective != 'CAMERA': if context.region_data.view_perspective != 'CAMERA':
return return
if context.scene.camera.name != 'draw_cam': if not context.scene.camera or context.scene.camera.name != 'draw_cam':
return return
main_cam = context.scene.camera.parent main_cam = context.scene.camera.parent
Regular → Executable
+8
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 = "3D View", space_type = "VIEW_3D")# in 3D context
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere # 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') 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') 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' kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_segment'
addon_keymaps.append((km, kmi)) 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(): def unregister_keymaps():
for km, kmi in addon_keymaps: for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
+129 -9
View File
@@ -1,4 +1,5 @@
import bpy import bpy
from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
IntProperty, IntProperty,
BoolProperty, BoolProperty,
@@ -8,7 +9,7 @@ from bpy.props import (
) )
from .OP_cursor_snap_canvas import cursor_follow_update from .OP_cursor_snap_canvas import cursor_follow_update
from .OP_layer_manager import layer_name_build
def change_edit_lines_opacity(self, context): def change_edit_lines_opacity(self, context):
for gp in bpy.data.grease_pencils: for gp in bpy.data.grease_pencils:
@@ -16,7 +17,25 @@ def change_edit_lines_opacity(self, context):
gp.edit_line_color[3]=self.edit_lines_opacity 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 != 'GPENCIL':
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.info = self.layer_name
class GP_PG_FixSettings(PropertyGroup):
check_only : BoolProperty(
name="Dry run mode (Check only)",
description="Do not change anything, just print the messages",
default=False, options={'HIDDEN'})
lock_main_cam : BoolProperty( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
description="Lock the main camera (works only if 'layout' is not in name)", description="Lock the main camera (works only if 'layout' is not in name)",
@@ -25,7 +44,7 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
set_scene_res : BoolProperty( set_scene_res : BoolProperty(
name="Reset Scene Resolution", name="Reset Scene Resolution",
description="Set the scene resolution to current prefs project settings", description="Set the scene resolution to current prefs project settings",
default=True, options={'HIDDEN'}) default=False, options={'HIDDEN'})
set_res_percentage : BoolProperty( set_res_percentage : BoolProperty(
name="Reset Resolution Percentage To 100%", name="Reset Resolution Percentage To 100%",
@@ -42,9 +61,9 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
description="Toggle on the use of show slider and sync range", description="Toggle on the use of show slider and sync range",
default=True, options={'HIDDEN'}) default=True, options={'HIDDEN'})
set_cursor_type : BoolProperty( set_gp_use_lights_off : BoolProperty(
name="Set Select Cursor Mode", name="Set Off Use lights On All Gpencil Objects",
description="Set the type of the selection cursor (according to addon prefs)", description="Uncheck Use lights on all grease pencil objects\nAt object level, not layer level (Object properties > Visibility > GP uselight)",
default=True, options={'HIDDEN'}) default=True, options={'HIDDEN'})
check_front_axis : BoolProperty( check_front_axis : BoolProperty(
@@ -65,19 +84,76 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
disable_guide : BoolProperty( disable_guide : BoolProperty(
name="Disable Drawing Guide", name="Disable Drawing Guide",
description="Disable constrained guide in draw mode", description="Disable constrained guide in draw mode",
default=False, options={'HIDDEN'}) default=True, options={'HIDDEN'})
list_disabled_anim : BoolProperty( list_disabled_anim : BoolProperty(
name="List Disabled Animation", name="List Disabled Animation",
description="Alert if there are disabled animations", description="Alert if there are disabled animations",
default=True, options={'HIDDEN'}) 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( autokey_add_n_replace : BoolProperty(
name="Autokey Mode Add and Replace", name="Autokey Mode Add and Replace",
description="Change autokey mode back to 'Add & Replace'", description="Change autokey mode back to 'Add & Replace'",
default=True, options={'HIDDEN'}) 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( eraser_radius : IntProperty(
name="Eraser Radius", description="Radius of eraser brush", name="Eraser Radius", description="Radius of eraser brush",
default=20, min=0, max=500, subtype='PIXEL') default=20, min=0, max=500, subtype='PIXEL')
@@ -107,7 +183,7 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
edit_lines_opacity : FloatProperty( edit_lines_opacity : FloatProperty(
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils", 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) default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
## render ## render
@@ -127,6 +203,12 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5), ('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) reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)
render_settings = bpy.props.BoolProperty(default = False) render_settings = bpy.props.BoolProperty(default = False)
@@ -147,3 +229,41 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
stroke_select = bpy.props.EnumProperty(items = [("POINT","Point",""),("STROKE","Stroke","")],update = update_selection_mode) 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)
+649 -13
View File
@@ -3,6 +3,7 @@ import numpy as np
import bmesh import bmesh
import mathutils import mathutils
from mathutils import Vector from mathutils import Vector
import math
from math import sqrt from math import sqrt
from sys import platform from sys import platform
import subprocess import subprocess
@@ -15,6 +16,10 @@ import subprocess
else : else :
return layer.parent return layer.parent
""" """
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
def get_matrix(ob) : def get_matrix(ob) :
'''return a copy of the world_matrix, applied object matrix if its a bone''' '''return a copy of the world_matrix, applied object matrix if its a bone'''
if isinstance(ob, bpy.types.PoseBone) : if isinstance(ob, bpy.types.PoseBone) :
@@ -29,19 +34,14 @@ def set_matrix(gp_frame,mat):
# get view vector location (the 2 methods work fine) # get view vector location (the 2 methods work fine)
def get_view_origin_position(): def get_view_origin_position():
#method 1 ## method 1
# from bpy_extras import view3d_utils # from bpy_extras import view3d_utils
# region = bpy.context.region # region = bpy.context.region
# rv3d = bpy.context.region_data # rv3d = bpy.context.region_data
# view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0)) # 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
#method 2
r3d = bpy.context.space_data.region_3d r3d = bpy.context.space_data.region_3d
view_loc2 = r3d.view_matrix.inverted().translation view_loc2 = r3d.view_matrix.inverted().translation
# print("view_loc2", view_loc2)#Dbg
# if view_loc != view_loc2: print('Might be an error when finding view coordinate')
return view_loc2 return view_loc2
def location_to_region(worldcoords): def location_to_region(worldcoords):
@@ -75,6 +75,10 @@ def object_derived_get(ob, scene):
return ob_matrix_pairs return ob_matrix_pairs
# -----------------
### Bmesh
# -----------------
def link_vert(v,ordered_vert) : def link_vert(v,ordered_vert) :
for e in v.link_edges : for e in v.link_edges :
other_vert = e.other_vert(v) other_vert = e.other_vert(v)
@@ -146,6 +150,10 @@ def gp_stroke_to_bmesh(strokes):
return strokes_info return strokes_info
# -----------------
### GP Drawing
# -----------------
def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0): def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
''' '''
draw basic stroke by passing list of point 3D coordinate draw basic stroke by passing list of point 3D coordinate
@@ -251,26 +259,34 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
# Convert the 0-1 range into a value in the right range. # Convert the 0-1 range into a value in the right range.
return rightMin + (valueScaled * rightSpan) return rightMin + (valueScaled * rightSpan)
#### GP funcs # -----------------
### GP funcs
# -----------------
def get_gp_draw_plane(context): """ V1
def get_gp_draw_plane(obj=None):
''' return tuple with plane coordinate and normal ''' return tuple with plane coordinate and normal
of the curent drawing accordign to geometry''' of the curent drawing accordign to geometry'''
context = bpy.context
settings = context.scene.tool_settings settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
if obj:
mat = obj.matrix_world
else:
mat = context.object.matrix_world if context.object else None mat = context.object.matrix_world if context.object else None
# -> placement # -> placement
if loc == "CURSOR": if loc == "CURSOR":
plane_co = context.scene.cursor.location plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE') else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
if not context.object: if not context.object:
plane_co = None plane_co = None
else: else:
plane_co = context.object.matrix_world.to_translation()# context.object.location plane_co = context.object.matrix_world.to_translation()# context.object.location
# -> orientation # -> orientation
if orient == 'VIEW': if orient == 'VIEW':
#only depth is important, no need to get view vector #only depth is important, no need to get view vector
@@ -293,6 +309,81 @@ def get_gp_draw_plane(context):
plane_no.rotate(context.scene.cursor.matrix) plane_no.rotate(context.scene.cursor.matrix)
return plane_co, plane_no return plane_co, plane_no
"""
## V2
def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal
of the curent drawing according to geometry'''
if obj is None:
obj = bpy.context.object
settings = bpy.context.scene.tool_settings
if orient is None:
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
mat = obj.matrix_world
plane_no = Vector((0.0, 0.0, 1.0))
plane_co = mat.to_translation()
# -> orientation
if orient == 'VIEW':
mat = bpy.context.scene.camera.matrix_world
# -> placement
if loc == "CURSOR":
plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix
elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0))
elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0))
elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
return plane_co, plane_no
def check_angle_from_view(obj=None, plane_no=None, context=None):
'''Return angle to obj according to chosen drawing axis'''
import math
from bpy_extras import view3d_utils
if not context:
context = bpy.context
if not plane_no:
_plane_co, plane_no = get_gp_draw_plane(obj=obj)
view_direction = view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, (context.region.width/2.0, context.region.height/2.0))
angle = math.degrees(view_direction.angle(plane_no))
# correct angle value when painting from other side (seems a bit off...)
if angle > 90:
angle = abs(90 - (angle - 90))
# over angle warning
if angle > 75:
return angle, 'ERROR', f"Canvas not aligned at all! (angle: {angle:.2f}°), use Realign GP or change draw axis"
if angle > 6:
return angle, 'ERROR', f'Canvas not aligned! angle: {angle:.2f}°, use Realign GP'
if angle > 3: # below 3 is reasonable (camera seem to view induce error !)
return angle, 'INFO', f'Canvas almost aligned (angle: {angle:.2f}°)'
if angle == 0:
return angle, 'INFO', f'Canvas perfectly aligned (angle: {angle:.2f}°)'
# below 1 consider aligned
return angle, 'INFO', f'Canvas alignement seem ok (angle: {angle:.2f}°)'
## need big update ## need big update
def create_gp_palette(gp_data_block,info) : def create_gp_palette(gp_data_block,info) :
@@ -449,6 +540,10 @@ from mathutils import Vector
### Vector utils 3d ### Vector utils 3d
# ----------------- # -----------------
def matrix_transform(coords, matrix):
coords_4d = np.column_stack((coords, np.ones(len(coords), dtype='float64')))
return np.einsum('ij,aj->ai', matrix, coords_4d)[:, :-1]
def single_vector_length(v): def single_vector_length(v):
return sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2])) return sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]))
@@ -506,15 +601,20 @@ def extrapolate_points_by_length(a,b, length):
### Vector utils 2d ### Vector utils 2d
# ----------------- # -----------------
def is_vector_close(a, b, rel_tol=1e-03):
'''compare Vector or sequence of value
by default tolerance is set on 1e-03 (0.001)
'''
return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)])
def single_vector_length_2d(v): def single_vector_length_2d(v):
return sqrt((v[0] * v[0]) + (v[1] * v[1])) return sqrt((v[0] * v[0]) + (v[1] * v[1]))
def vector_length_2d(A,B): def vector_length_2d(A,B):
''''take two Vector and return length''' ''''take two Vector and return length'''
return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2) return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
def vector_length_coeff_2d(size, A, B): def vector_length_coeff_2d(size, A, B):
''' '''
Calculate the vector lenght Calculate the vector lenght
@@ -529,7 +629,7 @@ def vector_length_coeff_2d(size, A, B):
def cross_vector_coord_2d(foo, bar, size): def cross_vector_coord_2d(foo, bar, size):
'''Return the coord in space of a cross vector between the two point with specified size''' '''Return the coord in space of a cross vector between the two point with specified size'''
###middle location between 2 vector is calculated by adding the two vector and divide by two ##middle location between 2 vector is calculated by adding the two vector and divide by two
##mid = (foo + bar) / 2 ##mid = (foo + bar) / 2
between = foo - bar between = foo - bar
#create a generic Up vector (on Y or Z) #create a generic Up vector (on Y or Z)
@@ -606,6 +706,17 @@ def get_addon_prefs():
addon_prefs = preferences.addons[addon_name].preferences addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs) return (addon_prefs)
def open_addon_prefs():
'''Open addon prefs windows with focus on current addon'''
from .__init__ import bl_info
wm = bpy.context.window_manager
wm.addon_filter = 'All'
if not 'COMMUNITY' in wm.addon_support: # reactivate community
wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY'])
wm.addon_search = bl_info['name']
bpy.context.preferences.active_section = 'ADDONS'
bpy.ops.preferences.addon_expand(module=__package__)
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
def open_file(file_path) : def open_file(file_path) :
'''Open filepath with default browser''' '''Open filepath with default browser'''
@@ -663,6 +774,25 @@ def detect_OS():
print("Cannot detect OS, python 'sys.platform' give :", myOS) print("Cannot detect OS, python 'sys.platform' give :", myOS)
return None return None
def fuzzy_match(s1, s2, tol=0.8, case_sensitive=False):
'''Tell if two strings are similar using a similarity ratio (0 to 1) value passed as third arg'''
from difflib import SequenceMatcher
# can also use difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio() > tol
def fuzzy_match_ratio(s1, s2, case_sensitive=False):
'''Tell how much two passed strings are similar 1.0 being exactly similar'''
from difflib import SequenceMatcher
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio()
def convert_attr(Attr): def convert_attr(Attr):
'''Convert given value to a Json serializable format''' '''Convert given value to a Json serializable format'''
if isinstance(Attr, (mathutils.Vector,mathutils.Color)): if isinstance(Attr, (mathutils.Vector,mathutils.Color)):
@@ -676,13 +806,519 @@ def convert_attr(Attr):
## confirm pop-up message: ## confirm pop-up message:
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list
if _message if a list of lists:
if sublist have 2 element:
considered a label [text,icon]
if sublist have 3 element:
considered as an operator [ops_id_name, text, icon]
'''
def draw(self, context): def draw(self, context):
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
self.layout.label(text=l) self.layout.label(text=l)
else: else:
if len(l) == 2: # label with icon
self.layout.label(text=l[0], icon=l[1]) self.layout.label(text=l[0], icon=l[1])
elif len(l) == 3: # ops
self.layout.operator_context = "INVOKE_DEFAULT"
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
## offset pnale when using row...
# row = self.layout.row()
# row.label(text=l[1])
# row.operator(l[0], icon=l[2])
if isinstance(_message, str): if isinstance(_message, str):
_message = [_message] _message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# -----------------
### UI utils
# -----------------
## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout):
map_type = kmi.map_type
## col = _indented_layout(layout)
# layout.label(text=f'{km.name} - {km.space_type} - {km.region_type}') # debug
# layout.label(text=f'{km.name}')
col = layout.column()
if kmi.show_expanded:
col = col.column(align=True)
box = col.box()
else:
box = col.column()
split = box.split()
# header bar
row = split.row(align=True)
row.prop(kmi, "show_expanded", text="", emboss=False)
row.prop(kmi, "active", text="", emboss=False)
if km.is_modal:
row.separator()
row.prop(kmi, "propvalue", text="")
else:
row.label(text=kmi.name)
row = split.row()
row.prop(kmi, "map_type", text="")
if map_type == 'KEYBOARD':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'MOUSE':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'NDOF':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'TWEAK':
subrow = row.row()
subrow.prop(kmi, "type", text="")
subrow.prop(kmi, "value", text="")
elif map_type == 'TIMER':
row.prop(kmi, "type", text="")
else:
row.label()
### / Hided delete button
if (not kmi.is_user_defined) and kmi.is_user_modified:
row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id
else:
pass ### NO REMOVE
# row.operator(
# "preferences.keyitem_remove",
# text="",
# # Abusing the tracking icon, but it works pretty well here.
# icon=('TRACKING_CLEAR_BACKWARDS' if kmi.is_user_defined else 'X')
# ).item_id = kmi.id
### Hided delete button /
# Expanded, additional event settings
if kmi.show_expanded:
box = col.box()
split = box.split(factor=0.4)
sub = split.row()
if km.is_modal:
sub.prop(kmi, "propvalue", text="")
else:
# One day...
# sub.prop_search(kmi, "idname", bpy.context.window_manager, "operators_all", text="")
sub.prop(kmi, "idname", text="")
if map_type not in {'TEXTINPUT', 'TIMER'}:
sub = split.column()
subrow = sub.row(align=True)
if map_type == 'KEYBOARD':
subrow.prop(kmi, "type", text="", event=True)
subrow.prop(kmi, "value", text="")
subrow_repeat = subrow.row(align=True)
subrow_repeat.active = kmi.value in {'ANY', 'PRESS'}
subrow_repeat.prop(kmi, "repeat", text="Repeat")
elif map_type in {'MOUSE', 'NDOF'}:
subrow.prop(kmi, "type", text="")
subrow.prop(kmi, "value", text="")
subrow = sub.row()
subrow.scale_x = 0.75
subrow.prop(kmi, "any", toggle=True)
subrow.prop(kmi, "shift", toggle=True)
subrow.prop(kmi, "ctrl", toggle=True)
subrow.prop(kmi, "alt", toggle=True)
subrow.prop(kmi, "oskey", text="Cmd", toggle=True)
subrow.prop(kmi, "key_modifier", text="", event=True)
# Operator properties
box.template_keymap_item_properties(kmi)
## Modal key maps attached to this operator
# if not km.is_modal:
# kmm = kc.keymaps.find_modal(kmi.idname)
# if kmm:
# draw_km(display_keymaps, kc, kmm, None, layout + 1)
# layout.context_pointer_set("keymap", km)
# -----------------
### linking utility
# -----------------
def link_objects_in_blend(filepath, obj_name_list, link=True):
'''Link an object by name from a file, if link is False, append instead of linking'''
if isinstance(obj_name_list, str):
obj_name_list = [obj_name_list]
with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
data_to.objects = [o for o in data_from.objects if o in obj_name_list]
return data_to.objects
def check_materials_in_blend(filepath):
'''Return a list of all material in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
l = [m for m in data_from.materials]
return l
def check_objects_in_blend(filepath, avoid_camera=True):
'''Return a list of object name in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
if avoid_camera:
l = [o for o in data_from.objects if not any(x in o.lower() for x in ('camera', 'draw_cam', 'obj_cam'))]
else:
l = [o for o in data_from.objects]
return l
# -----------------
### props handling
# -----------------
def iterate_selector(zone, attr, state, info_attr = None, active_access='active'):
'''Iterate with given attribute'''
item_number = len(zone)
if item_number <= 1:
return
if getattr(zone, attr) == None:
print('no', attr, 'in', zone)
return
if state: # swap
info = None
bottom = None
new_index = getattr(zone, attr) + state
setattr(zone, attr, new_index % item_number)
if new_index == item_number:
bottom = 1 # bottom reached, cycle to first
elif new_index < 0:
bottom = -1 # up reached, cycle to last
if info_attr:
active_item = getattr(zone, active_access) # active by default
if active_item:
info = getattr(active_item, info_attr)
return info, bottom
# -----------------
### Curve handle
# -----------------
def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
'''Create curve at provided location and direction vector'''
## option to create nurbs instaed of bezier ?
context = context or bpy.context
## using ops (dirty)
# bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1))
# curve = context.object
# curve.name = 'curve_path'
# # fast straighten
# bpy.ops.curve.handle_type_set(type='VECTOR')
# bpy.ops.curve.handle_type_set(type='ALIGNED')
# bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL',
# orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL',
# constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False)
## using data
curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT')
curve_data.dimensions = '3D'
curve_data.use_path = True
curve = bpy.data.objects.new(name, curve_data)
spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS')
spl.bezier_points.add(1) # One point already exists
for i in range(2):
spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO')
spl.bezier_points[i].handle_right_type = 'VECTOR'
spl.bezier_points[1].co = direction
# Back to aligned mode
for i in range(2):
spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED'
# Select second point
spl.bezier_points[1].select_control_point = True
spl.bezier_points[1].select_left_handle = True
spl.bezier_points[1].select_right_handle = True
# link
context.scene.collection.objects.link(curve)
# curve object settings
curve.location = location
curve.show_in_front = True
# enter edit
if enter_edit and context.mode == 'OBJECT':
curve.select_set(True)
context.view_layer.objects.active = curve
bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE
## set viewport overlay visibility for better view
if context.space_data.type == 'VIEW_3D':
context.space_data.overlay.show_curve_normals = True
context.space_data.overlay.normals_length = 0.2
return curve
def get_direction_vector_from_enum(string) -> Vector:
orient_vectors = {
'FORWARD_X' : Vector((1,0,0)),
'FORWARD_Y' : Vector((0,1,0)),
'FORWARD_Z' : Vector((0,0,1)),
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
}
return orient_vectors[string]
def orentation_track_from_vector(input_vector) -> str:
'''return closest world track orientation name from passed vector direction'''
orient_vectors = {
'FORWARD_X' : Vector((1,0,0)),
'FORWARD_Y' : Vector((0,1,0)),
'FORWARD_Z' : Vector((0,0,1)),
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
}
orient = None
min_angle = 10000
for track, v in orient_vectors.items():
angle = input_vector.angle(v)
if angle < min_angle:
min_angle = angle
orient = track
return orient
def create_follow_path_constraint(ob, curve, follow_curve=False, use_fixed_location=True):
'''return create constraint'''
# # Clear bone follow path constraint
exiting_fp_constraints = [c for c in ob.constraints if c.type == 'FOLLOW_PATH']
for c in exiting_fp_constraints:
ob.constraints.remove(c)
# loc = ob.matrix_world @ ob.matrix.to_translation()
if ob.location != (0,0,0):
old_loc = ob.location
ob.location = (0,0,0)
print(f'ob moved from {old_loc} to (0,0,0) to counter follow curve offset')
const = ob.constraints.new('FOLLOW_PATH')
const.target = curve
if follow_curve:
const.use_curve_follow = True
if use_fixed_location:
const.use_fixed_location = True
return const
## on_bones:
# prefs = get_addon_prefs()
# root_name = prefs.tgt_bone
# root = ob.pose.bones.get(root_name)
# if not root:
# return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name')
# # Clear bone follow path constraint
# exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH']
# for c in exiting_fp_constraints:
# root.constraints.remove(c)
# # loc = ob.matrix_world @ root.matrix.to_translation()
# if root.name == ('world', 'root') and root.location != (0,0,0):
# old_loc = root.location
# root.location = (0,0,0)
# print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset')
# const = root.constraints.new('FOLLOW_PATH')
# const.target = curve
# # axis only in this case, should be in addon to prefs
# ## determine which axis to use... maybe found orientation in world space from matrix_basis ?
# root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis)
# const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X'
# print('const.forward_axis: ', const.forward_axis)
# const.use_curve_follow = True
# return curve, const
# -----------------
### Object
# -----------------
def go_edit_mode(ob, context=None):
'''set mode to object, set passed obhject as active and go Edit'''
context = context or bpy.context
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
ob.select_set(True)
context.view_layer.objects.active = ob
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
## Not used anymore
def all_anim_enabled(objects) -> bool:
'''return False if at least one fcurve/group has disabled animation in passed object anim'''
for o in objects:
if o.animation_data and o.animation_data.action:
## Skip hided object ?
# if o.hide_get():
# continue
## Check if groups are muted
for grp in o.animation_data.action.groups:
if grp.mute:
return False
## Check if fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
if o.type in ('GPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
return True
## Not used anymore
def all_object_modifier_enabled(objects) -> bool:
'''Return False if one modifier of one object has GP modifier disabled in viewport but enabled in render'''
for o in objects:
if o.type != 'GPENCIL':
continue
for m in o.grease_pencil_modifiers:
if m.show_render and not m.show_viewport:
return False
## return False when viewport but no render ?
# if not m.show_render and m.show_viewport:
# return False
return True
## Only used in anim_status in 'per GP object' mode
def has_fully_enabled_anim(o):
if o.animation_data and o.animation_data.action:
## Skip hided object ? (missmatch)
# if o.hide_get():
# continue
## Check if groups are muted
for grp in o.animation_data.action.groups:
if grp.mute:
return False
## Check if fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
if o.type in ('GPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
return True
def anim_status(objects) -> tuple((str, str)):
'''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')'''
on_count = off_count = count = 0
for o in objects:
## Skip hidden objects
if o.hide_get() and o.hide_render:
continue
### Per Object
# count += 1
# if has_fully_enabled_anim(o):
# on_count += 1
# else:
# off_count += 1
### Consider All channels individually
if o.animation_data and o.animation_data.action:
for grp in o.animation_data.action.groups:
## Check if groups are muted
if grp.mute:
off_count += 1
else:
on_count += 1
count += 1
for fcu in o.animation_data.action.fcurves:
## Check if fcurves are muted
if fcu.mute:
off_count += 1
else:
on_count += 1
count += 1
if o.type in ('GPENCIL', 'CAMERA'):
datablock = o.data
if datablock.animation_data is None:
continue
if not datablock.animation_data.action:
continue
## Check if object data attributes fcurves are muted
for fcu in datablock.animation_data.action.fcurves:
if fcu.mute:
off_count += 1
else:
on_count += 1
count += 1
if not on_count and not off_count:
return ('BLANK1', 'BLANK1') # 'NONE'
elif on_count == count:
return ('LAYER_ACTIVE', 'BLANK1') # 'ALL_ON'
elif off_count == count:
return ('BLANK1', 'LAYER_ACTIVE') # 'ALL_OFF'
else:
return ('LAYER_USED', 'LAYER_USED') # 'MIXED'
def gp_modifier_status(objects) -> tuple((str, str)):
'''return icons on/off tuple'''
on_count = off_count = count = 0
for o in objects:
if o.type != 'GPENCIL':
continue
## Skip hided object
if o.hide_get() and o.hide_render:
continue
for m in o.grease_pencil_modifiers:
if m.show_render and not m.show_viewport:
off_count += 1
else:
on_count += 1
count += 1
## return False when viewport but no render ?
# if not m.show_render and m.show_viewport:
# return False
if not on_count and not off_count:
return ('BLANK1', 'BLANK1') # 'NONE'
elif on_count == count:
return ('LAYER_ACTIVE', 'BLANK1') # 'ALL_ON'
elif off_count == count:
return ('BLANK1', 'LAYER_ACTIVE') # 'ALL_OFF'
else:
return ('LAYER_USED', 'LAYER_USED')
+237
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)