Compare commits
170 Commits
Author | SHA1 | Date |
---|---|---|
pullusb | cf3f39b730 | |
pullusb | 1c39d81ef1 | |
pullusb | 7665dd4f4f | |
pullusb | 2013c55ba8 | |
pullusb | 3695470354 | |
pullusb | 186229cdba | |
pullusb | e441f485a6 | |
pullusb | 0cee6163aa | |
pullusb | 58e6816e39 | |
pullusb | 4d6dc06e4e | |
pullusb | c8763f5ca4 | |
pullusb | f9e7c9cc3b | |
pullusb | 86fb848e4a | |
pullusb | abb61ca6d4 | |
pullusb | 7430bca02f | |
pullusb | f5c20a3499 | |
pullusb | edfefa874a | |
pullusb | 6dbf666ee3 | |
pullusb | d3e4072564 | |
pullusb | 63f377d7d1 | |
pullusb | 49c70860a6 | |
pullusb | b525cda28e | |
pullusb | 05053cff68 | |
pullusb | 4937aa32c0 | |
pullusb | 5d930df06b | |
pullusb | 5d35074d3d | |
pullusb | 94fc926f7a | |
pullusb | 0160b4eae4 | |
pullusb | 1dfb8cff9c | |
pullusb | 4732110b93 | |
pullusb | 7bc7d5d9ff | |
pullusb | 998bd4b0cb | |
pullusb | 25adb5beb6 | |
pullusb | 6e94ee270d | |
pullusb | 3ece64e517 | |
pullusb | eae69b6f75 | |
pullusb | f5a78601b6 | |
pullusb | 98ed92afe2 | |
pullusb | c02b890915 | |
pullusb | 19e26f8cee | |
pullusb | 01ce06201e | |
pullusb | 92e53f8368 | |
pullusb | 386be46251 | |
pullusb | 47b9b68e9e | |
pullusb | 810256f5cb | |
pullusb | cf2ba8448a | |
pullusb | 4fadf35163 | |
pullusb | 429179d936 | |
pullusb | 053a9d7f7b | |
pullusb | 280a575631 | |
pullusb | aa0eb9bd25 | |
pullusb | 2191be97cb | |
pullusb | 990b18f665 | |
pullusb | 798afbe82a | |
pullusb | 7e92ae182e | |
pullusb | 9a49175859 | |
Pullusb | eaf9c3b22f | |
pullusb | 8e223b9f3a | |
pullusb | d1b03c804c | |
pullusb | d1748c592d | |
pullusb | 652c4632a9 | |
pullusb | 80ec798144 | |
pullusb | 4ae5d3a55b | |
pullusb | 6506536fb2 | |
pullusb | 5dbd59652e | |
pullusb | 5d55afcf4d | |
pullusb | b370dd7344 | |
pullusb | b2a6e6a899 | |
Pullusb | bf30254871 | |
Pullusb | ef566c494f | |
Pullusb | a021535b3f | |
Pullusb | 336d1b264c | |
Pullusb | 4622aa4520 | |
Pullusb | 88cdbf88cd | |
Pullusb | 56a83910bd | |
Pullusb | caa3187772 | |
Pullusb | e0a50ea49a | |
Pullusb | c32ea207c6 | |
Pullusb | 515ba4aa9f | |
Pullusb | 02d13c6f29 | |
Pullusb | 8e3b3871e6 | |
Pullusb | 43a28876d1 | |
Pullusb | 050e8a405e | |
Pullusb | 762b92fbfe | |
Pullusb | e1681cd6ad | |
Pullusb | 7092bbf1be | |
Pullusb | 308b41e401 | |
Pullusb | 5574c75769 | |
Pullusb | bf60370ab0 | |
Pullusb | f36b31e3aa | |
Pullusb | 26c71f7bb3 | |
Pullusb | 8404e16633 | |
Pullusb | 8c49c1efcb | |
Pullusb | eccb450081 | |
Pullusb | 651f9e7747 | |
Pullusb | 1145b73b58 | |
Pullusb | cab3a91f70 | |
Pullusb | 7985844226 | |
Pullusb | 4ac743cd32 | |
Pullusb | 97b09444ab | |
Pullusb | 3c7477c442 | |
Pullusb | 6c19fa54af | |
Pullusb | 78b70c8fca | |
Pullusb | 9389faed22 | |
Pullusb | 204553ed47 | |
Pullusb | 44ccb3d146 | |
Pullusb | 56cbc04c65 | |
Pullusb | a0ed941e4b | |
Pullusb | 4c6bd20b60 | |
Pullusb | b62a23858c | |
Pullusb | f7e8dce0ff | |
Pullusb | 8b838b52ca | |
Pullusb | d4ea39fb40 | |
Pullusb | ea772b297a | |
Pullusb | 884ee2a695 | |
Pullusb | a9ed3f7e79 | |
Pullusb | dd68e74e2b | |
Pullusb | 9d26ed4085 | |
Pullusb | 1017c01668 | |
Pullusb | f05e4fe41d | |
Pullusb | 408f017e81 | |
Pullusb | e6ff824684 | |
Pullusb | 985e395beb | |
Pullusb | a218aefd10 | |
Pullusb | 3974a15ff0 | |
Pullusb | 7ff96ad205 | |
Pullusb | d480fa0817 | |
Pullusb | 212436c451 | |
Pullusb | 9612c84396 | |
Pullusb | 66ef75c76d | |
Pullusb | 75c8cb9206 | |
Pullusb | e44607bc2c | |
Pullusb | fdd3e2caef | |
Pullusb | 801e14cb7a | |
Pullusb | 15aec65b19 | |
Pullusb | 0740e835d8 | |
Pullusb | f599964296 | |
Pullusb | 4a4d72b410 | |
Pullusb | 253d1501a1 | |
Christophe SEUX | 2b4c797e7e | |
Christophe SEUX | beba17fe2f | |
Pullusb | 2efbb36ddf | |
Christophe SEUX | 060fa8ff92 | |
Christophe SEUX | 81e21bb428 | |
Christophe SEUX | 9c7184836b | |
Christophe SEUX | bfcac1615d | |
Christophe SEUX | 6eabc02ce8 | |
Christophe SEUX | 3d7a208a50 | |
ChristopheSeux | adfd887924 | |
Pullusb | 86139a20fb | |
Pullusb | 682048af63 | |
Pullusb | 94e3b7a7ad | |
Pullusb | f10f572bdc | |
Pullusb | d8f71e3356 | |
Pullusb | 2754ebf89b | |
Pullusb | 4b9cd20891 | |
Pullusb | e22b91265b | |
Pullusb | 6cf22b81e8 | |
Pullusb | 517eceab76 | |
Pullusb | 9b06aee9af | |
Pullusb | 3594c4812d | |
Pullusb | bf85582f28 | |
Pullusb | 492095d333 | |
Pullusb | 07a44190c9 | |
Pullusb | b193c67358 | |
Pullusb | 02ed04cd3d | |
Pullusb | 801235c760 | |
Pullusb | 6d43064f0b | |
Pullusb | b9dd1196ca | |
Pullusb | 5e876f360b |
|
@ -0,0 +1,748 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
4.0.3
|
||||||
|
|
||||||
|
changed: File checker doest not fix directly when clicked (also removed choice in preference):
|
||||||
|
- list potential change and display an `Apply Fix`
|
||||||
|
changed: Enhanced visibility conflict list:
|
||||||
|
- also include viewlayer hide value
|
||||||
|
- allow to set all hide value from the state of one of the three
|
||||||
|
- fixed: material move operator
|
||||||
|
|
||||||
|
4.0.1
|
||||||
|
|
||||||
|
- fixed: layer nav operator on page up/down
|
||||||
|
|
||||||
|
4.0.0
|
||||||
|
|
||||||
|
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
|
||||||
|
|
||||||
|
3.3.0
|
||||||
|
|
||||||
|
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
|
||||||
|
|
||||||
|
3.2.0
|
||||||
|
|
||||||
|
- added: UI settings to show GP tool settings placement and orientation
|
||||||
|
- fixed: Bug with reproject orientation settings
|
||||||
|
- added: show current orientation in batch reproject popup UI (if current is selected)
|
||||||
|
|
||||||
|
3.1.0
|
||||||
|
|
||||||
|
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
|
||||||
|
|
||||||
|
3.0.2
|
||||||
|
|
||||||
|
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
|
||||||
|
|
||||||
|
3.0.1
|
||||||
|
|
||||||
|
- fixed: Crash after generating empty frames
|
||||||
|
|
||||||
|
3.0.0
|
||||||
|
|
||||||
|
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
|
||||||
|
- fixed: openGL draw camera frame and passepartout
|
||||||
|
|
||||||
|
2.5.0
|
||||||
|
|
||||||
|
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
|
||||||
|
|
||||||
|
2.4.0
|
||||||
|
|
||||||
|
- changed: Batch reproject consider camera movement and is almost 8x faster
|
||||||
|
- added: Batch reproject have "Current" mode (using current tool setting)
|
||||||
|
|
||||||
|
2.3.4
|
||||||
|
|
||||||
|
- fixed: bug when exporting json palettes containing empty material slots
|
||||||
|
|
||||||
|
2.3.3
|
||||||
|
|
||||||
|
- fixed: Bug with animation manager objects data
|
||||||
|
|
||||||
|
2.3.2
|
||||||
|
|
||||||
|
- fixed: Bug with animation manager when there is empty object in scene
|
||||||
|
|
||||||
|
2.3.1
|
||||||
|
|
||||||
|
- changed: Animation manager show hints when animation is enabled: fully, partially or not at all.
|
||||||
|
|
||||||
|
2.3.0
|
||||||
|
|
||||||
|
- added: Animation manager buttons are colored red when objects have disabled animation
|
||||||
|
- fixed: Animation manager not enabling/disabling Action Groups
|
||||||
|
- fixed: Animation manager `List Disabled Anims` list groups as well
|
||||||
|
|
||||||
|
2.2.3
|
||||||
|
|
||||||
|
- fixed: Type error on realign ops
|
||||||
|
|
||||||
|
2.2.2
|
||||||
|
|
||||||
|
- fixed: draw_cam data not changed when working with multiple camera in a shot
|
||||||
|
|
||||||
|
2.2.1
|
||||||
|
|
||||||
|
- added: class View3D to calculate area 3d related coordinates
|
||||||
|
|
||||||
|
2.2.0
|
||||||
|
|
||||||
|
- added: _Remove redundant stroke_ in File checker (Just list duplicate numbers in "check only" mode)
|
||||||
|
|
||||||
|
2.1.6
|
||||||
|
|
||||||
|
- fixed: Prevent some keymaps to register when blender is launched in background mode
|
||||||
|
|
||||||
|
2.1.5
|
||||||
|
|
||||||
|
- fixed: false positive with checkfile in modifier layer target
|
||||||
|
|
||||||
|
2.1.4
|
||||||
|
|
||||||
|
- fixed: layer change msgbus not working at first activation of the addon
|
||||||
|
|
||||||
|
2.1.3
|
||||||
|
|
||||||
|
- fixed: decoralate Prefix and Suffix UI_lists scroll
|
||||||
|
- fixed: Problem when settings project namespaces
|
||||||
|
- added: Button to reset project namespace (to have the right order)
|
||||||
|
|
||||||
|
2.1.2
|
||||||
|
|
||||||
|
- added: `gp.brush_set` operator to manually assign a brush by name to a shortcut
|
||||||
|
- preferably add shortcut to `Grease Pencil > Grease Pencil Stroke Paint Mode`
|
||||||
|
|
||||||
|
2.1.1
|
||||||
|
|
||||||
|
- added: follow curve show offset property in UI
|
||||||
|
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||||
|
- changed: created follow curve use `fixed offset`
|
||||||
|
|
||||||
|
2.1.1
|
||||||
|
|
||||||
|
- added: follow curve show offset property in UI
|
||||||
|
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||||
|
- changed: created follow curve use `fixed offset`
|
||||||
|
|
||||||
|
2.1.0
|
||||||
|
|
||||||
|
- added: 3 actions buttons:
|
||||||
|
- create curve with follow path and go into curve edit
|
||||||
|
- go back to object
|
||||||
|
- got to curve edit (if follow path constraint exists with a curve target)
|
||||||
|
- if follow path exists, button to remove constraint
|
||||||
|
|
||||||
|
2.0.11
|
||||||
|
|
||||||
|
- fix: prefix set by project environment
|
||||||
|
|
||||||
|
2.0.10
|
||||||
|
|
||||||
|
- fix: poll error in console
|
||||||
|
|
||||||
|
2.0.9
|
||||||
|
|
||||||
|
- fix: prefix/suffix UIlist actions trigger UI redraw to see changes live
|
||||||
|
- changed: PropertyGroups are now registered in their own file
|
||||||
|
- code: cleanup
|
||||||
|
|
||||||
|
2.0.8
|
||||||
|
|
||||||
|
- changed: suffix as UIlist in prefs,
|
||||||
|
- fix: prefix and suffix register and load
|
||||||
|
|
||||||
|
2.0.7
|
||||||
|
|
||||||
|
- fix: broken auto-fade with gp layer navigation when used with a customized shortcut.
|
||||||
|
- changed: supported version number to 3.0.0
|
||||||
|
|
||||||
|
2.0.6
|
||||||
|
|
||||||
|
- changed: Use prefixes toggle is enabled by default
|
||||||
|
- changed: prefixes are now set in preferences as a reorderable UI list with full name for description
|
||||||
|
- This should not affect `PREFIXES` env variable (`'CO, LN'`)
|
||||||
|
- Now can be also passed as prefix:name pair ex: `'CO : Color, LN : Line`
|
||||||
|
|
||||||
|
2.0.5
|
||||||
|
|
||||||
|
- changed: redo panel for GP layer picker has better name and display active layer name
|
||||||
|
- changed: some operator id_name to expose "add shortcut" in context menu for some button
|
||||||
|
- fix: error with draw_cam handler when no camera is active
|
||||||
|
|
||||||
|
2.0.3
|
||||||
|
|
||||||
|
- changed: `X-ray` to `In Front`, match original object property name
|
||||||
|
|
||||||
|
2.0.2
|
||||||
|
|
||||||
|
- added: possibility to select which point attribute is copied by GP layer copy to clipboard
|
||||||
|
|
||||||
|
2.0.1
|
||||||
|
|
||||||
|
- added: enable/disable camera animation separately in `animation manager`
|
||||||
|
- for cam,disable properties like focal and shift (to be able to work on a fixed cam)
|
||||||
|
- camera is not disable using `obj` anymore
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
|
||||||
|
- fix: error using playblast (api changes of Blender 3.0+)
|
||||||
|
- added: possibility to change playblast destination in preferences
|
||||||
|
- changed: playblast default is `playblast` folder instead of `images`
|
||||||
|
- changed: completely disable experimental precise eraser
|
||||||
|
|
||||||
|
1.9.9
|
||||||
|
|
||||||
|
- fix: Bug setting GP layers actions on/off
|
||||||
|
- fix: Add GP layers animation datas in `list disabled animation` report
|
||||||
|
|
||||||
|
1.9.8
|
||||||
|
|
||||||
|
- fix: animation manager `GP anim on/off` also toggle layers animation datas
|
||||||
|
|
||||||
|
1.9.7
|
||||||
|
|
||||||
|
- changed: `list modifiers visibility conflict` (used in `check file` or through search menu) now not limited to GP objects type.
|
||||||
|
|
||||||
|
1.9.6
|
||||||
|
|
||||||
|
- fix: icon removed in 3.0
|
||||||
|
|
||||||
|
1.9.5
|
||||||
|
|
||||||
|
- added: `Check file` also check if GP modifiers have broken layer target
|
||||||
|
- changed: `Check file` disable drawing guide is now On by default
|
||||||
|
|
||||||
|
1.9.4
|
||||||
|
|
||||||
|
- feat: renaming using the name_manager take care of changing layer target values in GP modifiers
|
||||||
|
|
||||||
|
1.9.3
|
||||||
|
|
||||||
|
- feat: Add an update button at bottom of preferences if addon is a cloned repository
|
||||||
|
|
||||||
|
1.9.2
|
||||||
|
|
||||||
|
- feat: Palette linker has a name exclusion list in preferences
|
||||||
|
- Avoid linking some material that are prefered local to file
|
||||||
|
- Default item in list : `line`
|
||||||
|
|
||||||
|
1.9.1
|
||||||
|
|
||||||
|
- fix: add error handling on palette linker when blend_path isn't valid anymore
|
||||||
|
- added: file checker entry, Disable all GP object `use lights` (True by default in addon pref `checklist`)
|
||||||
|
- added: WIP of Batch reproject all on cursor.
|
||||||
|
- No UI. In search menu `Flat Reproject Selected On cursor` (idname `gp.batch_flat_reproject`)
|
||||||
|
|
||||||
|
1.9.0
|
||||||
|
|
||||||
|
- feat: New shortcuts:
|
||||||
|
- `F2` in Paint and Edit to rename active layer
|
||||||
|
- `Insert` add a new layer (same as Krita)
|
||||||
|
- `Shift + Insert` add a new layer and immediately pop-up a rename box
|
||||||
|
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
|
||||||
|
- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut
|
||||||
|
|
||||||
|
1.8.1
|
||||||
|
|
||||||
|
- fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore
|
||||||
|
|
||||||
|
1.8.0
|
||||||
|
|
||||||
|
- feat: palette linker (beta), with pop-up from material stack dropdown
|
||||||
|
- feat: palette name fuzzy match
|
||||||
|
- code: add an open addon preference ops
|
||||||
|
|
||||||
|
1.7.8
|
||||||
|
|
||||||
|
- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)
|
||||||
|
|
||||||
|
1.7.7
|
||||||
|
|
||||||
|
- feat: add copy path to `check link` ops with multiple path representation choices
|
||||||
|
|
||||||
|
1.7.6
|
||||||
|
|
||||||
|
- ui: expose (almost) all keymap added by the addon to allow user for customize/disable as needed
|
||||||
|
- ui: changed some names
|
||||||
|
|
||||||
|
1.7.5
|
||||||
|
|
||||||
|
- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
|
||||||
|
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
|
||||||
|
- add undo step for `W`'s select layer from closest stroke
|
||||||
|
|
||||||
|
1.7.4
|
||||||
|
|
||||||
|
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
||||||
|
- fix: copy-paste keymap error on background rendering
|
||||||
|
|
||||||
|
1.7.3
|
||||||
|
|
||||||
|
- added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`)
|
||||||
|
|
||||||
|
1.7.2
|
||||||
|
|
||||||
|
- added: `Object visibility conflict` in file check
|
||||||
|
- print in console when object have render activated but not viewport (& vice-versa)
|
||||||
|
- Standalone ops "List Object Visibility Conflicts" (`gp.list_object_visibility`)
|
||||||
|
- added: `GP Modifier visibility conflict` in file check.
|
||||||
|
- print in console when gp modifiers have render activated but not viewport (& vice-versa)
|
||||||
|
- Standalone ops "List GP Modifiers Visibility Conflicts" (`gp.list_modifier_visibility`)
|
||||||
|
- code: show_message_box utils can now receive operator in sublist (if 3 element)
|
||||||
|
|
||||||
|
1.7.1
|
||||||
|
|
||||||
|
- feat: Improved `Create Empty Frames` operator with mutliple filters to choose source layers
|
||||||
|
|
||||||
|
1.7.0
|
||||||
|
|
||||||
|
- removed: Obsolete operators and panels
|
||||||
|
- Remove "line closer" panel as it's been a native tool for a while in 2.9x (stop register of `GP_guided_colorize > OP_line_closer`)
|
||||||
|
- Remove "Render" subpanel, obsolete and not adapted to production
|
||||||
|
- ui: clean and refactor
|
||||||
|
- Gp clipboard panel and aimation Manager subpanel layout to aligned columns (gain space)
|
||||||
|
- add `GP` on each panel name for wuick eye search
|
||||||
|
- follow cursor now in animation manager subpanel (should be in an extra menu or removed in the end)
|
||||||
|
|
||||||
|
1.6.9
|
||||||
|
|
||||||
|
- change: material picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
|
||||||
|
- this is made to let other operator use functionality on long press using `S`
|
||||||
|
- feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all)
|
||||||
|
- by default `S` is still fill only
|
||||||
|
- but `Alt+S` is now stroke only instead of all
|
||||||
|
|
||||||
|
1.6.8
|
||||||
|
|
||||||
|
- fix: reproject GP repeating projection on same frames
|
||||||
|
- fix: batch reproject all frame not poping dialog menu
|
||||||
|
|
||||||
|
1.6.7
|
||||||
|
|
||||||
|
- fix: error with operator `OP_key_duplicate_send` poll flooding console
|
||||||
|
|
||||||
|
1.6.6
|
||||||
|
|
||||||
|
- fix: inverted scene resolution from project environnement
|
||||||
|
|
||||||
|
1.6.5
|
||||||
|
|
||||||
|
- feat: check canvas alignement of the Gp object compare to chosen draw axis
|
||||||
|
|
||||||
|
1.6.4
|
||||||
|
|
||||||
|
- fix: disable multi-selection for layer naming manager
|
||||||
|
- the dopesheet selection sometimes still consider layer as selected
|
||||||
|
|
||||||
|
1.6.3
|
||||||
|
|
||||||
|
<!-- - abort :: add checkbox to prevent from loading project environnement namespace -->
|
||||||
|
- add buttons to load manually environnement namespace
|
||||||
|
|
||||||
|
1.6.2
|
||||||
|
|
||||||
|
- disable keymap register for breakdowner on background
|
||||||
|
|
||||||
|
1.6.1
|
||||||
|
|
||||||
|
- removed: Auto Updater that was failing since 2.93
|
||||||
|
- prefs: add a checkbox to disable the "load base palette button" in UI.
|
||||||
|
|
||||||
|
1.6.0
|
||||||
|
|
||||||
|
- feat: Namespace upgrade
|
||||||
|
- support pseudo group naming
|
||||||
|
- add group and indent button
|
||||||
|
- feat: Fill tool shortcut for material color picker (from closest visible stroke)
|
||||||
|
- `S` : get material closest *fill* stroke
|
||||||
|
- `Àlt+S` : get closest stroke (fill or stroke)
|
||||||
|
|
||||||
|
1.5.8
|
||||||
|
|
||||||
|
- feat: Namespace improvement:
|
||||||
|
- new suffixes list that generate suffix buttons
|
||||||
|
- dynamic layer name field that show active (msgbus)
|
||||||
|
- possible to disable the panel with an option
|
||||||
|
|
||||||
|
1.5.7
|
||||||
|
|
||||||
|
- feat: check list, specify in addon pref if you prefer a dry run (check without set)
|
||||||
|
- feat: check file can change the lock object mode state (do nothing by default)
|
||||||
|
|
||||||
|
1.5.6
|
||||||
|
|
||||||
|
- feat: layer drop-down menu include an operator for batch find/replace GP layers (idname: `gp.rename_gp_layers`)
|
||||||
|
|
||||||
|
1.5.5
|
||||||
|
|
||||||
|
- feat: check file: add check for filepath mapping (if all relative or all absolute)
|
||||||
|
- change: check file: disable resolution set by default
|
||||||
|
|
||||||
|
1.5.4
|
||||||
|
|
||||||
|
- feat: Layer manager
|
||||||
|
- select/set layer prefix
|
||||||
|
- select/set layer color
|
||||||
|
- code: refactor name builder function
|
||||||
|
|
||||||
|
1.5.3
|
||||||
|
|
||||||
|
- feat: layer aquick-prefix for layer using pref separator
|
||||||
|
- list editable in addon pref
|
||||||
|
- add interface button above layers
|
||||||
|
- code: added environnement variable for prefix and separator:
|
||||||
|
- `PREFIXES` : list of prefix (comma separated uppercase letters between 1 and 6 character, ex: 'AN,SP,L')
|
||||||
|
- `SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
|
||||||
|
- UI: add addon prefs namespace ui-box in project settings
|
||||||
|
|
||||||
|
|
||||||
|
1.5.2
|
||||||
|
|
||||||
|
- add environnement variables to set addon preferences project settings at register through `os.getenv('KEY')`:
|
||||||
|
- `RENDER_WIDTH` : resolution x
|
||||||
|
- `RENDER_HEIGHT` : resolution y
|
||||||
|
- `FPS` : project frame rate
|
||||||
|
- `PALETTES` : path to the blends (or json) containing materials palettes
|
||||||
|
- `BRUSHES` : path to the blend containing brushes to load
|
||||||
|
|
||||||
|
1.5.1
|
||||||
|
|
||||||
|
- fix: eraser brush
|
||||||
|
- change: check file has custom check list in addon prefs
|
||||||
|
|
||||||
|
1.5.0
|
||||||
|
|
||||||
|
- feat: Eraser Brush Tool (Need to be enable in the preferences)
|
||||||
|
|
||||||
|
1.4.3
|
||||||
|
|
||||||
|
- feat: load brushes from blend
|
||||||
|
- ui: add load brushes within tool brush dropdown panel and in the top bar in drawmode
|
||||||
|
- pref: Set project brushes folder in addon preferences
|
||||||
|
|
||||||
|
1.4.2
|
||||||
|
|
||||||
|
- feat: new material cleaner in GP layer menu with 3 options
|
||||||
|
- clean material duplication (with sub option to not clear if color settings are different)
|
||||||
|
- fuse material slots that have the same materials
|
||||||
|
- remove empty slots
|
||||||
|
|
||||||
|
1.4.1
|
||||||
|
|
||||||
|
- fix: custom passepartout size limit when dezooming in camera
|
||||||
|
|
||||||
|
1.4.0
|
||||||
|
|
||||||
|
- feat: Passepartout displayed in main cam (permanent draw handler)
|
||||||
|
- UI: add Straight stroke operator button in Toolbox if GP tools official addon is on
|
||||||
|
- UI: placed Toolbox Playblast in a subpanel
|
||||||
|
- UI: removed Onion skin and Autolock layer checkbox (not often used)
|
||||||
|
- UI: sent rarely `cursor follow` to bottom of the panel
|
||||||
|
|
||||||
|
1.3.3
|
||||||
|
|
||||||
|
- feat: show main cam frame when in draw_cam
|
||||||
|
|
||||||
|
1.3.2
|
||||||
|
|
||||||
|
- change: disable manip cam name drawing
|
||||||
|
- code: add initial support for main cam frame draw within camera view
|
||||||
|
|
||||||
|
1.3.1
|
||||||
|
|
||||||
|
- fix: native refresh error that rarely happen that doesn't completely refresh the scene on keyframe jump.
|
||||||
|
- Use a double frame change to ensure refresh.
|
||||||
|
|
||||||
|
1.3.0
|
||||||
|
|
||||||
|
- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
|
||||||
|
|
||||||
|
1.2.2
|
||||||
|
|
||||||
|
- fix: realign anim return error
|
||||||
|
|
||||||
|
1.2.1
|
||||||
|
|
||||||
|
- fix: Breakdowner initial error check
|
||||||
|
|
||||||
|
1.2.0
|
||||||
|
|
||||||
|
- feat: New depth move operator that handle both perspective and orthographic cam
|
||||||
|
- feat: Realign, added drawing plane checkbox to autoset to Front after realign
|
||||||
|
- UI: Reorganised realign panel
|
||||||
|
- UI: Switched part of the sidebar items to columns intead of basic layout to gain space
|
||||||
|
- doc: Added changelog file (moved list from readme)
|
||||||
|
- doc: relative link to changelog and FR_readme in main readme
|
||||||
|
|
||||||
|
1.1.0
|
||||||
|
|
||||||
|
- Important change : Remap relative is now disabled by default in addon preferences
|
||||||
|
- feat: Add realign operator in sidebar with reproject as true by default
|
||||||
|
- UI: Batch reproject all frames is now in menus. Same places as native reproject
|
||||||
|
|
||||||
|
1.0.9
|
||||||
|
|
||||||
|
- feat: Reproject all frames operator
|
||||||
|
|
||||||
|
1.0.8
|
||||||
|
|
||||||
|
- feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified
|
||||||
|
|
||||||
|
1.0.7
|
||||||
|
|
||||||
|
- feat: Keyframe jump filter by type. User can now choose if the shortcut should jump on a specific keyframe type (All by default)
|
||||||
|
|
||||||
|
1.0.5
|
||||||
|
|
||||||
|
- GP copy-paste : Pasted stroke are now selected (allow to use it to quickly rip/split strokes with cut/paste on the same layer)
|
||||||
|
|
||||||
|
1.0.4
|
||||||
|
|
||||||
|
- UI: Better cam ref exposition in Toolbox panel
|
||||||
|
- Access to opacity
|
||||||
|
- merge activation bool with source type icon
|
||||||
|
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps)
|
||||||
|
|
||||||
|
1.0.3
|
||||||
|
|
||||||
|
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
|
||||||
|
|
||||||
|
1.0.2
|
||||||
|
|
||||||
|
- pref: Added option to disable always remap relative on save in addon-preference
|
||||||
|
|
||||||
|
1.0.1
|
||||||
|
|
||||||
|
- fix: copy paste problems
|
||||||
|
- Get points uv_properties (used for brushed points)
|
||||||
|
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
|
||||||
|
|
||||||
|
1.0.0
|
||||||
|
|
||||||
|
- Compatible with official grease pencil tools
|
||||||
|
- removed box deform and rotate canvas that existed in other
|
||||||
|
|
||||||
|
0.9.3
|
||||||
|
|
||||||
|
- feat: keyframe jump keys are now auto-binded
|
||||||
|
- UI: added keyframe jump customisation in addon pref
|
||||||
|
- code: split keyframe jump in a separate file with his new key updater
|
||||||
|
|
||||||
|
0.9.2
|
||||||
|
|
||||||
|
- doc: Correct download link (important, bugged the addon install) + update
|
||||||
|
- code: added tracker url
|
||||||
|
- updater: remove updater temp file, reset minimum version, turn off verbose mode
|
||||||
|
|
||||||
|
0.9.1
|
||||||
|
|
||||||
|
- Public release
|
||||||
|
- prefs: added fps as part of project settings
|
||||||
|
- check file use pref fps value (previously used harcoded 24fps value)
|
||||||
|
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
|
||||||
|
- update: apply changes in integrated copy-paste from the last version of standalone addon
|
||||||
|
- doc: Added fully-detailed french readme
|
||||||
|
|
||||||
|
|
||||||
|
0.8.0
|
||||||
|
|
||||||
|
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
|
||||||
|
- stripped associated properties from properties.py and passed as wm props.
|
||||||
|
|
||||||
|
0.7.2
|
||||||
|
|
||||||
|
- fix: Palette importer bug
|
||||||
|
|
||||||
|
0.7.0
|
||||||
|
|
||||||
|
- feat: auto create empty frame on color layer
|
||||||
|
|
||||||
|
0.6.3
|
||||||
|
|
||||||
|
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
|
||||||
|
|
||||||
|
0.6.2
|
||||||
|
|
||||||
|
- feat: colorisation, Option to change stop lines length
|
||||||
|
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
|
||||||
|
- Minor refactor for submodule register
|
||||||
|
|
||||||
|
0.6.1
|
||||||
|
|
||||||
|
- feat: render objects grouped, one anim render with all ticked object using manual output name
|
||||||
|
|
||||||
|
0.6.0
|
||||||
|
|
||||||
|
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
|
||||||
|
|
||||||
|
0.5.9
|
||||||
|
|
||||||
|
- feat: render exporter
|
||||||
|
- Render a selection of GP object isolated from the rest
|
||||||
|
- added exclusions names for GP object listing
|
||||||
|
- setup settings and output according to a name
|
||||||
|
- open render folder
|
||||||
|
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
|
||||||
|
- check file: set scene resolution to settings in prefs (default 2048x1080)
|
||||||
|
|
||||||
|
0.5.8
|
||||||
|
|
||||||
|
- feat: GP material append on active object from single blend file
|
||||||
|
|
||||||
|
0.5.7
|
||||||
|
|
||||||
|
- Added warning message for cursor snapping
|
||||||
|
|
||||||
|
0.5.6
|
||||||
|
|
||||||
|
- check file: added check for placement an projection mode for Gpencil.
|
||||||
|
- add a slider to change edit_lines_opacity globally for all GP data at once
|
||||||
|
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
|
||||||
|
|
||||||
|
0.5.4
|
||||||
|
|
||||||
|
- feat: anim manager in his own GP_toolbox submenu:
|
||||||
|
- button to list disabled anim (allow to quickly check state of the scene)
|
||||||
|
- disable/enable all fcurve in for GP object or other object separately to paint
|
||||||
|
- shift clic to target selection only
|
||||||
|
- check file: added disabled fcurved counter alert with detail in console
|
||||||
|
|
||||||
|
0.5.3
|
||||||
|
|
||||||
|
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
|
||||||
|
- check file option: change select active tool (choice added in addon preferences)
|
||||||
|
|
||||||
|
0.5.2
|
||||||
|
|
||||||
|
- Revert back obj_cam operator for following object (native lock view follow only translation)
|
||||||
|
- Changed method for canvas rotation to more robust rotate axis.
|
||||||
|
- Add operators on link checker to open containing folder/file of link
|
||||||
|
- Refactor: file checkers in their own file
|
||||||
|
|
||||||
|
0.5.1
|
||||||
|
|
||||||
|
- fix: error when empty material slot on GP object.
|
||||||
|
- fix: cursor snap on GP canvas when GP is parented
|
||||||
|
- change: Deleted obj cam (and related set view) operator
|
||||||
|
- change: blacker note background for playblast (stamp_background)
|
||||||
|
- feat: Always playblast from main camera (if in draw_cam)
|
||||||
|
- feat: Handler added to Remap relative on save (pre)
|
||||||
|
- ops: Check for broken links with porposition to find missing files
|
||||||
|
- ops: Added basic hardcoded file checker
|
||||||
|
- Lock main cam
|
||||||
|
- set scene percentage at 100
|
||||||
|
- set show slider and sync range
|
||||||
|
- set fps to 24
|
||||||
|
|
||||||
|
0.4.6
|
||||||
|
|
||||||
|
- feat: basic Palette manager with base material check and warning
|
||||||
|
|
||||||
|
0.4.5
|
||||||
|
|
||||||
|
- open blender config folder from addon preference
|
||||||
|
- fix: obj cam parent on selected object
|
||||||
|
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
|
||||||
|
- freeview : bug when rotating free viewfrom cardianl views
|
||||||
|
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
|
||||||
|
|
||||||
|
|
||||||
|
0.4.4
|
||||||
|
|
||||||
|
- feat: added cursor follow handlers and UI toggle
|
||||||
|
|
||||||
|
0.4.3
|
||||||
|
|
||||||
|
- change playblast out to 'images' and add playblast as name prefix
|
||||||
|
|
||||||
|
0.4.2
|
||||||
|
|
||||||
|
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
|
||||||
|
- fix: canvas rotate works with parented camera !
|
||||||
|
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
|
||||||
|
|
||||||
|
0.4.1
|
||||||
|
|
||||||
|
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
|
||||||
|
|
||||||
|
0.4.0
|
||||||
|
|
||||||
|
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
|
||||||
|
|
||||||
|
0.3.8
|
||||||
|
|
||||||
|
- UI: expose onion skin in interface
|
||||||
|
- UI: expose autolock in interface
|
||||||
|
- UI : putted tint layers in a submenu
|
||||||
|
- code: refactor, pushed most of class register in their owner file
|
||||||
|
- tool: tool to rename current or all grease pencil datablock with different name than container object
|
||||||
|
|
||||||
|
0.3.7
|
||||||
|
|
||||||
|
- UI: new interface with tabs for addon preferences
|
||||||
|
- UI: possible to disable color panel from preference (might be deleted if unusable)
|
||||||
|
- docs: change readme changelog format and correct doc
|
||||||
|
|
||||||
|
0.3.6
|
||||||
|
|
||||||
|
- UI: Stoplines : add a button for quickly set stoplines visibility.
|
||||||
|
|
||||||
|
0.3.5
|
||||||
|
|
||||||
|
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
|
||||||
|
- Fix: Enter key added to valid object-breakdown modal.
|
||||||
|
|
||||||
|
0.3.3
|
||||||
|
|
||||||
|
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
|
||||||
|
|
||||||
|
0.3.3
|
||||||
|
|
||||||
|
- version 1 beta of gmic colorize
|
||||||
|
- variant of `screen.gp_keyframe_jump` through keymap seetings
|
||||||
|
|
||||||
|
0.3.0
|
||||||
|
|
||||||
|
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
|
||||||
|
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
|
||||||
|
|
||||||
|
0.2.3
|
||||||
|
|
||||||
|
- add operator to `screen.gp_keyframe_jump`
|
||||||
|
- add shortcut to rotate canvas
|
||||||
|
- fix duplicate class
|
||||||
|
|
||||||
|
0.2.2
|
||||||
|
|
||||||
|
- separated props resolution_percentage parameter
|
||||||
|
- playblast options for launching folder and opening folder
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
|
||||||
|
- playblast feature
|
||||||
|
- Button to go zoom 100% or fit screen
|
||||||
|
- display scene resolution with res indicator
|
||||||
|
- Fix reference panel : works with video and display in a box layout.
|
||||||
|
- close pseudo-color panel by default (plan to move it to Gpencil tab)
|
||||||
|
|
||||||
|
0.2.0
|
||||||
|
|
||||||
|
- UI: Toggle camera background images from Toolbox panel
|
||||||
|
- UI: quick access to passepartout
|
||||||
|
- Feature: option to use namespace for pseudo color
|
||||||
|
|
||||||
|
0.1.5
|
||||||
|
|
||||||
|
- added CGC-auto-updater
|
||||||
|
|
||||||
|
0.1.3
|
||||||
|
|
||||||
|
- flip cam x
|
||||||
|
- inital stage of overlay toggle (need pref/multiple pref)
|
||||||
|
|
||||||
|
0.1.2
|
||||||
|
|
||||||
|
- subpanel of GP data (instead of direct append)
|
||||||
|
- initial commit with GP pseudo color
|
|
@ -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)
|
||||||
|
|
|
@ -1,33 +1,199 @@
|
||||||
## 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)
|
||||||
|
from .. import utils
|
||||||
|
from ..utils import is_hidden
|
||||||
|
|
||||||
|
## copied from OP_key_duplicate_send
|
||||||
|
def get_layer_list(self, context):
|
||||||
|
'''return (identifier, name, description) of enum content'''
|
||||||
|
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||||
|
|
||||||
|
def get_group_list(self, context):
|
||||||
|
return [(g.name, g.name, '') for g in context.object.data.layer_groups]
|
||||||
|
|
||||||
|
|
||||||
class GP_OT_create_empty_frames(bpy.types.Operator):
|
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 targeted layers\
|
||||||
|
\n(usefull in color layers to match line frames)"
|
||||||
bl_options = {'REGISTER','UNDO'}
|
bl_options = {'REGISTER','UNDO'}
|
||||||
|
|
||||||
|
layers_enum : EnumProperty(
|
||||||
|
name="Empty Keys from Layer",
|
||||||
|
description="Reference keys from layer",
|
||||||
|
items=get_layer_list
|
||||||
|
)
|
||||||
|
|
||||||
|
groups_enum : EnumProperty(
|
||||||
|
name="Empty Keys from Group",
|
||||||
|
description="Duplicate keys from group",
|
||||||
|
items=get_group_list
|
||||||
|
)
|
||||||
|
|
||||||
|
targeted_layers : EnumProperty(
|
||||||
|
name="Sources", # Empty keys from targets
|
||||||
|
description="Duplicate keys as empty on current layer from selected targets",
|
||||||
|
default="ALL_ABOVE",
|
||||||
|
items=(
|
||||||
|
('ALL_ABOVE', 'All Layers Above', 'Empty frames from all layers above'),
|
||||||
|
('ALL_BELOW', 'All Layers Below', 'Empty frames from all layers below'),
|
||||||
|
('NUMBER', 'Number Above Or Below', 'Positive number above layers\nNegative number below layers'),
|
||||||
|
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
|
||||||
|
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
|
||||||
|
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
|
||||||
|
('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'),
|
||||||
|
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
range : EnumProperty(
|
||||||
|
name="Range",
|
||||||
|
description="Restraint empty copy from a defined range",
|
||||||
|
default="FULL",
|
||||||
|
items=(
|
||||||
|
('FULL', 'Full range', 'Empty frames from all layers above'),
|
||||||
|
('BEFORE', 'Before Time Cursor', 'Empty frames from all layers below'),
|
||||||
|
('AFTER', 'After Time Cursor', 'Only After time cursor'),
|
||||||
|
('SCENE', 'On scene range', 'Restric to Scene/Preview range'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
number : IntProperty(name='Number',
|
||||||
|
default=1,
|
||||||
|
description='Number of layer to create empty key from\nabove (positive) or layer below (negative)',
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
@classmethod
|
@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 == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Possible preset with shortcut
|
||||||
|
# if event.alt:
|
||||||
|
# self.targeted_layers = 'ALL_VISIBLE'
|
||||||
|
gp = context.grease_pencil
|
||||||
|
layer_from_group = None
|
||||||
|
if gp.layer_groups.active:
|
||||||
|
layer_from_group = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
|
||||||
|
## Can just do if not utils.get_closest_active_layer(context.grease_pencil):
|
||||||
|
if not gp.layers.active and not layer_from_group:
|
||||||
|
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
# layout.label(text='Create Empty Frames From Other Layers')
|
||||||
|
# target
|
||||||
|
layout.prop(self, 'targeted_layers')
|
||||||
|
if self.targeted_layers == 'CHOSEN':
|
||||||
|
if self.layers_enum:
|
||||||
|
layout.prop(self, 'layers_enum')
|
||||||
|
else:
|
||||||
|
layout.label(text='No other layers to match keyframe!', icon='ERROR')
|
||||||
|
|
||||||
|
if self.targeted_layers == 'CHOSEN_GROUP':
|
||||||
|
if self.groups_enum:
|
||||||
|
layout.prop(self, 'groups_enum')
|
||||||
|
else:
|
||||||
|
layout.label(text='No other groups to match keyframe!', icon='ERROR')
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'NUMBER':
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(self, 'number')
|
||||||
|
row.active = self.number != 0
|
||||||
|
if self.number == 0:
|
||||||
|
layout.label(text="Can't have 0 as value")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.prop(self, 'range')
|
||||||
|
if self.range == 'SCENE':
|
||||||
|
if context.scene.use_preview_range:
|
||||||
|
layout.label(text='Using preview range', icon='INFO')
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
obj = context.object
|
obj = context.object
|
||||||
gpl = obj.data.layers
|
gp = obj.data
|
||||||
gpl.active_index
|
gpl = gp.layers
|
||||||
|
|
||||||
## Only possible on 'fill' layer ??
|
if gp.layer_groups.active:
|
||||||
# if not 'fill' in gpl.active.info.lower():
|
reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
|
||||||
# self.report({'ERROR'}, f"There must be 'fill' text in layer name")
|
else:
|
||||||
# return {'CANCELLED'}
|
reference_layer = gpl.active
|
||||||
|
|
||||||
|
active_index = next((i for i, l in enumerate(gpl) if l == reference_layer), None)
|
||||||
|
|
||||||
|
print(self.targeted_layers)
|
||||||
|
if self.targeted_layers == 'ALL_ABOVE':
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if i > active_index]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'ALL_BELOW':
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if i < active_index]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'ABOVE':
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'BELOW':
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'ALL_VISIBLE':
|
||||||
|
tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'CHOSEN':
|
||||||
|
if not self.layers_enum:
|
||||||
|
self.report({'ERROR'}, f"No chosen layers, aborted")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
tgt_layers = [l for l in gpl if l.name == self.layers_enum]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'CHOSEN_GROUP':
|
||||||
|
if not self.groups_enum:
|
||||||
|
self.report({'ERROR'}, f"No chosen groups, aborted")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
group = gp.layer_groups.get(self.groups_enum)
|
||||||
|
tgt_layers = [l for l in gpl if l.parent_group == group]
|
||||||
|
|
||||||
|
elif self.targeted_layers == 'NUMBER':
|
||||||
|
if self.number == 0:
|
||||||
|
self.report({'ERROR'}, f"Can't have 0 as value")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
l_range = active_index + self.number
|
||||||
|
print('l_range: ', l_range)
|
||||||
|
if self.number > 0: # positive
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range]
|
||||||
|
else:
|
||||||
|
tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range]
|
||||||
|
|
||||||
|
if not tgt_layers:
|
||||||
|
self.report({'ERROR'}, f"No layers found with chosen Targets")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
frame_id_list = []
|
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))
|
||||||
|
@ -40,17 +206,17 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
||||||
if num in current_frames:
|
if num in current_frames:
|
||||||
continue
|
continue
|
||||||
#Create empty frame
|
#Create empty frame
|
||||||
gpl.active.frames.new(num, active=False)
|
gpl.active.frames.new(num)
|
||||||
fct += 1
|
fct += 1
|
||||||
|
|
||||||
|
gpl.update()
|
||||||
if fct:
|
if fct:
|
||||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
|
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
|
||||||
else:
|
else:
|
||||||
self.report({'WARNING'}, f"No frames to create !")
|
self.report({'WARNING'}, f"No frames to create !")
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,14 @@ from ..utils import (location_to_region,
|
||||||
vector_length,
|
vector_length,
|
||||||
draw_gp_stroke,
|
draw_gp_stroke,
|
||||||
extrapolate_points_by_length,
|
extrapolate_points_by_length,
|
||||||
simple_draw_gp_stroke)
|
simple_draw_gp_stroke,
|
||||||
|
is_hidden,
|
||||||
|
is_locked)
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from math import degrees
|
from math import degrees
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
# from os.path import join, basename, exists, dirname, abspath, splitext
|
# from os.path import join, basename, exists, dirname, abspath, splitext
|
||||||
|
|
||||||
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
|
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
|
||||||
|
@ -53,11 +56,14 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
||||||
encounter = defaultdict(list)
|
encounter = defaultdict(list)
|
||||||
plist = []
|
plist = []
|
||||||
matrix = ob.matrix_world
|
matrix = ob.matrix_world
|
||||||
for s in f.strokes:#add first and last
|
for s in f.drawing.strokes: #add first and last
|
||||||
smat = ob.material_slots[s.material_index].material
|
smat = ob.material_slots[s.material_index].material
|
||||||
if not smat:continue#no material on line
|
if not smat:
|
||||||
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
|
continue #no material on line
|
||||||
if len(s.points) < 2:continue#avoid 0 or 1 points
|
if smat.grease_pencil.show_fill:
|
||||||
|
continue # skip fill lines -> #smat.grease_pencil.show_stroke
|
||||||
|
if len(s.points) < 2:
|
||||||
|
continue #avoid 0 or 1 points
|
||||||
plist.append(s.points[0])
|
plist.append(s.points[0])
|
||||||
plist.append(s.points[-1])
|
plist.append(s.points[-1])
|
||||||
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
|
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
|
||||||
|
@ -70,7 +76,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
||||||
for op in plist:#other points
|
for op in plist:#other points
|
||||||
if p == op:# print('same point')
|
if p == op:# print('same point')
|
||||||
continue
|
continue
|
||||||
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
|
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position))
|
||||||
# print('gap2d: ', gap2d)
|
# print('gap2d: ', gap2d)
|
||||||
if gap2d > tol:
|
if gap2d > tol:
|
||||||
continue
|
continue
|
||||||
|
@ -102,16 +108,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
||||||
encounter[p].append(op)
|
encounter[p].append(op)
|
||||||
|
|
||||||
|
|
||||||
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
|
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
|
||||||
ctl += 1
|
ctl += 1
|
||||||
|
|
||||||
print(f'{ctl} line created')
|
print(f'{ctl} line created')
|
||||||
|
|
||||||
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
|
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
|
||||||
|
|
||||||
def create_closing_line(tolerance=0.2):
|
def create_closing_line(tolerance=0.2):
|
||||||
for ob in bpy.context.selected_objects:
|
for ob in bpy.context.selected_objects:
|
||||||
if ob.type != 'GPENCIL':
|
if ob.type != 'GREASEPENCIL':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mat_id = get_closeline_mat(ob)# get a the closing material
|
mat_id = get_closeline_mat(ob)# get a the closing material
|
||||||
|
@ -128,7 +134,7 @@ def create_closing_line(tolerance=0.2):
|
||||||
## filter on selected
|
## filter on selected
|
||||||
if not l.select:continue# comment this line for all
|
if not l.select:continue# comment this line for all
|
||||||
# for f in l.frames:#not all for now
|
# for f in l.frames:#not all for now
|
||||||
f = l.active_frame
|
f = l.current_frame()
|
||||||
## create gap stroke
|
## create gap stroke
|
||||||
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
|
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
|
||||||
|
|
||||||
|
@ -143,9 +149,9 @@ def is_deviating_by(s, deviation=0.75):
|
||||||
pb = s.points[-2]
|
pb = s.points[-2]
|
||||||
pc = s.points[-3]
|
pc = s.points[-3]
|
||||||
|
|
||||||
a = location_to_region(pa.co)
|
a = location_to_region(pa.position)
|
||||||
b = location_to_region(pb.co)
|
b = location_to_region(pb.position)
|
||||||
c = location_to_region(pc.co)
|
c = location_to_region(pc.position)
|
||||||
|
|
||||||
#cb-> compare angle with ba->
|
#cb-> compare angle with ba->
|
||||||
angle = (b-c).angle(a-b)
|
angle = (b-c).angle(a-b)
|
||||||
|
@ -158,16 +164,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
|
||||||
'''extend line boundary by given length'''
|
'''extend line boundary by given length'''
|
||||||
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
|
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
|
||||||
## 2D mode
|
## 2D mode
|
||||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
|
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position)
|
||||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
|
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
|
||||||
# b = location_to_region(b_loc)
|
# b = location_to_region(b_loc)
|
||||||
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
|
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
|
||||||
# c_loc = region_to_location(c, b_loc)
|
# c_loc = region_to_location(c, b_loc)
|
||||||
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
|
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
|
||||||
|
|
||||||
## 3D
|
## 3D
|
||||||
a = s.points[id_pair[0]].co# ob.matrix_world @
|
a = s.points[id_pair[0]].position# ob.matrix_world @
|
||||||
b = s.points[id_pair[1]].co# ob.matrix_world @
|
b = s.points[id_pair[1]].position# ob.matrix_world @
|
||||||
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
|
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
|
||||||
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
|
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
|
||||||
|
|
||||||
|
@ -188,15 +194,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
|
||||||
|
|
||||||
## Change length of current length to designated
|
## Change length of current length to designated
|
||||||
# Vector point A to point B (direction), push point B in this direction
|
# Vector point A to point B (direction), push point B in this direction
|
||||||
a = s.points[-2].co
|
a = s.points[-2].position
|
||||||
bp = s.points[-1]#end-point
|
bp = s.points[-1]#end-point
|
||||||
b = bp.co
|
b = bp.position
|
||||||
ab = b - a
|
ab = b - a
|
||||||
if not ab:
|
if not ab:
|
||||||
continue
|
continue
|
||||||
# new pos of B is A + new length in the AB direction
|
# new pos of B is A + new length in the AB direction
|
||||||
newb = a + (ab.normalized() * length)
|
newb = a + (ab.normalized() * length)
|
||||||
bp.co = newb
|
bp.position = newb
|
||||||
ct += 1
|
ct += 1
|
||||||
|
|
||||||
return ct
|
return ct
|
||||||
|
@ -210,14 +216,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO need custom filters or go in GP refine strokes...
|
# TODO need custom filters or go in GP refine strokes...
|
||||||
# frame = ob.data.layers.active.active_frame
|
# frame = ob.data.layers.active.current_frame()
|
||||||
|
|
||||||
if not frame: return
|
if not frame: return
|
||||||
ct = 0
|
ct = 0
|
||||||
#TODO need to delete previous closing lines on frame before launching
|
#TODO need to delete previous closing lines on frame before launching
|
||||||
|
|
||||||
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
|
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in !
|
||||||
for s in list(frame.strokes):
|
for s in list(frame.drawing.strokes):
|
||||||
if s.material_index == mat_id:#is a closeline
|
if s.material_index == mat_id:#is a closeline
|
||||||
continue
|
continue
|
||||||
if len(s.points) < 2:#not enough point to evaluate
|
if len(s.points) < 2:#not enough point to evaluate
|
||||||
|
@ -241,7 +247,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
||||||
|
|
||||||
@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 == 'GREASEPENCIL'
|
||||||
|
|
||||||
# mode : bpy.props.StringProperty(
|
# mode : bpy.props.StringProperty(
|
||||||
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
|
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
|
||||||
|
@ -273,18 +279,18 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
||||||
if self.layer_tgt == 'ACTIVE':
|
if self.layer_tgt == 'ACTIVE':
|
||||||
lays = [ob.data.layers.active]
|
lays = [ob.data.layers.active]
|
||||||
elif self.layer_tgt == 'SELECTED':
|
elif self.layer_tgt == 'SELECTED':
|
||||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||||
lays = [l for l in ob.data.layers if not l.hide]
|
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||||
else:
|
else:
|
||||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||||
|
|
||||||
fct = 0
|
fct = 0
|
||||||
for l in lays:
|
for l in lays:
|
||||||
if not l.active_frame:
|
if not l.current_frame():
|
||||||
print(f'{l.info} has no active frame')
|
print(f'{l.name} has no active frame')
|
||||||
continue
|
continue
|
||||||
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
|
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
|
||||||
|
|
||||||
if not fct:
|
if not fct:
|
||||||
mess = "No strokes extended... see console"
|
mess = "No strokes extended... see console"
|
||||||
|
@ -306,7 +312,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
||||||
|
|
||||||
@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 == 'GREASEPENCIL'
|
||||||
|
|
||||||
layer_tgt : bpy.props.EnumProperty(
|
layer_tgt : bpy.props.EnumProperty(
|
||||||
name="Extend layers", description="Choose which layer to target",
|
name="Extend layers", description="Choose which layer to target",
|
||||||
|
@ -334,18 +340,18 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
||||||
if self.layer_tgt == 'ACTIVE':
|
if self.layer_tgt == 'ACTIVE':
|
||||||
lays = [ob.data.layers.active]
|
lays = [ob.data.layers.active]
|
||||||
elif self.layer_tgt == 'SELECTED':
|
elif self.layer_tgt == 'SELECTED':
|
||||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||||
lays = [l for l in ob.data.layers if not l.hide]
|
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||||
else:
|
else:
|
||||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||||
|
|
||||||
fct = 0
|
fct = 0
|
||||||
for l in lays:
|
for l in lays:
|
||||||
if not l.active_frame:
|
if not l.current_frame():
|
||||||
print(f'{l.info} has no active frame')
|
print(f'{l.name} has no active frame')
|
||||||
continue
|
continue
|
||||||
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
|
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
|
||||||
|
|
||||||
if not fct:
|
if not fct:
|
||||||
mess = "No extension modified... see console"
|
mess = "No extension modified... see console"
|
||||||
|
@ -367,15 +373,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
|
||||||
|
|
||||||
@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 == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ct = 0
|
ct = 0
|
||||||
ob = context.object
|
ob = context.object
|
||||||
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
|
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||||
for l in lays:
|
for l in lays:
|
||||||
if not l.active_frame:continue
|
if not l.current_frame():continue
|
||||||
for s in l.active_frame.strokes:
|
for s in l.current_frame().drawing.strokes:
|
||||||
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
|
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
|
||||||
ct+=1
|
ct+=1
|
||||||
|
|
||||||
|
@ -397,7 +403,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (context.object is not None)# and context.object.type == 'GPENCIL'
|
return (context.object is not None)# and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
## draw stuff inside the header (place before main label)
|
## draw stuff inside the header (place before main label)
|
||||||
# def draw_header(self, context):
|
# def draw_header(self, context):
|
||||||
|
@ -414,7 +420,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
||||||
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
|
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
|
||||||
|
|
||||||
#diplay closeline visibility
|
#diplay closeline visibility
|
||||||
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
|
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
|
||||||
row=layout.row()
|
row=layout.row()
|
||||||
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
|
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
|
||||||
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
|
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
|
||||||
|
|
|
@ -211,16 +211,23 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
||||||
## cursors
|
## cursors
|
||||||
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
|
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
|
||||||
## start checks
|
## start checks
|
||||||
message = None
|
if context.area.type != 'VIEW_3D':
|
||||||
if context.area.type != 'VIEW_3D':message = 'View3D not found, cannot run operator'
|
self.report({'WARNING'}, 'View3D not found, cannot run operator')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
obj = bpy.context.object# better use self.context
|
obj = bpy.context.object# better use self.context
|
||||||
if not obj:message = 'no active object'
|
if not obj:
|
||||||
|
self.report({'WARNING'}, 'No active object')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
anim_data = obj.animation_data
|
anim_data = obj.animation_data
|
||||||
if not anim_data:message = f'no animation data on obj: {obj.name}'
|
if not anim_data:
|
||||||
|
self.report({'WARNING'}, f'No animation data on obj: {obj.name}')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
action = anim_data.action
|
action = anim_data.action
|
||||||
if not action:message = f'no action on animation data of obj: {obj.name}'
|
if not action:
|
||||||
if message:
|
self.report({'WARNING'}, f'No action on animation data of obj: {obj.name}')
|
||||||
self.report({'WARNING'}, message)# ERROR
|
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
## initiate variable to use
|
## initiate variable to use
|
||||||
|
@ -295,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
|
||||||
|
@ -313,26 +322,25 @@ 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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import bpy
|
||||||
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
from pathlib import Path
|
||||||
|
from .utils import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
|
def get_brushes(blend_fp):
|
||||||
|
'''Get all brush from passed blend that aren't already in there (can take Path object)'''
|
||||||
|
cur_brushes = [b.name for b in bpy.data.brushes]
|
||||||
|
with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
|
||||||
|
# load brushes if not already there
|
||||||
|
data_to.brushes = [b for b in data_from.brushes if not b in cur_brushes]
|
||||||
|
|
||||||
|
## force fake user for appended the brushes
|
||||||
|
for b in data_to.brushes:
|
||||||
|
print(f'Append Brush: {b.name}')
|
||||||
|
b.use_fake_user = True
|
||||||
|
|
||||||
|
return len(data_to.brushes)
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
|
||||||
|
bl_idname = "gp.load_brushes"
|
||||||
|
bl_label = "Load Brushes"
|
||||||
|
bl_description = "Load all brushes from chosen blend file in current if brushes aren't already there\nIf a replacement is needed, delete the previous brush before"
|
||||||
|
#bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def poll(cls, context):
|
||||||
|
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
filename_ext = '.blend'
|
||||||
|
|
||||||
|
filter_glob: bpy.props.StringProperty(default='*.blend', options={'HIDDEN'} )
|
||||||
|
|
||||||
|
filepath : bpy.props.StringProperty(
|
||||||
|
name="File Path",
|
||||||
|
description="File path used for import",
|
||||||
|
maxlen= 1024)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
print(f'Appending brushes from file : {self.filepath}')
|
||||||
|
bct = get_brushes(self.filepath)
|
||||||
|
if bct:
|
||||||
|
self.report({'INFO'}, f'{bct} brushes appended')
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, 'Brushes are already there (if need to re-import, delete first)')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_brush_set(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.brush_set"
|
||||||
|
bl_label = "Set Brush"
|
||||||
|
bl_description = "Set Gpencil brush"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
brush_name : bpy.props.StringProperty(name='Brush', description='Name of the brush to use')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
brush = bpy.data.brushes.get(self.brush_name)
|
||||||
|
if not brush:
|
||||||
|
self.report({'ERROR'}, f'Brush "{self.brush_name}" not found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
context.scene.tool_settings.gpencil_paint.brush = brush
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
### -- MENU ENTRY --
|
||||||
|
|
||||||
|
def load_brush_ui(self, context):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||||
|
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
|
||||||
|
|
||||||
|
def load_brush_top_bar_ui(self, context):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||||
|
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_load_brushes,
|
||||||
|
GPTB_OT_brush_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cl in classes:
|
||||||
|
bpy.utils.register_class(cl)
|
||||||
|
|
||||||
|
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui)
|
||||||
|
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
|
||||||
|
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
|
||||||
|
|
||||||
|
for cl in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cl)
|
472
OP_copy_paste.py
472
OP_copy_paste.py
|
@ -1,38 +1,15 @@
|
||||||
# This program is free software; you can redistribute it and/or modify
|
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
|
||||||
# it under the terms of the GNU General Public License as published by
|
## View3D > Toolbar > Gpencil > GP clipboard
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but
|
|
||||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
## based on GPclipboard 1.3.2 (just stripped addon prefs)
|
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name": "GP clipboard",
|
|
||||||
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
|
|
||||||
"author": "Samuel Bernou",
|
|
||||||
"version": (1, 3, 2),
|
|
||||||
"blender": (2, 83, 0),
|
|
||||||
"location": "View3D > Toolbar > Gpencil > GP clipboard",
|
|
||||||
"warning": "",
|
|
||||||
"doc_url": "https://github.com/Pullusb/GP_clipboard",
|
|
||||||
"category": "Object" }
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import os
|
|
||||||
import mathutils
|
import mathutils
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
import json
|
import json
|
||||||
from time import time
|
from time import time
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
# from pprint import pprint
|
from .utils import is_locked, is_hidden
|
||||||
|
|
||||||
def convertAttr(Attr):
|
def convertAttr(Attr):
|
||||||
'''Convert given value to a Json serializable format'''
|
'''Convert given value to a Json serializable format'''
|
||||||
|
@ -45,100 +22,109 @@ def convertAttr(Attr):
|
||||||
else:
|
else:
|
||||||
return(Attr)
|
return(Attr)
|
||||||
|
|
||||||
def getMatrix (layer) :
|
def getMatrix(layer) :
|
||||||
matrix = mathutils.Matrix.Identity(4)
|
matrix = mathutils.Matrix.Identity(4)
|
||||||
|
|
||||||
if layer.is_parented:
|
if parent := layer.parent:
|
||||||
if layer.parent_type == 'BONE':
|
if parent.type == 'ARMATURE' and layer.parent_bone:
|
||||||
object = layer.parent
|
bone = parent.pose.bones[layer.parent_bone]
|
||||||
bone = object.pose.bones[layer.parent_bone]
|
matrix = bone.matrix @ parent.matrix_world
|
||||||
matrix = bone.matrix @ object.matrix_world
|
matrix = matrix.copy() @ layer.matrix_parent_inverse
|
||||||
matrix = matrix.copy() @ layer.matrix_inverse
|
else:
|
||||||
else :
|
matrix = parent.matrix_world @ layer.matrix_parent_inverse
|
||||||
matrix = layer.parent.matrix_world @ layer.matrix_inverse
|
|
||||||
|
|
||||||
return matrix.copy()
|
return matrix.copy()
|
||||||
|
|
||||||
default_pt_uv_fill = Vector((0.5, 0.5))
|
# default_pt_uv_fill = Vector((0.5, 0.5))
|
||||||
|
|
||||||
def dump_gp_point(p, l, obj):
|
def dump_gp_point(p, l, obj,
|
||||||
|
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
|
||||||
'''add properties of a given points to a dic and return it'''
|
'''add properties of a given points to a dic and return it'''
|
||||||
pdic = {}
|
point_dict = {}
|
||||||
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
|
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
|
||||||
#for att in point_attr_list:
|
#for att in point_attr_list:
|
||||||
# pdic[att] = convertAttr(getattr(p, att))
|
# point_dict[att] = convertAttr(getattr(p, att))
|
||||||
if l.parent:
|
if l.parent:
|
||||||
mat = getMatrix(l)
|
mat = getMatrix(l)
|
||||||
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
|
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
|
||||||
else:
|
else:
|
||||||
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
|
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
|
||||||
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
|
|
||||||
# pdic['select'] = convertAttr(getattr(p,'select'))# need selection ?
|
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
|
||||||
pdic['strength'] = convertAttr(getattr(p,'strength'))
|
if radius and p.radius != 1.0:
|
||||||
|
point_dict['radius'] = convertAttr(getattr(p,'radius'))
|
||||||
|
|
||||||
|
if opacity and p.opacity != 1.0:
|
||||||
|
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
|
||||||
|
|
||||||
## get vertex color (long...)
|
## 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)
|
point_dict['vertex_color'] = convertAttr(p.vertex_color)
|
||||||
|
|
||||||
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
|
if rotation and p.rotation != 0.0:
|
||||||
if p.uv_fill != default_pt_uv_fill:
|
point_dict['rotation'] = convertAttr(p.rotation)
|
||||||
pdic['uv_fill'] = convertAttr(p.uv_fill)
|
|
||||||
if p.uv_factor != 0.0:
|
|
||||||
pdic['uv_factor'] = convertAttr(p.uv_factor)
|
|
||||||
if p.uv_rotation != 0.0:
|
|
||||||
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
|
|
||||||
|
|
||||||
return pdic
|
## No time infos
|
||||||
|
# if delta_time and p.delta_time != 0.0:
|
||||||
|
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
|
||||||
|
|
||||||
|
return point_dict
|
||||||
|
|
||||||
def dump_gp_stroke_range(s, sid, l, obj):
|
def dump_gp_stroke_range(s, sid, l, obj,
|
||||||
|
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||||
'''Get a grease pencil stroke and return a dic with attribute
|
'''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)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
sdic = {}
|
stroke_dict = {}
|
||||||
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
|
# stroke_attr_list = ('line_width',)
|
||||||
for att in stroke_attr_list:
|
# for att in stroke_attr_list:
|
||||||
sdic[att] = getattr(s, att)
|
# stroke_dict[att] = getattr(s, att)
|
||||||
|
|
||||||
## Dump following these value only if they are non default
|
## Dump following these value only if they are non default
|
||||||
if s.material_index != 0:
|
if s.material_index != 0:
|
||||||
sdic['material_index'] = s.material_index
|
stroke_dict['material_index'] = s.material_index
|
||||||
|
|
||||||
if getattr(s, 'draw_cyclic', None):# pre-2.92
|
if s.cyclic:
|
||||||
sdic['draw_cyclic'] = s.draw_cyclic
|
stroke_dict['cyclic'] = s.cyclic
|
||||||
|
|
||||||
if getattr(s, 'use_cyclic', None):# from 2.92
|
if s.softness != 0.0:
|
||||||
sdic['use_cyclic'] = s.use_cyclic
|
stroke_dict['softness'] = s.softness
|
||||||
|
|
||||||
if s.uv_scale != 1.0:
|
if s.aspect_ratio != 1.0:
|
||||||
sdic['uv_scale'] = s.uv_scale
|
stroke_dict['aspect_ratio'] = s.aspect_ratio
|
||||||
|
|
||||||
if s.uv_rotation != 0.0:
|
if s.start_cap != 0:
|
||||||
sdic['uv_rotation'] = s.uv_rotation
|
stroke_dict['start_cap'] = s.start_cap
|
||||||
|
|
||||||
if s.hardness != 1.0:
|
if s.end_cap != 0:
|
||||||
sdic['hardness'] = s.hardness
|
stroke_dict['end_cap'] = s.end_cap
|
||||||
|
|
||||||
if s.uv_translation != Vector((0.0, 0.0)):
|
if fill_color and s.fill_color[:] != (0,0,0,0):
|
||||||
sdic['uv_translation'] = convertAttr(s.uv_translation)
|
stroke_dict['fill_color'] = convertAttr(s.fill_color)
|
||||||
|
|
||||||
if s.vertex_color_fill[:] != (0,0,0,0):
|
if fill_opacity and s.fill_opacity != 0.0:
|
||||||
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
|
stroke_dict['fill_opacity'] = s.fill_opacity
|
||||||
|
|
||||||
|
## No time infos
|
||||||
|
# if s.time_start != 0.0:
|
||||||
|
# stroke_dict['time_start'] = s.time_start
|
||||||
|
|
||||||
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,
|
||||||
|
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=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,
|
||||||
sdic['points'] = points
|
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||||
return sdic
|
|
||||||
|
stroke_dict['points'] = points
|
||||||
|
return stroke_dict
|
||||||
|
|
||||||
|
|
||||||
|
def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
||||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow filter)
|
|
||||||
'''
|
'''
|
||||||
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
|
||||||
|
@ -153,46 +139,50 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
||||||
# if not color:#get active color name
|
# if not color:#get active color name
|
||||||
# color = gp.palettes.active.colors.active.name
|
# color = gp.palettes.active.colors.active.name
|
||||||
if not layers:
|
if not layers:
|
||||||
#by default all visible layers
|
# by default all visible layers
|
||||||
layers = [l for l in gpl if not l.hide and not l.lock]#[]
|
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
|
||||||
if not isinstance(layers, list):
|
if not isinstance(layers, list):
|
||||||
#if a single layer object is send put in a list
|
# if a single layer object is send put in a list
|
||||||
layers = [layers]
|
layers = [layers]
|
||||||
|
|
||||||
stroke_list = []#one stroke list for all layers.
|
stroke_list = [] # one stroke list for all layers.
|
||||||
|
|
||||||
for l in layers:
|
for l in layers:
|
||||||
f = l.active_frame
|
f = l.current_frame()
|
||||||
|
|
||||||
if f:#active frame can be None
|
if f: # active frame can be None
|
||||||
if not copy:
|
if not copy:
|
||||||
staylist = []#init part of strokes that must survive on this layer
|
staylist = [] # init part of strokes that must survive on this layer
|
||||||
|
|
||||||
for s in f.strokes:
|
rm_list = [] # init strokes that must be removed from this layer
|
||||||
if s.select:
|
for s_index, stroke in enumerate(f.drawing.strokes):
|
||||||
|
if stroke.select:
|
||||||
# separate in multiple stroke if parts of the strokes a selected.
|
# separate in multiple stroke if parts of the strokes a selected.
|
||||||
sel = [i for i, p in enumerate(s.points) if p.select]
|
sel = [i for i, p in enumerate(stroke.points) if p.select]
|
||||||
substrokes = []# list of list containing isolated selection
|
substrokes = [] # list of list containing isolated selection
|
||||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
|
|
||||||
|
# continuity stroke have same substract result between point index and enumerator
|
||||||
|
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
|
||||||
group = list(map(itemgetter(1), g))
|
group = list(map(itemgetter(1), g))
|
||||||
substrokes.append(group)
|
substrokes.append(group)
|
||||||
|
|
||||||
for ss in substrokes:
|
for ss in substrokes:
|
||||||
if len(ss) > 1:#avoid copy isolated points
|
if len(ss) > 1: # avoid copy isolated points
|
||||||
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj))
|
||||||
|
|
||||||
#Cutting operation
|
# Cutting operation
|
||||||
if not copy:
|
if not copy:
|
||||||
maxindex = len(s.points)-1
|
maxindex = len(stroke.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.drawing.strokes.remove(stroke) # gpv2
|
||||||
|
rm_list.append(s_index)
|
||||||
else:
|
else:
|
||||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
neg = [i for i, p in enumerate(stroke.points) if not p.select]
|
||||||
|
|
||||||
staying = []
|
staying = []
|
||||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||||
group = list(map(itemgetter(1), g))
|
group = list(map(itemgetter(1), g))
|
||||||
#extend group to avoid gap when cut, a bit dirty
|
# extend group to avoid gap when cut, a bit dirty
|
||||||
if group[0] > 0:
|
if group[0] > 0:
|
||||||
group.insert(0,group[0]-1)
|
group.insert(0,group[0]-1)
|
||||||
if group[-1] < maxindex:
|
if group[-1] < maxindex:
|
||||||
|
@ -201,37 +191,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
||||||
|
|
||||||
for ns in staying:
|
for ns in staying:
|
||||||
if len(ns) > 1:
|
if len(ns) > 1:
|
||||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
|
||||||
#make a negative list containing all last index
|
# make a negative list containing all last index
|
||||||
|
|
||||||
|
if rm_list:
|
||||||
'''#full stroke version
|
f.drawing.remove_strokes(indices=rm_list)
|
||||||
# if s.colorname == color: #line for future filters
|
|
||||||
stroke_list.append(dump_gp_stroke(s,l))
|
|
||||||
#delete stroke on the fly
|
|
||||||
if not copy:
|
|
||||||
f.strokes.remove(s)
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not copy:
|
if not copy:
|
||||||
|
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
|
||||||
|
|
||||||
# delete all selected strokes...
|
# delete all selected strokes...
|
||||||
for s in f.strokes:
|
if selected_ids:
|
||||||
if s.select:
|
f.drawing.remove_strokes(indices=selected_ids)
|
||||||
f.strokes.remove(s)
|
|
||||||
# ...recreate these uncutted ones
|
# ...recreate these uncutted ones
|
||||||
#pprint(staylist)
|
#pprint(staylist)
|
||||||
if staylist:
|
if staylist:
|
||||||
add_multiple_strokes(staylist, l)
|
add_multiple_strokes(staylist, l)
|
||||||
#for ns in staylist:#weirdly recreate the stroke twice !
|
|
||||||
# add_stroke(ns, f, l)
|
|
||||||
|
|
||||||
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
# If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
||||||
if not copy and not keep_empty:#
|
if not copy and not keep_empty:
|
||||||
if not len(f.strokes):
|
if not len(f.drawing.strokes):
|
||||||
l.frames.remove(f)
|
l.frames.remove(f)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||||
#print(stroke_list)
|
#print(stroke_list)
|
||||||
return stroke_list
|
return stroke_list
|
||||||
|
@ -253,7 +236,7 @@ def copy_all_strokes(layers=None):
|
||||||
|
|
||||||
if not layers:
|
if not layers:
|
||||||
# by default all visible layers
|
# by default all visible layers
|
||||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
|
||||||
if not isinstance(layers, list):
|
if not isinstance(layers, list):
|
||||||
# if a single layer object is send put in a list
|
# if a single layer object is send put in a list
|
||||||
layers = [layers]
|
layers = [layers]
|
||||||
|
@ -261,12 +244,12 @@ def copy_all_strokes(layers=None):
|
||||||
stroke_list = []# one stroke list for all layers.
|
stroke_list = []# one stroke list for all layers.
|
||||||
|
|
||||||
for l in layers:
|
for l in layers:
|
||||||
f = l.active_frame
|
f = l.current_frame()
|
||||||
|
|
||||||
if not f:
|
if not f:
|
||||||
continue# active frame can be None
|
continue# active frame can be None
|
||||||
|
|
||||||
for s in f.strokes:
|
for s in f.drawing.strokes:
|
||||||
## full stroke version
|
## full stroke version
|
||||||
# if s.select:
|
# if s.select:
|
||||||
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
|
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
|
||||||
|
@ -276,11 +259,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,
|
||||||
|
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||||
'''
|
'''
|
||||||
copy all stroke, not affected by selection on active frame
|
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
|
||||||
|
@ -293,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
||||||
|
|
||||||
if not layers:
|
if not layers:
|
||||||
# by default all visible layers
|
# by default all visible layers
|
||||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
|
||||||
if not isinstance(layers, list):
|
if not isinstance(layers, list):
|
||||||
# if a single layer object is send put in a list
|
# if a single layer object is send put in a list
|
||||||
layers = [layers]
|
layers = [layers]
|
||||||
|
@ -301,68 +284,69 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
||||||
stroke_list = []
|
stroke_list = []
|
||||||
|
|
||||||
for l in layers:
|
for l in layers:
|
||||||
f = l.active_frame
|
f = l.current_frame()
|
||||||
|
|
||||||
if not f:
|
if not f:
|
||||||
continue# active frame can be None
|
continue# active frame can be None
|
||||||
|
|
||||||
for s in f.strokes:
|
for s in f.drawing.strokes:
|
||||||
## 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,
|
||||||
|
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
|
||||||
|
|
||||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
# print(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):
|
def add_stroke(s, frame, layer, obj, select=False):
|
||||||
'''add stroke on a given frame, (layer is for parentage setting)'''
|
'''add stroke on a given frame, (layer is for parentage setting)'''
|
||||||
# print(3*'-',s)
|
# print(3*'-',s)
|
||||||
ns = frame.strokes.new()
|
pts_to_add = len(s['points'])
|
||||||
|
frame.drawing.add_strokes([pts_to_add])
|
||||||
|
|
||||||
|
ns = frame.drawing.strokes[-1]
|
||||||
|
|
||||||
|
## set strokes atrributes
|
||||||
for att, val in s.items():
|
for att, val in s.items():
|
||||||
if att not in ('points'):
|
if att not in ('points'):
|
||||||
setattr(ns, att, val)
|
setattr(ns, att, val)
|
||||||
pts_to_add = len(s['points'])
|
|
||||||
# print(pts_to_add, 'points')#dbg
|
|
||||||
|
|
||||||
ns.points.add(pts_to_add)
|
|
||||||
|
|
||||||
ob_mat_inv = obj.matrix_world.inverted()
|
ob_mat_inv = obj.matrix_world.inverted()
|
||||||
|
|
||||||
## patch pressure 1
|
if layer.parent:
|
||||||
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
|
layer_matrix = getMatrix(layer).inverted()
|
||||||
|
transform_matrix = ob_mat_inv @ layer_matrix
|
||||||
if layer.is_parented:
|
|
||||||
mat = getMatrix(layer).inverted()
|
|
||||||
for i, pt in enumerate(s['points']):
|
|
||||||
for k, v in pt.items():
|
|
||||||
if k == 'co':
|
|
||||||
setattr(ns.points[i], k, v)
|
|
||||||
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
|
|
||||||
else:
|
|
||||||
setattr(ns.points[i], k, v)
|
|
||||||
else:
|
else:
|
||||||
for i, pt in enumerate(s['points']):
|
transform_matrix = ob_mat_inv
|
||||||
for k, v in pt.items():
|
|
||||||
if k == 'co':
|
|
||||||
setattr(ns.points[i], k, v)
|
|
||||||
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
|
|
||||||
else:
|
|
||||||
setattr(ns.points[i], k, v)
|
|
||||||
|
|
||||||
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
|
## Set points attributes
|
||||||
ns.points.update()
|
for i, pt in enumerate(s['points']):
|
||||||
|
for k, v in pt.items():
|
||||||
|
if k == 'position':
|
||||||
|
setattr(ns.points[i], k, v)
|
||||||
|
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
|
||||||
|
else:
|
||||||
|
setattr(ns.points[i], k, v)
|
||||||
|
if select:
|
||||||
|
ns.points[i].select = True
|
||||||
|
|
||||||
## patch pressure 2
|
## Opacity initialized at 0.0 (should be 1.0)
|
||||||
# ns.points.foreach_set('pressure', pressure_flat_list)
|
if not 'opacity' in pt:
|
||||||
|
ns.points[i].opacity = 1.0
|
||||||
|
|
||||||
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
|
## Radius initialized at 0.0 (should probably be 0.01)
|
||||||
|
if not 'radius' in pt:
|
||||||
|
ns.points[i].radius = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
|
||||||
'''
|
'''
|
||||||
add a list of strokes to active frame of given layer
|
add a list of strokes to active frame of given layer
|
||||||
if no layer specified, active layer is used
|
if no layer specified, active layer is used
|
||||||
if use_current_frame is True, a new frame will be created only if needed
|
if use_current_frame is True, a new frame will be created only if needed
|
||||||
|
if select is True, newly added strokes are 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
|
||||||
|
@ -375,25 +359,23 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
|
||||||
|
|
||||||
fnum = scene.frame_current
|
fnum = scene.frame_current
|
||||||
target_frame = False
|
target_frame = False
|
||||||
act = layer.active_frame
|
act = layer.current_frame()
|
||||||
|
## set frame if needed
|
||||||
|
if act:
|
||||||
|
if use_current_frame or act.frame_number == fnum:
|
||||||
|
#work on current frame if exists
|
||||||
|
# use current frame anyway if one key exist at this scene.frame
|
||||||
|
target_frame = act
|
||||||
|
|
||||||
|
if not target_frame:
|
||||||
|
#no active frame
|
||||||
|
#or active exists but not aligned scene.current with use_current_frame disabled
|
||||||
|
target_frame = layer.frames.new(fnum)
|
||||||
|
|
||||||
for s in stroke_list:
|
for s in stroke_list:
|
||||||
if act:
|
add_stroke(s, target_frame, layer, obj, select=select)
|
||||||
if use_current_frame or act.frame_number == fnum:
|
|
||||||
#work on current frame if exists
|
|
||||||
# use current frame anyway if one key exist at this scene.frame
|
|
||||||
target_frame = act
|
|
||||||
|
|
||||||
if not target_frame:
|
# print(len(stroke_list), 'strokes pasted')
|
||||||
#no active frame
|
|
||||||
#or active exists but not aligned scene.current with use_current_frame disabled
|
|
||||||
target_frame = layer.frames.new(fnum)
|
|
||||||
|
|
||||||
add_stroke(s, target_frame, layer, obj)
|
|
||||||
'''
|
|
||||||
for s in stroke_data:
|
|
||||||
add_stroke(s, target_frame)
|
|
||||||
'''
|
|
||||||
print(len(stroke_list), 'strokes pasted')
|
|
||||||
|
|
||||||
|
|
||||||
### OPERATORS
|
### OPERATORS
|
||||||
|
@ -401,24 +383,24 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
|
||||||
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)
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# if not context.object or not context.object.type == 'GPENCIL':
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||||
# self.report({'ERROR'},'No GP object selected')
|
# self.report({'ERROR'},'No GP object selected')
|
||||||
# return {"CANCELLED"}
|
# return {"CANCELLED"}
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
#ct = check_pressure()
|
#ct = check_radius()
|
||||||
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:
|
||||||
|
@ -431,20 +413,20 @@ 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", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# if not context.object or not context.object.type == 'GPENCIL':
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||||
# self.report({'ERROR'},'No GP object selected')
|
# self.report({'ERROR'},'No GP object selected')
|
||||||
# return {"CANCELLED"}
|
# return {"CANCELLED"}
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
strokelist = copycut_strokes(copy=False, keep_empty=True)#ct = check_pressure()
|
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
|
||||||
if not strokelist:
|
if not strokelist:
|
||||||
self.report({'ERROR'},'Nothing to cut')
|
self.report({'ERROR'},'Nothing to cut')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
@ -461,10 +443,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# if not context.object or not context.object.type == 'GPENCIL':
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||||
# self.report({'ERROR'},'No GP object selected to paste on')
|
# self.report({'ERROR'},'No GP object selected to paste on')
|
||||||
# return {"CANCELLED"}
|
# return {"CANCELLED"}
|
||||||
|
|
||||||
|
@ -478,7 +460,7 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
print('data loaded', time() - t0)
|
print('data loaded', time() - t0)
|
||||||
add_multiple_strokes(data, use_current_frame=True)
|
add_multiple_strokes(data, use_current_frame=True, select=True)
|
||||||
print('total_time', time() - t0)
|
print('total_time', time() - t0)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
@ -487,14 +469,47 @@ 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)
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
radius : bpy.props.BoolProperty(name='radius', default=True,
|
||||||
|
description='Dump point radius attribute (already skipped if at default value)')
|
||||||
|
opacity : bpy.props.BoolProperty(name='opacity', default=True,
|
||||||
|
description='Dump point opacity attribute (already skipped if at default value)')
|
||||||
|
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
|
||||||
|
description='Dump point vertex_color attribute (already skipped if at default value)')
|
||||||
|
fill_color : bpy.props.BoolProperty(name='fill color', default=True,
|
||||||
|
description='Dump point fill_color attribute (already skipped if at default value)')
|
||||||
|
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
|
||||||
|
description='Dump point fill_opacity attribute (already skipped if at default value)')
|
||||||
|
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
|
||||||
|
description='Dump point uv_factor attribute (already skipped if at default value)')
|
||||||
|
rotation : bpy.props.BoolProperty(name='rotation', default=True,
|
||||||
|
description='Dump point rotation attribute (already skipped if at default value)')
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# self.file_dump = event.ctrl
|
||||||
|
return context.window_manager.invoke_props_dialog(self) # , width=400
|
||||||
|
# return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout=self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
col = layout.column()
|
||||||
|
col.label(text='Keep following point attributes:')
|
||||||
|
col.prop(self, 'radius')
|
||||||
|
col.prop(self, 'opacity')
|
||||||
|
col.prop(self, 'vertex_color')
|
||||||
|
col.prop(self, 'fill_color')
|
||||||
|
col.prop(self, 'fill_opacity')
|
||||||
|
col.prop(self, 'rotation')
|
||||||
|
return
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
bake_moves = True
|
bake_moves = True
|
||||||
|
@ -504,32 +519,35 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||||
obj = context.object
|
obj = context.object
|
||||||
gpl = obj.data.layers
|
gpl = obj.data.layers
|
||||||
t0 = time()
|
t0 = time()
|
||||||
#ct = check_pressure()
|
#ct = check_radius()
|
||||||
layerdic = {}
|
layerdic = {}
|
||||||
|
|
||||||
layerpool = [l for l in gpl if not l.hide and l.select]# and not l.lock
|
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
|
||||||
if not layerpool:
|
if not layerpool:
|
||||||
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
|
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
if not bake_moves:# copy only drawed frames as is.
|
if not bake_moves: # copy only drawed frames as is.
|
||||||
for l in layerpool:
|
for l in layerpool:
|
||||||
if not l.frames:
|
if not l.frames:
|
||||||
continue# skip empty layers
|
continue# skip empty layers
|
||||||
|
|
||||||
frame_dic = {}
|
frame_dic = {}
|
||||||
for f in l.frames:
|
for f in l.frames:
|
||||||
if skip_empty_frame and not len(f.strokes):
|
if skip_empty_frame and not len(f.drawing.strokes):
|
||||||
continue
|
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,
|
||||||
|
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||||
|
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||||
|
|
||||||
frame_dic[f.frame_number] = strokelist
|
frame_dic[f.frame_number] = strokelist
|
||||||
|
|
||||||
layerdic[l.info] = frame_dic
|
layerdic[l.name] = frame_dic
|
||||||
|
|
||||||
else:# bake position: copy frame where object as moved even if frame is unchanged
|
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.name)
|
||||||
if not l.frames:
|
if not l.frames:
|
||||||
continue# skip empty layers
|
continue# skip empty layers
|
||||||
|
|
||||||
|
@ -541,7 +559,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||||
curmat = prevmat = obj.matrix_world.copy()
|
curmat = prevmat = obj.matrix_world.copy()
|
||||||
|
|
||||||
for i in range(context.scene.frame_start, context.scene.frame_end):
|
for i in range(context.scene.frame_start, context.scene.frame_end):
|
||||||
context.scene.frame_set(i)#use matrix of this frame
|
context.scene.frame_set(i) # use matrix of this frame
|
||||||
curmat = obj.matrix_world.copy()
|
curmat = obj.matrix_world.copy()
|
||||||
|
|
||||||
# if object has moved or current time is on a draw key
|
# if object has moved or current time is on a draw key
|
||||||
|
@ -553,14 +571,17 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||||
break
|
break
|
||||||
|
|
||||||
## skip empty frame if specified
|
## skip empty frame if specified
|
||||||
if skip_empty_frame and not len(f.strokes):
|
if skip_empty_frame and not len(f.drawing.strokes):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
|
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
||||||
|
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||||
|
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||||
|
|
||||||
frame_dic[i] = strokelist
|
frame_dic[i] = strokelist
|
||||||
|
|
||||||
prevmat = curmat
|
prevmat = curmat
|
||||||
layerdic[l.info] = frame_dic
|
layerdic[l.name] = frame_dic
|
||||||
|
|
||||||
## All to clipboard manager
|
## All to clipboard manager
|
||||||
bpy.context.window_manager.clipboard = json.dumps(layerdic)
|
bpy.context.window_manager.clipboard = json.dumps(layerdic)
|
||||||
|
@ -573,21 +594,21 @@ 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)
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
org_frame = context.scene.frame_current
|
org_frame = context.scene.frame_current
|
||||||
obj = context.object
|
obj = context.object
|
||||||
gpl = obj.data.layers
|
gpl = obj.data.layers
|
||||||
t0 = time()
|
t0 = time()
|
||||||
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
|
# add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
|
||||||
try:
|
try:
|
||||||
data = json.loads(bpy.context.window_manager.clipboard)
|
data = json.loads(bpy.context.window_manager.clipboard)
|
||||||
except:
|
except:
|
||||||
|
@ -608,8 +629,8 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
|
||||||
if not layer:
|
if not layer:
|
||||||
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)
|
||||||
|
|
||||||
|
@ -631,13 +652,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
|
||||||
|
|
||||||
|
@ -706,6 +730,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)
|
||||||
|
|
||||||
|
@ -713,6 +740,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)
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import bpy
|
import bpy
|
||||||
import mathutils
|
import mathutils
|
||||||
from bpy_extras import view3d_utils
|
from bpy_extras import view3d_utils
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
|
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
|
||||||
|
|
||||||
## override all sursor snap shortcut with this in keymap
|
## override all sursor snap shortcut with this in keymap
|
||||||
|
@ -13,7 +15,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
||||||
|
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def poll(cls, context):
|
# def poll(cls, context):
|
||||||
# return context.object and context.object.type == 'GPENCIL'
|
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
#print('-!SNAP!-')
|
#print('-!SNAP!-')
|
||||||
|
@ -23,7 +25,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if not context.object or context.object.type != 'GPENCIL':
|
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||||
self.report({'INFO'}, 'Not GP, Cursor surface project')
|
self.report({'INFO'}, 'Not GP, Cursor surface project')
|
||||||
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
|
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
@ -48,7 +50,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
|
||||||
|
@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
|
||||||
k.idname = new_idname
|
k.idname = new_idname
|
||||||
|
|
||||||
|
|
||||||
# prev_matrix = mathutils.Matrix()
|
|
||||||
prev_matrix = None
|
prev_matrix = None
|
||||||
|
|
||||||
# @call_once(bpy.app.handlers.frame_change_post)
|
# @call_once(bpy.app.handlers.frame_change_post)
|
||||||
|
|
||||||
def cursor_follow_update(self,context):
|
## used in properties file to register in boolprop update
|
||||||
|
def cursor_follow_update(self, context):
|
||||||
'''append or remove cursor_follow handler according a boolean'''
|
'''append or remove cursor_follow handler according a boolean'''
|
||||||
|
ob = bpy.context.object
|
||||||
|
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||||
|
## override with target object is specified
|
||||||
|
ob = bpy.context.scene.gptoolprops.cursor_follow_target
|
||||||
global prev_matrix
|
global prev_matrix
|
||||||
# imported in properties to register in boolprop update
|
# imported in properties to register in boolprop update
|
||||||
if self.cursor_follow:#True
|
if self.cursor_follow:#True
|
||||||
|
if ob:
|
||||||
|
# out of below condition to be called when setting target as well
|
||||||
|
prev_matrix = ob.matrix_world.copy()
|
||||||
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||||
if context.object:
|
|
||||||
prev_matrix = context.object.matrix_world
|
|
||||||
|
|
||||||
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||||
|
|
||||||
else:#False
|
else:#False
|
||||||
|
@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
|
||||||
|
|
||||||
def cursor_follow(scene):
|
def cursor_follow(scene):
|
||||||
'''Handler to make the cursor follow active object matrix changes on frame change'''
|
'''Handler to make the cursor follow active object matrix changes on frame change'''
|
||||||
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
|
ob = bpy.context.object
|
||||||
if not bpy.context.object:
|
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||||
|
## override with target object is specified
|
||||||
|
ob = bpy.context.scene.gptoolprops.cursor_follow_target
|
||||||
|
if not ob:
|
||||||
return
|
return
|
||||||
global prev_matrix
|
global prev_matrix
|
||||||
ob = bpy.context.object
|
|
||||||
current_matrix = ob.matrix_world
|
current_matrix = ob.matrix_world
|
||||||
if not prev_matrix:
|
if not prev_matrix:
|
||||||
prev_matrix = current_matrix.copy()
|
prev_matrix = current_matrix.copy()
|
||||||
|
@ -147,14 +155,43 @@ def cursor_follow(scene):
|
||||||
## translation only
|
## translation only
|
||||||
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
|
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
|
||||||
|
|
||||||
# print('offset:', (current_matrix - prev_matrix).to_translation())
|
|
||||||
|
|
||||||
## full
|
## full
|
||||||
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
|
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
|
||||||
|
|
||||||
# store for next use
|
# store for next use
|
||||||
prev_matrix = current_matrix.copy()
|
prev_matrix = current_matrix.copy()
|
||||||
|
|
||||||
|
prev_active_obj = None
|
||||||
|
|
||||||
|
## Add check for object selection change
|
||||||
|
def selection_changed():
|
||||||
|
"""Callback function for selection changes"""
|
||||||
|
if not bpy.context.scene.gptoolprops.cursor_follow:
|
||||||
|
return
|
||||||
|
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||||
|
# we are following a target, nothing to update on selection change
|
||||||
|
return
|
||||||
|
global prev_matrix, prev_active_obj
|
||||||
|
if prev_active_obj != bpy.context.object:
|
||||||
|
## Set stored matrix to active object
|
||||||
|
prev_matrix = bpy.context.object.matrix_world.copy()
|
||||||
|
prev_active_obj = bpy.context.object
|
||||||
|
|
||||||
|
## Note: Same owner as layer manager (will be removed as well)
|
||||||
|
def subscribe_object_change():
|
||||||
|
subscribe_to = (bpy.types.LayerObjects, 'active')
|
||||||
|
bpy.msgbus.subscribe_rna(
|
||||||
|
key=subscribe_to,
|
||||||
|
# owner of msgbus subcribe (for clearing later)
|
||||||
|
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
|
||||||
|
args=(),
|
||||||
|
notify=selection_changed,
|
||||||
|
options={'PERSISTENT'},
|
||||||
|
)
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def subscribe_object_change_handler(dummy):
|
||||||
|
subscribe_object_change()
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
GPTB_OT_cusor_snap,
|
GPTB_OT_cusor_snap,
|
||||||
|
@ -163,14 +200,18 @@ GPTB_OT_cusor_snap,
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
|
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
|
||||||
|
|
||||||
# bpy.app.handlers.frame_change_post.append(cursor_follow)
|
## Follow cursor matrix update on object change
|
||||||
|
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
|
||||||
|
# ## Directly set msgbus to work at first addon activation # select_change
|
||||||
|
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
|
||||||
|
|
||||||
|
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
||||||
|
|
||||||
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
|
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
|
||||||
|
|
||||||
|
@ -180,3 +221,5 @@ def unregister():
|
||||||
# force remove handler if it's there at unregister
|
# force remove handler if it's there at unregister
|
||||||
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||||
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||||
|
|
||||||
|
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
|
@ -0,0 +1,120 @@
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
class ODM_OT_depth_move(bpy.types.Operator):
|
||||||
|
bl_idname = "object.depth_proportional_move"
|
||||||
|
bl_label = "Depth move"
|
||||||
|
bl_description = "Move object in the depth from camera POV while retaining same size in framing"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type != 'CAMERA' # and context.scene.camera
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.init_mouse_x = event.mouse_x
|
||||||
|
self.cam = bpy.context.scene.camera
|
||||||
|
if not self.cam:
|
||||||
|
self.report({'ERROR'}, 'No active camera')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
self.cam_pos = self.cam.matrix_world.translation
|
||||||
|
self.mode = 'distance'
|
||||||
|
self.objects = [o for o in context.selected_objects if o.type != 'CAMERA']
|
||||||
|
self.init_mats = [o.matrix_world.copy() for o in self.objects]
|
||||||
|
|
||||||
|
if self.cam.data.type == 'ORTHO':
|
||||||
|
context.area.header_text_set(f'Move factor: 0.00')
|
||||||
|
# distance is view vector based
|
||||||
|
self.view_vector = Vector((0,0,-1))
|
||||||
|
self.view_vector.rotate(self.cam.matrix_world)
|
||||||
|
else:
|
||||||
|
self.init_vecs = [o.matrix_world.translation - self.cam_pos for o in self.objects]
|
||||||
|
self.init_dists = [v.length for v in self.init_vecs]
|
||||||
|
context.area.header_text_set(f'Move factor: 0.00 | Mode: {self.mode} (M to switch)')
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if self.mode == 'distance':
|
||||||
|
factor = 0.1
|
||||||
|
if event.shift:
|
||||||
|
factor = 0.01
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Smaller factor for proportional dist
|
||||||
|
factor = 0.01
|
||||||
|
if event.shift:
|
||||||
|
factor = 0.001
|
||||||
|
|
||||||
|
if event.type in {'MOUSEMOVE'}:
|
||||||
|
diff = (event.mouse_x - self.init_mouse_x) * factor
|
||||||
|
|
||||||
|
if self.cam.data.type == 'ORTHO':
|
||||||
|
# just push in view vector direction
|
||||||
|
context.area.header_text_set(f'Move factor: {diff:.2f}')
|
||||||
|
for i, obj in enumerate(self.objects):
|
||||||
|
new_vec = self.init_mats[i].translation + (self.view_vector * diff)
|
||||||
|
obj.matrix_world.translation = new_vec
|
||||||
|
else:
|
||||||
|
# Push from camera point and scale accordingly
|
||||||
|
context.area.header_text_set(f'Move factor: {diff:.2f} | Mode: {self.mode} (M to switch)')
|
||||||
|
|
||||||
|
for i, obj in enumerate(self.objects):
|
||||||
|
if self.mode == 'distance':
|
||||||
|
## move with the same length for everyone
|
||||||
|
new_vec = self.init_vecs[i] + (self.init_vecs[i].normalized() * diff)
|
||||||
|
|
||||||
|
else:
|
||||||
|
## move with proportional factor from individual distance vector to camera
|
||||||
|
new_vec = self.init_vecs[i] + (self.init_vecs[i] * diff)
|
||||||
|
|
||||||
|
obj.matrix_world.translation = self.cam_pos + new_vec
|
||||||
|
dist_percentage = new_vec.length / self.init_dists[i]
|
||||||
|
|
||||||
|
obj.scale = self.init_mats[i].to_scale() * dist_percentage
|
||||||
|
|
||||||
|
if event.type in {'M'} and event.value == 'PRESS':
|
||||||
|
# Switch mode
|
||||||
|
self.mode = 'distance' if self.mode == 'proportional' else 'proportional'
|
||||||
|
|
||||||
|
if event.type in {'LEFTMOUSE'} and event.value == 'PRESS':
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS':
|
||||||
|
for i, obj in enumerate(self.objects):
|
||||||
|
obj.matrix_world = self.init_mats[i]
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
""" # Own standalone panel
|
||||||
|
class ODM_PT_sudden_depth_panel(bpy.types.Panel):
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Gpencil"
|
||||||
|
bl_label = "Depth move"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
row = layout.row()
|
||||||
|
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
ODM_OT_depth_move,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,517 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
import gpu
|
||||||
|
from time import time
|
||||||
|
from mathutils import Vector, Matrix, Euler
|
||||||
|
from mathutils.kdtree import KDTree
|
||||||
|
from mathutils.geometry import intersect_line_plane, intersect_line_sphere_2d, intersect_line_line
|
||||||
|
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d, \
|
||||||
|
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
|
||||||
|
from time import time
|
||||||
|
from math import pi, cos, sin
|
||||||
|
from .utils import is_locked, is_hidden
|
||||||
|
|
||||||
|
|
||||||
|
def get_gp_mat(gp, name, set_active=False):
|
||||||
|
mat = bpy.data.materials.get(name)
|
||||||
|
if not mat:
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
bpy.data.materials.create_gpencil_data(mat)
|
||||||
|
|
||||||
|
if mat not in gp.data.materials[:]:
|
||||||
|
gp.data.materials.append(mat)
|
||||||
|
mat_index = gp.data.materials[:].index(mat)
|
||||||
|
|
||||||
|
if set_active:
|
||||||
|
gp.active_material_index = mat_index
|
||||||
|
|
||||||
|
return mat
|
||||||
|
|
||||||
|
def get_gp_frame(gp_layer, frame=None):
|
||||||
|
if frame is None:
|
||||||
|
frame = bpy.context.scene.frame_current
|
||||||
|
gp_frame = next((f for f in gp_layer.frames if f.frame_number==frame), None)
|
||||||
|
if not gp_frame:
|
||||||
|
gp_frame = gp_layer.frames.new(frame)
|
||||||
|
|
||||||
|
return gp_frame
|
||||||
|
|
||||||
|
def get_gp_layer(gp, name=None):
|
||||||
|
if not name:
|
||||||
|
return gp.data.layers.active
|
||||||
|
|
||||||
|
layer = gp.data.layers.get(name)
|
||||||
|
if not layer:
|
||||||
|
layer = gp.data.layers.new(name)
|
||||||
|
|
||||||
|
gp.data.layers.active = layer
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def co_2d_to_3d(co, depth=0.1):
|
||||||
|
area = bpy.context.area
|
||||||
|
region = bpy.context.region
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = view_mat.to_translation()
|
||||||
|
|
||||||
|
depth_3d = view_mat @ Vector((0, 0, -depth))
|
||||||
|
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||||
|
|
||||||
|
return region_2d_to_location_3d(region, rv3d, co, depth_3d)
|
||||||
|
|
||||||
|
|
||||||
|
#vec = (region_2d_to_origin_3d(region, rv3d, co) - org).normalized()
|
||||||
|
|
||||||
|
return org + vec
|
||||||
|
|
||||||
|
return org + region_2d_to_vector_3d(region, rv3d , co)
|
||||||
|
|
||||||
|
def get_cuts_data(strokes, mouse, radius):
|
||||||
|
gp = bpy.context.object
|
||||||
|
|
||||||
|
area = bpy.context.area
|
||||||
|
region = bpy.context.region
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = view_mat.to_translation()
|
||||||
|
mat = gp.matrix_world
|
||||||
|
|
||||||
|
cuts_data = []
|
||||||
|
for s in strokes:
|
||||||
|
is_polyline = 2<=len(s.points)<=5
|
||||||
|
|
||||||
|
if not is_polyline and not s.select:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print('Cut Stroke', s)
|
||||||
|
for i, p in enumerate(s.points):
|
||||||
|
|
||||||
|
if not p.select and not is_polyline:
|
||||||
|
continue
|
||||||
|
# Test if the next or previous is unselected
|
||||||
|
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
if i > 0:
|
||||||
|
prev_p = s.points[i-1]
|
||||||
|
if not prev_p.select and not is_polyline:
|
||||||
|
edges.append((i-1, i))
|
||||||
|
|
||||||
|
if i < len(s.points)-1:
|
||||||
|
next_p = s.points[i+1]
|
||||||
|
if not next_p.select or is_polyline:
|
||||||
|
edges.append((i, i+1))
|
||||||
|
|
||||||
|
for p1_index, p2_index in edges:
|
||||||
|
p1 = s.points[p1_index]
|
||||||
|
p2 = s.points[p2_index]
|
||||||
|
|
||||||
|
length_3d = (p2.co-p1.co).length
|
||||||
|
|
||||||
|
p1_3d = mat @ p1.co
|
||||||
|
p2_3d = mat @ p2.co
|
||||||
|
|
||||||
|
p1_2d = location_3d_to_region_2d(region, rv3d, p1_3d)
|
||||||
|
p2_2d = location_3d_to_region_2d(region, rv3d, p2_3d)
|
||||||
|
|
||||||
|
if p1_2d is None or p2_2d is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
length_2d = (p2_2d-p1_2d).length
|
||||||
|
|
||||||
|
if length_2d <= 4:
|
||||||
|
continue
|
||||||
|
|
||||||
|
intersects = intersect_line_sphere_2d(p1_2d, p2_2d, mouse, radius+2)
|
||||||
|
intersects = [i for i in intersects if i is not None]
|
||||||
|
if not intersects:
|
||||||
|
continue
|
||||||
|
|
||||||
|
close_points = [(p1_2d-i).length < 1 or (p2_2d-i).length < 1 for i in intersects]
|
||||||
|
if any(close_points):
|
||||||
|
#print('close_points', close_points)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
print('intersects', intersects)
|
||||||
|
|
||||||
|
line_intersects = []
|
||||||
|
for i_2d in intersects:
|
||||||
|
#factor = ((i_2d-p1_2d).length) / length_2d
|
||||||
|
#factor_3d = factor_2d * length_3d
|
||||||
|
#vec = region_2d_to_vector_3d(region, rv3d, i_2d)
|
||||||
|
#p3_3d = region_2d_to_location_3d(region, rv3d, i_2d, org)
|
||||||
|
#p4_3d = region_2d_to_origin_3d(region, rv3d, i_2d)
|
||||||
|
|
||||||
|
p3_3d = co_2d_to_3d(i_2d, 0.1)
|
||||||
|
p4_3d = co_2d_to_3d(i_2d, 1000)
|
||||||
|
|
||||||
|
#bpy.context.scene.cursor.location = p4_3d
|
||||||
|
|
||||||
|
line_intersect = intersect_line_line(p1_3d, p2_3d, p3_3d, p4_3d)
|
||||||
|
if not line_intersect:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
i1_3d, _ = line_intersect
|
||||||
|
|
||||||
|
line_intersects += [i1_3d]
|
||||||
|
|
||||||
|
#context.scene.cursor.location = i1_3d
|
||||||
|
|
||||||
|
print('line_intersects', line_intersects)
|
||||||
|
if line_intersects:
|
||||||
|
line_intersects.sort(key=lambda x : (x-p1_3d).length)
|
||||||
|
#cut_data[-1].sort(key=lambda x : (x-p1_3d).length)
|
||||||
|
cut_data = [p1_index, p2_index, s, line_intersects]
|
||||||
|
cuts_data.append(cut_data)
|
||||||
|
|
||||||
|
return cuts_data
|
||||||
|
|
||||||
|
|
||||||
|
def circle(x, y, radius, segments):
|
||||||
|
coords = []
|
||||||
|
m = (1.0 / (segments - 1)) * (pi * 2)
|
||||||
|
|
||||||
|
for p in range(segments):
|
||||||
|
p1 = x + cos(m * p) * radius
|
||||||
|
p2 = y + sin(m * p) * radius
|
||||||
|
coords.append((p1, p2))
|
||||||
|
return coords
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_eraser(Operator):
|
||||||
|
"""Draw a line with the mouse"""
|
||||||
|
bl_idname = "gp.eraser"
|
||||||
|
bl_label = "Eraser Brush"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def draw_callback_px(self):
|
||||||
|
gpu.state.blend_set('ALPHA')
|
||||||
|
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
|
||||||
|
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
|
||||||
|
|
||||||
|
area = bpy.context.area
|
||||||
|
#region = bpy.context.region
|
||||||
|
#rv3d = area.spaces.active.region_3d
|
||||||
|
|
||||||
|
bg_color = area.spaces.active.shading.background_color
|
||||||
|
#print(bg_color)
|
||||||
|
|
||||||
|
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", (1, 1, 1, 1))
|
||||||
|
for mouse, radius in self.mouse_path:
|
||||||
|
circle_co = circle(*mouse, radius, 24)
|
||||||
|
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": circle_co})
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
|
||||||
|
gpu.state.blend_set('NONE')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
def draw_holdout(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
mat_inv = gp.matrix_world.inverted()
|
||||||
|
mouse_3d = co_2d_to_3d(self.mouse)
|
||||||
|
radius_3d = co_2d_to_3d(self.mouse + Vector((self.radius, 0)))
|
||||||
|
search_radius = (radius_3d-mouse_3d).length
|
||||||
|
|
||||||
|
#print('search_radius', search_radius)
|
||||||
|
#print('radius', self.radius)
|
||||||
|
|
||||||
|
#bpy.context.scene.cursor.location = mouse_3d
|
||||||
|
|
||||||
|
for gp_frame, hld_stroke in self.hld_strokes:
|
||||||
|
#print('Add Point')
|
||||||
|
|
||||||
|
hld_stroke.points.add(count=1)
|
||||||
|
p = hld_stroke.points[-1]
|
||||||
|
p.position = mat_inv @ mouse_3d
|
||||||
|
p.pressure = search_radius * 2000
|
||||||
|
|
||||||
|
#context.scene.cursor.location = mouse_3d
|
||||||
|
'''
|
||||||
|
def get_radius(self, context, event):
|
||||||
|
pressure = event.pressure or 1
|
||||||
|
return context.scene.gptoolprops.eraser_radius * pressure
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def erase(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
mat_inv = gp.matrix_world.inverted()
|
||||||
|
|
||||||
|
new_points = []
|
||||||
|
|
||||||
|
#print(self.cuts_data)
|
||||||
|
|
||||||
|
# for f in self.gp_frames:
|
||||||
|
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
|
||||||
|
# f.drawing.strokes.remove(s)
|
||||||
|
|
||||||
|
#gp.data.materials.pop(index=self.hld_index)
|
||||||
|
#bpy.data.materials.remove(self.hld_mat)
|
||||||
|
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||||
|
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||||
|
|
||||||
|
#bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||||
|
|
||||||
|
#for cut_data in self.cuts_data:
|
||||||
|
|
||||||
|
# print(cut_data, len(cut_data))
|
||||||
|
t0 = time()
|
||||||
|
print()
|
||||||
|
|
||||||
|
print('Number of cuts', len(self.mouse_path))
|
||||||
|
|
||||||
|
for mouse, radius in self.mouse_path:
|
||||||
|
t1 = time()
|
||||||
|
|
||||||
|
print()
|
||||||
|
x, y = mouse
|
||||||
|
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||||
|
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||||
|
|
||||||
|
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
|
||||||
|
#print('select_circle', time()-t1)
|
||||||
|
|
||||||
|
t2 = time()
|
||||||
|
cut_data = get_cuts_data(strokes, mouse, radius)
|
||||||
|
#print('get_cuts_data', time()-t2)
|
||||||
|
|
||||||
|
#print([s for s in strokes if s.select])
|
||||||
|
print('cut_data', cut_data)
|
||||||
|
|
||||||
|
t3 = time()
|
||||||
|
for p1_index, p2_index, stroke, intersects in cut_data[::-1]:
|
||||||
|
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
#print('p1_index', p1_index)
|
||||||
|
#print('p2_index', p2_index)
|
||||||
|
|
||||||
|
p1 = stroke.points[p1_index]
|
||||||
|
p2 = stroke.points[p2_index]
|
||||||
|
|
||||||
|
p1.select = True
|
||||||
|
p2.select = True
|
||||||
|
|
||||||
|
number_cuts = len(intersects)
|
||||||
|
|
||||||
|
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
|
||||||
|
|
||||||
|
new_p1 = stroke.points[p1_index+1]
|
||||||
|
new_p1.position = mat_inv@intersects[0]
|
||||||
|
new_points += [(stroke, p1_index+1)]
|
||||||
|
|
||||||
|
#print('number_cuts', number_cuts)
|
||||||
|
|
||||||
|
if number_cuts == 2:
|
||||||
|
new_p2 = stroke.points[p1_index+2]
|
||||||
|
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||||
|
#new_points += [new_p2]
|
||||||
|
|
||||||
|
new_p3 = stroke.points[p1_index+3]
|
||||||
|
new_p3.position = mat_inv@intersects[1]
|
||||||
|
new_points += [(stroke, p1_index+3)]
|
||||||
|
|
||||||
|
#print('subdivide', time() - t3)
|
||||||
|
|
||||||
|
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||||
|
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||||
|
|
||||||
|
'''
|
||||||
|
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||||
|
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
|
||||||
|
|
||||||
|
bpy.ops.gpencil.select_less()
|
||||||
|
|
||||||
|
for p in tip_points:
|
||||||
|
p.select = True
|
||||||
|
for stroke, index in new_points:
|
||||||
|
stroke.points[index].select = False
|
||||||
|
'''
|
||||||
|
|
||||||
|
t4 = time()
|
||||||
|
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||||
|
|
||||||
|
if selected_strokes:
|
||||||
|
bpy.ops.gpencil.delete(type='POINTS')
|
||||||
|
|
||||||
|
print('remove points', time()- t4)
|
||||||
|
|
||||||
|
|
||||||
|
#print('Total one cut', time()-t1)
|
||||||
|
|
||||||
|
#print('Total all cuts', time()-t0)
|
||||||
|
#bpy.ops.gpencil.select_less()
|
||||||
|
#for stroke, index in new_points:
|
||||||
|
# stroke.points[index].select = False
|
||||||
|
|
||||||
|
#bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
|
||||||
|
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||||
|
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
|
||||||
|
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
|
||||||
|
self.radius = self.get_radius(context, event)
|
||||||
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
if event.type == 'LEFTMOUSE':
|
||||||
|
#self.mouse = mouse
|
||||||
|
#self.mouse_path.append((self.mouse, self.radius))
|
||||||
|
self.erase(context, event)
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
if (self.mouse-self.mouse_prev).length < max(self.radius/1.33, 2):
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
self.mouse_prev = self.mouse
|
||||||
|
|
||||||
|
if event.type in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:
|
||||||
|
#self.draw_holdout(context, event)
|
||||||
|
self.mouse_path.append((self.mouse, self.radius))
|
||||||
|
#self.update_cuts_data(context, event)
|
||||||
|
#self.erase(context, event)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
matrix = gp.matrix_world
|
||||||
|
|
||||||
|
self.gpencil_selectmode_edit = context.scene.tool_settings.gpencil_selectmode_edit
|
||||||
|
self.radius = self.get_radius(context, event)
|
||||||
|
self.mouse_prev = self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
self.mouse_path = [(self.mouse_prev, self.radius)]
|
||||||
|
|
||||||
|
area = context.area
|
||||||
|
region = context.region
|
||||||
|
w, h = region.width, region.height
|
||||||
|
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = self.org = view_mat.to_translation()
|
||||||
|
|
||||||
|
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||||
|
|
||||||
|
|
||||||
|
#print('ORG', org)
|
||||||
|
#print('view_mat', view_mat)
|
||||||
|
|
||||||
|
|
||||||
|
self.cuts_data = []
|
||||||
|
|
||||||
|
#org = self.view_mat @ Vector((0, 0, -10))
|
||||||
|
#self.plane_no = self.plane_co-self.org
|
||||||
|
|
||||||
|
#bottom_left = region_2d_to_location_3d(region, rv3d , (0, 0), self.plane_co)
|
||||||
|
#bottom_right = region_2d_to_location_3d(region, rv3d , (0, w), self.plane_co)
|
||||||
|
|
||||||
|
#bottom_left = intersect_line_plane(self.org, bottom_left, self.plane_co, self.plane_no)
|
||||||
|
#bottom_right = intersect_line_plane(self.org, bottom_right, self.plane_co, self.plane_no)
|
||||||
|
|
||||||
|
#self.scale_fac = (bottom_right-bottom_left).length / w
|
||||||
|
|
||||||
|
#print('scale_fac', self.scale_fac)
|
||||||
|
#depth_location = view_mat @ Vector((0, 0, -1))
|
||||||
|
#context.scene.cursor.location = depth_location
|
||||||
|
|
||||||
|
#plane_2d = [(0, 0), (0, h), (w, h), (w, h)]
|
||||||
|
#plane_3d = [region_2d_to_location_3d(p)]
|
||||||
|
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
gp_mats = gp.data.materials
|
||||||
|
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
|
||||||
|
self.gp_frames = [l.current_frame() for l in gp_layers]
|
||||||
|
'''
|
||||||
|
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
|
||||||
|
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
|
||||||
|
print('get_gp_points', time()-t0)
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||||
|
points_data = [(s, f, m, p, org + ((matrix @ p.position)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||||
|
print('points_to_2d', time()-t0)
|
||||||
|
|
||||||
|
#print(points_data)
|
||||||
|
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
|
||||||
|
|
||||||
|
#for s, f, m, p, co in self.points_data:
|
||||||
|
# p.position = co
|
||||||
|
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
self.kd_tree = KDTree(len(self.points_data))
|
||||||
|
for i, point_data in enumerate(self.points_data):
|
||||||
|
s, f, m, p, co = point_data
|
||||||
|
self.kd_tree.insert(co, i)
|
||||||
|
|
||||||
|
self.kd_tree.balance()
|
||||||
|
print('create kdtree', time()-t0)
|
||||||
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Create holdout mat
|
||||||
|
self.hld_mat = get_gp_mat(gp, name='Eraser Holdout Stroke')
|
||||||
|
self.hld_mat.grease_pencil.use_stroke_holdout = True
|
||||||
|
self.hld_mat.grease_pencil.show_stroke = True
|
||||||
|
self.hld_mat.grease_pencil.show_fill = False
|
||||||
|
self.hld_mat.grease_pencil.use_overlap_strokes = True
|
||||||
|
|
||||||
|
self.hld_index = gp_mats[:].index(self.hld_mat)
|
||||||
|
|
||||||
|
self.hld_strokes = []
|
||||||
|
for f in self.gp_frames:
|
||||||
|
hld_stroke = f.drawing.strokes.new()
|
||||||
|
hld_stroke.start_cap_mode = 'ROUND'
|
||||||
|
hld_stroke.end_cap_mode = 'ROUND'
|
||||||
|
hld_stroke.material_index = self.hld_index
|
||||||
|
|
||||||
|
#hld_stroke.line_width = self.radius
|
||||||
|
|
||||||
|
self.hld_strokes.append((f, hld_stroke))
|
||||||
|
|
||||||
|
self.draw_holdout(context, event)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
context.area.tag_redraw()
|
||||||
|
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
GPTB_OT_eraser,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -1,20 +1,47 @@
|
||||||
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
|
||||||
|
|
||||||
|
from bpy.props import (BoolProperty,
|
||||||
|
PointerProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
StringProperty)
|
||||||
|
|
||||||
|
def remove_stroke_exact_duplications(apply=True):
|
||||||
|
'''Remove accidental stroke duplication (points exactly in the same place)
|
||||||
|
:apply: Remove the duplication instead of just listing dupes
|
||||||
|
return number of duplication found/deleted
|
||||||
|
'''
|
||||||
|
# TODO: add additional check of material (even if unlikely to happen)
|
||||||
|
ct = 0
|
||||||
|
gp_datas = [gp for gp in bpy.data.grease_pencils]
|
||||||
|
for gp in gp_datas:
|
||||||
|
for l in gp.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
stroke_list = []
|
||||||
|
for s in reversed(f.drawing.strokes):
|
||||||
|
|
||||||
|
point_list = [p.position for p in s.points]
|
||||||
|
|
||||||
|
if point_list in stroke_list:
|
||||||
|
ct += 1
|
||||||
|
if apply:
|
||||||
|
# Remove redundancy
|
||||||
|
f.drawing.strokes.remove(s)
|
||||||
|
else:
|
||||||
|
stroke_list.append(point_list)
|
||||||
|
return ct
|
||||||
class GPTB_OT_file_checker(bpy.types.Operator):
|
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,98 +50,240 @@ 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
|
||||||
|
|
||||||
|
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
|
||||||
|
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# need some self-control (I had to...)
|
||||||
|
self.ctrl = event.ctrl
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
prefs = get_addon_prefs()
|
prefs = utils.get_addon_prefs()
|
||||||
|
fix = prefs.fixprops
|
||||||
problems = []
|
problems = []
|
||||||
|
|
||||||
|
## Old method : Apply fixes based on pref (inverted by ctrl key)
|
||||||
|
# # If Ctrl is pressed, invert behavior (invert boolean)
|
||||||
|
# apply ^= self.ctrl
|
||||||
|
|
||||||
|
apply = self.apply_fixes
|
||||||
|
if self.ctrl:
|
||||||
|
apply = True
|
||||||
|
|
||||||
## Lock main cam:
|
## Lock main cam:
|
||||||
if not 'layout' in Path(bpy.data.filepath).stem:#dont touch layout cameras
|
if fix.lock_main_cam:
|
||||||
if context.scene.camera:
|
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
|
||||||
cam = context.scene.camera
|
if context.scene.camera:
|
||||||
if cam.name == 'draw_cam' and cam.parent:
|
cam = context.scene.camera
|
||||||
if cam.parent.type == 'CAMERA':
|
if cam.name == 'draw_cam' and cam.parent:
|
||||||
cam = cam.parent
|
if cam.parent.type == 'CAMERA':
|
||||||
else:
|
cam = cam.parent
|
||||||
cam = None
|
else:
|
||||||
if cam:
|
cam = None
|
||||||
triple = (True,True,True)
|
if cam:
|
||||||
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
|
triple = (True,True,True)
|
||||||
problems.append('Lock main camera')
|
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
|
||||||
cam.lock_location = cam.lock_rotation = triple
|
problems.append('Lock main camera')
|
||||||
|
if apply:
|
||||||
|
cam.lock_location = cam.lock_rotation = triple
|
||||||
|
|
||||||
## set scene res at pref res according to addon pref
|
## set scene res at pref res according to addon pref
|
||||||
rx, ry = prefs.render_res_x, prefs.render_res_y
|
if fix.set_scene_res:
|
||||||
# TODO set (rx, ry) to camera resolution if specified in camera name
|
rx, ry = prefs.render_res_x, prefs.render_res_y
|
||||||
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
|
# TODO set (rx, ry) to camera resolution if specified in camera name
|
||||||
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
|
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
|
||||||
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
|
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
|
||||||
|
if apply:
|
||||||
|
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
|
||||||
|
|
||||||
## set scene percentage at 100:
|
## set scene percentage at 100:
|
||||||
if context.scene.render.resolution_percentage != 100:
|
if fix.set_res_percentage:
|
||||||
problems.append('Resolution output to 100%')
|
if context.scene.render.resolution_percentage != 100:
|
||||||
context.scene.render.resolution_percentage = 100
|
problems.append('Resolution output to 100%')
|
||||||
|
if apply:
|
||||||
## set show slider and sync range
|
context.scene.render.resolution_percentage = 100
|
||||||
for window in bpy.context.window_manager.windows:
|
|
||||||
screen = window.screen
|
|
||||||
for area in screen.areas:
|
|
||||||
if area.type == 'DOPESHEET_EDITOR':
|
|
||||||
if hasattr(area.spaces[0], 'show_sliders'):
|
|
||||||
setattr(area.spaces[0], 'show_sliders', True)
|
|
||||||
|
|
||||||
if hasattr(area.spaces[0], 'show_locked_time'):
|
|
||||||
setattr(area.spaces[0], 'show_locked_time', True)
|
|
||||||
|
|
||||||
## set fps according to preferences settings
|
## set fps according to preferences settings
|
||||||
if context.scene.render.fps != prefs.fps:
|
if fix.set_fps:
|
||||||
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
|
if context.scene.render.fps != prefs.fps:
|
||||||
context.scene.render.fps = prefs.fps
|
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
|
||||||
|
if apply:
|
||||||
|
context.scene.render.fps = prefs.fps
|
||||||
|
|
||||||
## set cursor type (according to prefs ?)
|
## set show slider and sync range
|
||||||
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
|
if fix.set_slider_n_sync:
|
||||||
tool = prefs.select_active_tool
|
for window in bpy.context.window_manager.windows:
|
||||||
|
screen = window.screen
|
||||||
|
for area in screen.areas:
|
||||||
|
if area.type == 'DOPESHEET_EDITOR':
|
||||||
|
if hasattr(area.spaces[0], 'show_sliders'):
|
||||||
|
setattr(area.spaces[0], 'show_sliders', True)
|
||||||
|
if hasattr(area.spaces[0], 'show_locked_time'):
|
||||||
|
setattr(area.spaces[0], 'show_locked_time', True)
|
||||||
|
|
||||||
|
## set cursor type
|
||||||
|
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
|
||||||
|
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...)
|
||||||
|
# if not context.scene.tool_settings.use_gpencil_draw_additive:
|
||||||
|
# problems.append(f'Activated Gp additive drawing mode (snowflake)')
|
||||||
|
# context.scene.tool_settings.use_gpencil_draw_additive = True
|
||||||
|
|
||||||
## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
|
||||||
if not context.scene.tool_settings.use_gpencil_draw_additive:
|
|
||||||
problems.append(f'Activated Gp additive drawing mode (snowflake)')
|
|
||||||
context.scene.tool_settings.use_gpencil_draw_additive = True
|
|
||||||
|
|
||||||
## GP stroke placement/projection check
|
## GP stroke placement/projection check
|
||||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
|
if fix.check_front_axis:
|
||||||
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
|
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
|
||||||
|
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
|
||||||
|
|
||||||
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
|
if fix.check_placement:
|
||||||
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
|
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
|
||||||
|
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
|
||||||
|
|
||||||
|
## GP Use light disable
|
||||||
|
if fix.set_gp_use_lights_off:
|
||||||
|
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
|
||||||
|
if gp_with_lights:
|
||||||
|
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
|
||||||
|
if apply:
|
||||||
|
for o in gp_with_lights:
|
||||||
|
o.use_grease_pencil_lights = False
|
||||||
|
|
||||||
## Disabled animation
|
## Disabled animation
|
||||||
fcu_ct = 0
|
if fix.list_disabled_anim:
|
||||||
for act in bpy.data.actions:
|
fcu_ct = 0
|
||||||
if not act.users:
|
for act in bpy.data.actions:
|
||||||
continue
|
if not act.users:
|
||||||
for fcu in act.fcurves:
|
continue
|
||||||
if fcu.mute:
|
for fcu in act.fcurves:
|
||||||
fcu_ct += 1
|
if fcu.mute:
|
||||||
print(f"muted: {act.name} > {fcu.data_path}")
|
fcu_ct += 1
|
||||||
if fcu_ct:
|
print(f"muted: {act.name} > {fcu.data_path}")
|
||||||
problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
|
if fcu_ct:
|
||||||
|
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
|
||||||
|
|
||||||
|
## Object visibility conflict
|
||||||
|
if fix.list_obj_vis_conflict:
|
||||||
|
viz_ct = 0
|
||||||
|
for o in context.scene.objects:
|
||||||
|
if not (o.hide_get() == o.hide_viewport == o.hide_render):
|
||||||
|
hv = 'No' if o.hide_get() else 'Yes'
|
||||||
|
vp = 'No' if o.hide_viewport else 'Yes'
|
||||||
|
rd = 'No' if o.hide_render else 'Yes'
|
||||||
|
viz_ct += 1
|
||||||
|
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
|
||||||
|
if viz_ct:
|
||||||
|
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
||||||
|
|
||||||
|
## GP modifiers visibility conflict
|
||||||
|
if fix.list_gp_mod_vis_conflict:
|
||||||
|
mod_viz_ct = 0
|
||||||
|
for o in context.scene.objects:
|
||||||
|
for m in o.modifiers:
|
||||||
|
if m.show_viewport != m.show_render:
|
||||||
|
vp = 'Yes' if m.show_viewport else 'No'
|
||||||
|
rd = 'Yes' if m.show_render else 'No'
|
||||||
|
mod_viz_ct += 1
|
||||||
|
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
|
||||||
|
|
||||||
|
if mod_viz_ct:
|
||||||
|
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
|
||||||
|
|
||||||
|
## check if GP modifier have broken layer targets
|
||||||
|
if fix.list_broken_mod_targets:
|
||||||
|
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
|
||||||
|
lay_name_list = [l.name for l in o.data.layers]
|
||||||
|
for m in o.modifiers:
|
||||||
|
if not hasattr(m, 'layer_filter'):
|
||||||
|
continue
|
||||||
|
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
|
||||||
|
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
|
||||||
|
print(mess)
|
||||||
|
problems.append(mess)
|
||||||
|
|
||||||
|
## Use median point
|
||||||
|
if fix.set_pivot_median_point:
|
||||||
|
if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
|
||||||
|
problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
|
||||||
|
if apply:
|
||||||
|
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
|
||||||
|
|
||||||
|
if fix.disable_guide:
|
||||||
|
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
|
||||||
|
problems.append(f"Disabled Draw Guide")
|
||||||
|
if apply:
|
||||||
|
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
|
||||||
|
|
||||||
|
if fix.autokey_add_n_replace:
|
||||||
|
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
|
||||||
|
problems.append(f"Autokey mode reset to 'Add & Replace'")
|
||||||
|
if apply:
|
||||||
|
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
|
||||||
|
|
||||||
|
if fix.file_path_type != 'none':
|
||||||
|
pathes = []
|
||||||
|
for p in bpy.utils.blend_paths():
|
||||||
|
if fix.file_path_type == 'RELATIVE':
|
||||||
|
if not p.startswith('//'):
|
||||||
|
pathes.append(p)
|
||||||
|
elif fix.file_path_type == 'ABSOLUTE':
|
||||||
|
if p.startswith('//'):
|
||||||
|
pathes.append(p)
|
||||||
|
if pathes:
|
||||||
|
mess = f'{len(pathes)}/{len(bpy.utils.blend_paths())} paths not {fix.file_path_type.lower()} (see console)'
|
||||||
|
problems.append(mess)
|
||||||
|
print(mess)
|
||||||
|
print('\n'.join(pathes))
|
||||||
|
print('-')
|
||||||
|
|
||||||
|
if fix.lock_object_mode != 'none':
|
||||||
|
lockmode = bpy.context.scene.tool_settings.lock_object_mode
|
||||||
|
if fix.lock_object_mode == 'LOCK':
|
||||||
|
if not lockmode:
|
||||||
|
problems.append(f"Lock object mode toggled On")
|
||||||
|
if apply:
|
||||||
|
bpy.context.scene.tool_settings.lock_object_mode = True
|
||||||
|
|
||||||
|
elif fix.lock_object_mode == 'UNLOCK':
|
||||||
|
if lockmode:
|
||||||
|
problems.append(f"Lock object mode toggled Off")
|
||||||
|
if apply:
|
||||||
|
bpy.context.scene.tool_settings.lock_object_mode = False
|
||||||
|
|
||||||
|
if fix.remove_redundant_strokes:
|
||||||
|
ct = remove_stroke_exact_duplications(apply=apply)
|
||||||
|
if ct > 0:
|
||||||
|
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
|
||||||
|
problems.append(mess)
|
||||||
|
|
||||||
|
# ## Set onion skin filter to 'All type'
|
||||||
|
# fix_kf_type = 0
|
||||||
|
# for gp in bpy.data.grease_pencils:#from data
|
||||||
|
# if not gp.is_annotation:
|
||||||
|
# if gp.onion_keyframe_type != 'ALL':
|
||||||
|
# gp.onion_keyframe_type = 'ALL'
|
||||||
|
# fix_kf_type += 1
|
||||||
|
# if fix_kf_type:
|
||||||
|
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
||||||
|
|
||||||
## Set onion skin filter to 'All type'
|
|
||||||
fix_kf_type = 0
|
|
||||||
for gp in bpy.data.grease_pencils:#from data
|
|
||||||
if not gp.is_annotation:
|
|
||||||
if gp.onion_keyframe_type != 'ALL':
|
|
||||||
gp.onion_keyframe_type = 'ALL'
|
|
||||||
fix_kf_type += 1
|
|
||||||
if fix_kf_type:
|
|
||||||
problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
|
||||||
# for ob in context.scene.objects:#from object
|
# for ob in context.scene.objects:#from object
|
||||||
# if ob.type == 'GPENCIL':
|
# if ob.type == 'GREASEPENCIL':
|
||||||
# ob.data.onion_keyframe_type = 'ALL'
|
# ob.data.onion_keyframe_type = 'ALL'
|
||||||
|
|
||||||
#### --- print fix/problems report
|
#### --- print fix/problems report
|
||||||
|
@ -125,47 +294,92 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||||
print(p)
|
print(p)
|
||||||
else:
|
else:
|
||||||
print(p[0])
|
print(p[0])
|
||||||
|
|
||||||
|
if not self.apply_fixes:
|
||||||
|
## button to call the operator again with apply_fixes set to True
|
||||||
|
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
|
||||||
|
|
||||||
# Show in viewport
|
# Show in viewport
|
||||||
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO')
|
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
|
||||||
|
utils.show_message_box(problems, _title = title, _icon = 'INFO')
|
||||||
else:
|
else:
|
||||||
self.report({'INFO'}, 'All good')
|
self.report({'INFO'}, 'All good')
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
""" OLD links checker with show_message_box
|
class GPTB_OT_copy_string_to_clipboard(bpy.types.Operator):
|
||||||
class GPTB_OT_links_checker(bpy.types.Operator):
|
bl_idname = "gp.copy_string_to_clipboard"
|
||||||
bl_idname = "gp.links_checker"
|
bl_label = "Copy String"
|
||||||
bl_label = "Links check"
|
bl_description = "Copy passed string to clipboard"
|
||||||
bl_description = "Check states of file direct links"
|
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
def execute(self, context):
|
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
|
||||||
all_lnks = []
|
|
||||||
has_broken_link = False
|
|
||||||
## check for broken links
|
|
||||||
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
|
||||||
lfp = Path(lib)
|
|
||||||
realib = Path(current)
|
|
||||||
if not lfp.exists():
|
|
||||||
has_broken_link = True
|
|
||||||
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
|
|
||||||
else:
|
|
||||||
if realib.as_posix().startswith('//'):
|
|
||||||
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
|
|
||||||
else:
|
|
||||||
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
|
|
||||||
|
|
||||||
all_lnks.sort(key=lambda x: x[1], reverse=True)
|
def execute(self, context):
|
||||||
if all_lnks:
|
if not self.string:
|
||||||
print('===File check===')
|
# self.report({'ERROR'}, 'Nothing to copy')
|
||||||
for p in all_lnks:
|
return {"CANCELLED"}
|
||||||
if isinstance(p, str):
|
bpy.context.window_manager.clipboard = self.string
|
||||||
print(p)
|
self.report({'INFO'}, f'Copied: {self.string}')
|
||||||
else:
|
return {"FINISHED"}
|
||||||
print(p[0])
|
|
||||||
# Show in viewport
|
class GPTB_OT_copy_multipath_clipboard(bpy.types.Operator):
|
||||||
show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
|
bl_idname = "gp.copy_multipath_clipboard"
|
||||||
return {"FINISHED"} """
|
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):
|
class GPTB_OT_links_checker(bpy.types.Operator):
|
||||||
|
@ -194,14 +408,28 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
||||||
|
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
# layout = layout.column() # thinner linespace
|
||||||
for l in self.all_lnks:
|
for l in self.all_lnks:
|
||||||
if l[1] == 'LIBRARY_DATA_BROKEN':
|
if l[1] == 'CANCEL':
|
||||||
layout.label(text=l[0], icon=l[1])
|
layout.label(text=l[0], icon=l[1])
|
||||||
else:
|
continue
|
||||||
split=layout.split(factor=0.75)
|
|
||||||
|
if l[1] == 'LIBRARY_DATA_BROKEN':
|
||||||
|
split=layout.split(factor=0.85)
|
||||||
split.label(text=l[0], icon=l[1])
|
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()
|
# layout.label(text=l[0], icon=l[1])
|
||||||
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])))
|
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):
|
def invoke(self, context, event):
|
||||||
self.all_lnks = []
|
self.all_lnks = []
|
||||||
|
@ -210,19 +438,32 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
||||||
abs_ct = 0
|
abs_ct = 0
|
||||||
rel_ct = 0
|
rel_ct = 0
|
||||||
## check for broken links
|
## check for broken links
|
||||||
|
viewed = []
|
||||||
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
||||||
lfp = Path(lib)
|
# avoid relisting same path mutliple times
|
||||||
realib = Path(current)
|
if current in viewed:
|
||||||
if not lfp.exists():
|
continue
|
||||||
self.broke_ct += 1
|
# TODO find a proper way to show the number of user of this path...
|
||||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
|
viewed.append(current)
|
||||||
else:
|
|
||||||
if realib.as_posix().startswith('//'):
|
realib = Path(current) # path as-is
|
||||||
rel_ct += 1
|
lfp = Path(lib) # absolute path
|
||||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
|
|
||||||
|
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:
|
else:
|
||||||
abs_ct += 1
|
if realib.as_posix().startswith('//'):
|
||||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
|
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:
|
if not self.all_lnks:
|
||||||
self.report({'INFO'}, 'No external links in files')
|
self.report({'INFO'}, 'No external links in files')
|
||||||
|
@ -244,50 +485,221 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
||||||
print(p[0])
|
print(p[0])
|
||||||
# Show in viewport
|
# Show in viewport
|
||||||
|
|
||||||
|
maxlen = max(len(x) for x in viewed)
|
||||||
|
|
||||||
# if broke_ct == 0:
|
# if broke_ct == 0:
|
||||||
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
|
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
|
||||||
# return {"FINISHED"}
|
# return {"FINISHED"}
|
||||||
try:
|
popup_width = 800
|
||||||
self.proj = context.preferences.addons['pipe_sync'].preferences['local_folder']
|
if maxlen < 50:
|
||||||
except:
|
popup_width = 500
|
||||||
self.proj = None
|
elif maxlen > 100:
|
||||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
popup_width = 1000
|
||||||
|
|
||||||
'''### OLD
|
self.proj = os.environ.get('PROJECT_ROOT')
|
||||||
class GPTB_OT_check_scene(bpy.types.Operator):
|
return context.window_manager.invoke_props_dialog(self, width=popup_width)
|
||||||
bl_idname = "gp.scene_check"
|
|
||||||
bl_label = "Check GP scene"
|
|
||||||
bl_description = "Check and fix scene settings"
|
|
||||||
|
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.list_viewport_render_visibility"
|
||||||
|
bl_label = "List Viewport And Render Visibility Conflicts"
|
||||||
|
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
@classmethod
|
def invoke(self, context, event):
|
||||||
def poll(cls, context):
|
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
|
||||||
return True
|
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):
|
||||||
## 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
|
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
|
||||||
gp_os = [o for o in context.scene.objects if o.type == 'GPENCIL' if o.data.users == 1]#no multiple users
|
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.sync_visibility_from_viewlayer"
|
||||||
|
bl_label = "Sync Visibility From Viewlayer"
|
||||||
|
bl_description = "Set viewport and render visibility to match viewlayer visibility"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
for gpo in gp_os:
|
def execute(self, context):
|
||||||
if gpo.data.name.startswith('Stroke'):# dont touch already renamed group
|
for obj in context.scene.objects:
|
||||||
if gpo.data.name != gpo.name:
|
is_hidden = obj.hide_get() # Get viewlayer visibility
|
||||||
print('renaming GP data:', gpo.data.name, '-->', gpo.name)
|
obj.hide_viewport = is_hidden
|
||||||
gpo.data.name = gpo.name
|
obj.hide_render = is_hidden
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
## disable autolock
|
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
|
||||||
context.scene.tool_settings.lock_object_mode = False
|
bl_idname = "gp.sync_visibility_from_viewport"
|
||||||
|
bl_label = "Sync Visibility From Viewport"
|
||||||
|
bl_description = "Set viewlayer and render visibility to match viewport visibility"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
return {"FINISHED"}
|
def execute(self, context):
|
||||||
'''
|
for obj in context.scene.objects:
|
||||||
|
is_hidden = obj.hide_viewport
|
||||||
|
obj.hide_set(is_hidden)
|
||||||
|
obj.hide_render = is_hidden
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.sync_visibility_from_render"
|
||||||
|
bl_label = "Sync Visibility From Render"
|
||||||
|
bl_description = "Set viewlayer and viewport visibility to match render visibility"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
is_hidden = obj.hide_render
|
||||||
|
obj.hide_set(is_hidden)
|
||||||
|
obj.hide_viewport = is_hidden
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.sync_visibible_to_render"
|
||||||
|
bl_label = "Sync Overall Viewport Visibility To Render"
|
||||||
|
bl_description = "Set render visibility from"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
|
||||||
|
obj.hide_render = not obj.visible_get()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
|
||||||
|
"""Property group to handle object visibility"""
|
||||||
|
is_hidden: BoolProperty(
|
||||||
|
name="Hide in Viewport",
|
||||||
|
description="Toggle object visibility in viewport",
|
||||||
|
get=lambda self: self.get("is_hidden", False),
|
||||||
|
set=lambda self, value: self.set_visibility(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
object_name: StringProperty(name="Object Name")
|
||||||
|
|
||||||
|
def set_visibility(self, value):
|
||||||
|
"""Set the visibility using hide_set()"""
|
||||||
|
obj = bpy.context.view_layer.objects.get(self.object_name)
|
||||||
|
if obj:
|
||||||
|
obj.hide_set(value)
|
||||||
|
self["is_hidden"] = value
|
||||||
|
|
||||||
|
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.list_object_visibility_conflicts"
|
||||||
|
bl_label = "List Object Visibility Conflicts"
|
||||||
|
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Clear and rebuild both collections
|
||||||
|
self.visibility_items.clear()
|
||||||
|
|
||||||
|
# Store objects with conflicts
|
||||||
|
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
|
||||||
|
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
|
||||||
|
|
||||||
|
# Create visibility items in same order
|
||||||
|
for obj in objects_with_conflicts:
|
||||||
|
item = self.visibility_items.add()
|
||||||
|
item.object_name = obj.name
|
||||||
|
item["is_hidden"] = obj.hide_get()
|
||||||
|
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
# Add sync buttons at the top
|
||||||
|
row = layout.row(align=False)
|
||||||
|
row.label(text="Sync All Visibility From:")
|
||||||
|
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
|
||||||
|
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
|
||||||
|
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
# We can safely iterate over visibility_items since objects are stored in same order
|
||||||
|
for vis_item in self.visibility_items:
|
||||||
|
obj = context.view_layer.objects.get(vis_item.object_name)
|
||||||
|
if not obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = col.row(align=False)
|
||||||
|
row.label(text=obj.name)
|
||||||
|
|
||||||
|
## Viewlayer visibility "as prop" to allow slide toggle
|
||||||
|
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
|
||||||
|
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
|
||||||
|
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
|
||||||
|
|
||||||
|
# Direct object properties
|
||||||
|
row.prop(obj, 'hide_viewport', text='', emboss=False)
|
||||||
|
row.prop(obj, 'hide_render', text='', emboss=False)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
|
||||||
|
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.list_modifier_visibility"
|
||||||
|
bl_label = "List Objects Modifiers Visibility Conflicts"
|
||||||
|
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.ob_list = []
|
||||||
|
for o in context.scene.objects:
|
||||||
|
if not len(o.modifiers):
|
||||||
|
continue
|
||||||
|
mods = []
|
||||||
|
for m in o.modifiers:
|
||||||
|
if m.show_viewport != m.show_render:
|
||||||
|
if not mods:
|
||||||
|
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
|
||||||
|
mods.append(m)
|
||||||
|
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
if not self.ob_list:
|
||||||
|
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
|
||||||
|
return
|
||||||
|
|
||||||
|
for o in self.ob_list:
|
||||||
|
layout.label(text=o[0].name, icon=o[2])
|
||||||
|
for m in o[1]:
|
||||||
|
row = layout.row()
|
||||||
|
row.label(text='')
|
||||||
|
row.label(text=m.name, icon='MODIFIER_ON')
|
||||||
|
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
|
||||||
|
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
# GPTB_OT_check_scene,
|
GPTB_OT_list_viewport_render_visibility, # Only viewport and render
|
||||||
|
GPTB_OT_sync_visibility_from_viewlayer,
|
||||||
|
GPTB_OT_sync_visibility_from_viewport,
|
||||||
|
GPTB_OT_sync_visibility_from_render,
|
||||||
|
GPTB_OT_sync_visibible_to_render,
|
||||||
|
GPTB_PG_object_visibility,
|
||||||
|
GPTB_OT_list_object_visibility_conflicts,
|
||||||
|
GPTB_OT_list_modifier_visibility,
|
||||||
|
GPTB_OT_copy_string_to_clipboard,
|
||||||
|
GPTB_OT_copy_multipath_clipboard,
|
||||||
GPTB_OT_file_checker,
|
GPTB_OT_file_checker,
|
||||||
GPTB_OT_links_checker,
|
GPTB_OT_links_checker,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
from mathutils.geometry import intersect_line_plane
|
||||||
|
from math import pi
|
||||||
|
import numpy as np
|
||||||
|
from time import time
|
||||||
|
from .utils import (location_to_region, region_to_location)
|
||||||
|
|
||||||
|
## DISABLED (in init, also in menu append, see register below)
|
||||||
|
"""
|
||||||
|
## Do not work on multiple object
|
||||||
|
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||||
|
'''Reproject
|
||||||
|
:all_stroke: affect hidden, locked layers
|
||||||
|
'''
|
||||||
|
|
||||||
|
if restore_frame:
|
||||||
|
oframe = bpy.context.scene.frame_current
|
||||||
|
omode = bpy.context.mode
|
||||||
|
|
||||||
|
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
|
||||||
|
# frame_list = list(set(frame_list))
|
||||||
|
# frame_list.sort()
|
||||||
|
# for fnum in frame_list:
|
||||||
|
# bpy.context.scene.frame_current = fnum
|
||||||
|
t0 = time()
|
||||||
|
scn = bpy.context.scene
|
||||||
|
laynum = len(obj.data.layers)
|
||||||
|
for i, l in enumerate(obj.data.layers):
|
||||||
|
## \x1b[2K\r ?
|
||||||
|
fnum = len(l.frames)
|
||||||
|
zf = len(str(fnum))
|
||||||
|
for j, f in enumerate(reversed(l.frames)): # whynot...
|
||||||
|
print(f'{obj.name} : {i+1}/{laynum} : {l.name} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
|
||||||
|
scn.frame_set(f.frame_number) # more chance to update the matrix
|
||||||
|
bpy.context.view_layer.update() # update the matrix ?
|
||||||
|
bpy.context.scene.camera.location = bpy.context.scene.camera.location
|
||||||
|
scn.frame_current = f.frame_number
|
||||||
|
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
for p in s.points:
|
||||||
|
p.position = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.position), scn.cursor.location)
|
||||||
|
|
||||||
|
if restore_frame:
|
||||||
|
bpy.context.scene.frame_current = oframe
|
||||||
|
print(' '*50,end='\x1b[1K\r') # clear the line
|
||||||
|
print(f'{obj.name} ok ({time()-t0:.2f})')
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
def batch_flat_reproject(obj):
|
||||||
|
'''Reproject all strokes on 3D cursor for all existing frame of passed GP object'''
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
cam = scn.camera
|
||||||
|
|
||||||
|
for l in obj.data.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
scn.frame_set(f.frame_number)
|
||||||
|
|
||||||
|
cam_mat = cam.matrix_local.copy()
|
||||||
|
origin = cam.matrix_world.to_translation()
|
||||||
|
mat_inv = obj.matrix_world.inverted()
|
||||||
|
|
||||||
|
plane_no = Vector((0,0,1))
|
||||||
|
plane_no.rotate(cam_mat)
|
||||||
|
plane_co = scn.cursor.location
|
||||||
|
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
points_co = [obj.matrix_world @ p.position for p in s.points]
|
||||||
|
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
|
||||||
|
points_co = [co for vector in points_co for co in vector]
|
||||||
|
|
||||||
|
s.points.foreach_set('co', points_co)
|
||||||
|
s.points.add(1) # update
|
||||||
|
s.points.pop() # update
|
||||||
|
#for p in s.points:
|
||||||
|
# loc_2d = location_to_region(obj.matrix_world @ p.position)
|
||||||
|
# p.position = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def batch_flat_reproject(obj):
|
||||||
|
'''Reproject strokes of passed GP object on 3D cursor full scene range'''
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
cam = scn.camera
|
||||||
|
|
||||||
|
for i in range(scn.frame_start, scn.frame_end + 1):
|
||||||
|
scn.frame_set(i)
|
||||||
|
cam_mat = cam.matrix_local.copy()
|
||||||
|
origin = cam.matrix_world.to_translation()
|
||||||
|
mat_inv = obj.matrix_world.inverted()
|
||||||
|
plane_no = Vector((0,0,1))
|
||||||
|
plane_no.rotate(cam_mat)
|
||||||
|
plane_co = scn.cursor.location
|
||||||
|
|
||||||
|
for l in obj.data.layers:
|
||||||
|
f = l.current_frame()
|
||||||
|
if not f: # No active frame
|
||||||
|
continue
|
||||||
|
|
||||||
|
if f.frame_number != scn.frame_current:
|
||||||
|
f = l.frames.copy(f) # duplicate content of the previous frame
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
points_co = [obj.matrix_world @ p.position for p in s.points]
|
||||||
|
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
|
||||||
|
points_co = [co for vector in points_co for co in vector]
|
||||||
|
|
||||||
|
s.points.foreach_set('co', points_co)
|
||||||
|
s.points.add(1) # update
|
||||||
|
s.points.pop() # update
|
||||||
|
|
||||||
|
class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.batch_flat_reproject"
|
||||||
|
bl_label = "Flat Reproject Selected On cursor"
|
||||||
|
bl_description = "Reproject all frames of all selected gp object on cursor"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for o in context.selected_objects:
|
||||||
|
if o.type != 'GREASEPENCIL' or not o.select_get():
|
||||||
|
continue
|
||||||
|
batch_flat_reproject(o)
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
### -- MENU ENTRY --
|
||||||
|
|
||||||
|
def flat_reproject_clean_menu(self, context):
|
||||||
|
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||||
|
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
|
||||||
|
|
||||||
|
def flat_reproject_context_menu(self, context):
|
||||||
|
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||||
|
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_batch_flat_reproject,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cl in classes:
|
||||||
|
bpy.utils.register_class(cl)
|
||||||
|
|
||||||
|
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(flat_reproject_context_menu)
|
||||||
|
# bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
# bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu)
|
||||||
|
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(flat_reproject_context_menu)
|
||||||
|
|
||||||
|
for cl in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cl)
|
|
@ -0,0 +1,214 @@
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
|
||||||
|
bl_idname = "object.create_follow_path_curve"
|
||||||
|
bl_label = "Create Follow Path Curve"
|
||||||
|
bl_description = "Create curve and add follow path constraint\
|
||||||
|
\n(remove location offset from object if any)"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
# settings = context.scene.anim_cycle_settings
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||||
|
|
||||||
|
## For bones
|
||||||
|
# root_name = fn.get_root_name(context=context)
|
||||||
|
# root = ob.pose.bones.get(root_name)
|
||||||
|
# if not root:
|
||||||
|
# self.report({'ERROR'}, f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name')
|
||||||
|
# return {"CANCELLED"}
|
||||||
|
|
||||||
|
## create curve at bone position
|
||||||
|
# loc = ob.matrix_world @ root.matrix.to_translation()
|
||||||
|
# root_axis_vec = fn.get_direction_vector_from_enum(settings.forward_axis)
|
||||||
|
## get real world direction of the root
|
||||||
|
# world_forward = (root.matrix @ root_axis_vec) - root.matrix.to_translation()
|
||||||
|
|
||||||
|
loc = ob.matrix_world.to_translation()
|
||||||
|
|
||||||
|
## X global
|
||||||
|
# TODO: Set direction orientation in view space (UP, LEFT, RIGHT, DOWN)
|
||||||
|
direction = Vector((1,0,0))
|
||||||
|
curve = utils.create_curve(location=loc,
|
||||||
|
direction=direction.normalized() * 2,
|
||||||
|
name='curve_path',
|
||||||
|
context=context)
|
||||||
|
|
||||||
|
utils.create_follow_path_constraint(ob, curve)
|
||||||
|
|
||||||
|
## reset location to remove offset
|
||||||
|
ob.location = (0,0,0)
|
||||||
|
# ob.keyframe_insert('location')
|
||||||
|
ob.rotation_euler = (0,0,0)
|
||||||
|
# ob.keyframe_insert('rotation_euler')
|
||||||
|
|
||||||
|
# refresh evaluation so constraint shows up correctly
|
||||||
|
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_edit_curve(bpy.types.Operator):
|
||||||
|
bl_idname = "object.edit_curve"
|
||||||
|
bl_label = "Edit Curve"
|
||||||
|
bl_description = "Edit curve used as follow path constraint"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
curve = next((c.target for c in ob.constraints if c.type == 'FOLLOW_PATH' and c.target), None)
|
||||||
|
|
||||||
|
if curve is None:
|
||||||
|
self.report({"ERROR"}, 'No follow path curve found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Object mode, set curve as active, go Edit
|
||||||
|
utils.go_edit_mode(curve)
|
||||||
|
# curve context.mode -> EDIT_CURVE
|
||||||
|
# b.id_data.select_set(False)
|
||||||
|
ob.select_set(False)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_remove_follow_path(bpy.types.Operator):
|
||||||
|
bl_idname = "object.remove_follow_path"
|
||||||
|
bl_label = "Remove Follow Path Constraint"
|
||||||
|
bl_description = "Remove follow path on object"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
const = next((c for c in ob.constraints if c.type == 'FOLLOW_PATH'), None)
|
||||||
|
if not const:
|
||||||
|
self.report({'ERROR'}, f'No follow path constraint on "{ob.name}" found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# store position
|
||||||
|
mat = ob.matrix_world.copy()
|
||||||
|
|
||||||
|
ob.constraints.remove(const)
|
||||||
|
|
||||||
|
# restore position
|
||||||
|
ob.matrix_world = mat
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'Removed follow_path constraint on "{ob.name}"')
|
||||||
|
# Also remove offset action ? maybe give the choice
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_go_to_object(bpy.types.Operator):
|
||||||
|
bl_idname = "object.go_to_object"
|
||||||
|
bl_label = "Go To Object"
|
||||||
|
bl_description = "Go to object in pose mode"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
obj_name : bpy.props.StringProperty(options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
obj = context.scene.objects.get(self.obj_name)
|
||||||
|
if not obj:
|
||||||
|
self.report({'ERROR'}, f'Could not find object {self.obj_name} in scene objects')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||||
|
|
||||||
|
for ob in context.scene.objects:
|
||||||
|
ob.select_set(False)
|
||||||
|
|
||||||
|
# Set active
|
||||||
|
obj.select_set(True)
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
|
||||||
|
if obj.type == 'ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE', toggle=False)
|
||||||
|
self.report({'INFO'}, f'Back to pose mode, {obj.name}')
|
||||||
|
|
||||||
|
elif obj.type == 'GREASEPENCIL':
|
||||||
|
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL', toggle=False)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, f'Back to object mode, {obj.name}')
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_object_from_curve(bpy.types.Operator):
|
||||||
|
bl_idname = "object.object_from_curve"
|
||||||
|
bl_label = "Back To Following Object"
|
||||||
|
bl_description = "Go on following object from current curve"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'CURVE'
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
curve = context.object
|
||||||
|
self.objects = []
|
||||||
|
for o in context.scene.objects:
|
||||||
|
|
||||||
|
if o.type != 'ARMATURE':
|
||||||
|
for c in o.constraints:
|
||||||
|
if c.type == 'FOLLOW_PATH' and c.target and c.target == curve:
|
||||||
|
self.objects.append(o)
|
||||||
|
else:
|
||||||
|
for pb in o.pose.bones:
|
||||||
|
for c in pb.constraints:
|
||||||
|
if c.type == 'FOLLOW_PATH' and c.target and c.target == curve:
|
||||||
|
self.objects.append(o)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.objects:
|
||||||
|
self.report({'ERROR'}, 'No object following current curve found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
curve.select_set(False)
|
||||||
|
if len(self.objects) > 1:
|
||||||
|
return context.window_manager.invoke_props_popup(self, event) # execute on change
|
||||||
|
|
||||||
|
# set pose mode on only object available
|
||||||
|
obj = self.objects[0]
|
||||||
|
bpy.ops.object.go_to_object(obj_name=obj.name)
|
||||||
|
# bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||||
|
# for ob in context.scene.objects:
|
||||||
|
# ob.select_set(False)
|
||||||
|
# obj.select_set(True)
|
||||||
|
# context.view_layer.objects.active = obj
|
||||||
|
# bpy.ops.object.mode_set(mode='POSE', toggle=False)
|
||||||
|
# self.report({'INFO'}, f'Back to pose mode {obj.name} (constraint on {pb.name})')
|
||||||
|
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
for obj in self.objects:
|
||||||
|
layout.operator('object.go_to_object', text=obj.name, icon='OBJECT_DATA').obj_name = obj.name
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_create_follow_path_curve,
|
||||||
|
GPTB_OT_edit_curve,
|
||||||
|
GPTB_OT_remove_follow_path,
|
||||||
|
GPTB_OT_go_to_object,
|
||||||
|
GPTB_OT_object_from_curve,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -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)
|
455
OP_helpers.py
455
OP_helpers.py
|
@ -1,12 +1,19 @@
|
||||||
import bpy
|
import bpy
|
||||||
from mathutils import Vector#, Matrix
|
import mathutils
|
||||||
|
import math
|
||||||
|
|
||||||
|
from time import ctime
|
||||||
|
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"}
|
||||||
|
|
||||||
|
@ -42,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if not self.rename_all:
|
if not self.rename_all:
|
||||||
|
@ -56,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
|
||||||
else:
|
else:
|
||||||
oblist = []
|
oblist = []
|
||||||
for o in context.scene.objects:
|
for o in context.scene.objects:
|
||||||
if o.type == 'GPENCIL':
|
if o.type == 'GREASEPENCIL':
|
||||||
if o.name == o.data.name:
|
if o.name == o.data.name:
|
||||||
continue
|
continue
|
||||||
oblist.append(f'{o.data.name} -> {o.name}')
|
oblist.append(f'{o.data.name} -> {o.name}')
|
||||||
|
@ -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"}
|
||||||
|
|
||||||
|
@ -153,10 +190,11 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
||||||
# get main cam and error if not available
|
# get main cam and error if not available
|
||||||
if drawcam.name == 'draw_cam':
|
if drawcam.name == 'draw_cam':
|
||||||
maincam = drawcam.parent
|
maincam = drawcam.parent
|
||||||
|
maincam.data.show_passepartout = context.scene.gptoolprops.drawcam_passepartout
|
||||||
|
|
||||||
else:
|
else:
|
||||||
maincam = None
|
maincam = None
|
||||||
main_name = drawcam.get('maincam_name')# Custom prop with previous avtive cam.
|
main_name = drawcam.get('maincam_name')# Custom prop with previous active cam.
|
||||||
if main_name:
|
if main_name:
|
||||||
maincam = context.scene.objects.get(main_name)
|
maincam = context.scene.objects.get(main_name)
|
||||||
|
|
||||||
|
@ -169,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
|
||||||
|
@ -187,16 +225,22 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
||||||
if not drawcam:
|
if not drawcam:
|
||||||
created=True
|
created=True
|
||||||
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
|
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
|
||||||
drawcam.show_name = True
|
utils.set_collection(drawcam, 'manip_cams')
|
||||||
set_collection(drawcam, 'manip_cams')
|
|
||||||
|
|
||||||
if dcam_name == 'draw_cam':
|
if dcam_name == 'draw_cam':
|
||||||
drawcam.parent = maincam
|
drawcam.parent = maincam
|
||||||
if created:#set to main at creation time
|
if created: # set to main at creation time
|
||||||
drawcam.matrix_world = maincam.matrix_world
|
drawcam.matrix_world = maincam.matrix_world
|
||||||
drawcam.lock_location = (True,True,True)
|
drawcam.lock_location = (True,True,True)
|
||||||
|
# drawcam.hide_viewport = True
|
||||||
|
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout
|
||||||
|
drawcam.data = maincam.data # get data from parent
|
||||||
|
|
||||||
|
# Hide the other passepartout to let only the custom OpenGL one
|
||||||
|
maincam.data.show_passepartout = False
|
||||||
|
|
||||||
else:
|
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
|
||||||
|
@ -206,7 +250,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
||||||
drawcam.parent = act
|
drawcam.parent = act
|
||||||
vec = Vector((0,1,0))
|
vec = Vector((0,1,0))
|
||||||
|
|
||||||
if act.type == 'GPENCIL':
|
if act.type == 'GREASEPENCIL':
|
||||||
#change vector according to alignement
|
#change vector according to alignement
|
||||||
vec = get_gp_alignement_vector(context)
|
vec = get_gp_alignement_vector(context)
|
||||||
|
|
||||||
|
@ -226,7 +270,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
||||||
drawcam.hide_viewport = False
|
drawcam.hide_viewport = False
|
||||||
maincam.hide_viewport = True
|
maincam.hide_viewport = True
|
||||||
|
|
||||||
if created and drawcam.name == 'obj_cam':#Go in camera view
|
if created and drawcam.name == 'obj_cam': # Go in camera view
|
||||||
context.region_data.view_perspective = 'CAMERA'
|
context.region_data.view_perspective = 'CAMERA'
|
||||||
# ## make active
|
# ## make active
|
||||||
# bpy.context.view_layer.objects.active = ob
|
# bpy.context.view_layer.objects.active = ob
|
||||||
|
@ -234,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):
|
||||||
|
@ -275,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"}
|
||||||
|
|
||||||
|
@ -286,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 == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
|
||||||
continue
|
continue
|
||||||
if self.skip_obj and o.type != 'GPENCIL':
|
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||||
continue
|
continue
|
||||||
|
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# mute attribute animation for GP and cameras
|
||||||
|
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
|
||||||
|
gp_act = o.data.animation_data.action
|
||||||
|
if gp_act:
|
||||||
|
print(f'\n---{o.name} data:')
|
||||||
|
self.set_action_mute(gp_act)
|
||||||
|
|
||||||
if not o.animation_data:
|
if not o.animation_data:
|
||||||
continue
|
continue
|
||||||
|
@ -339,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 != 'GREASEPENCIL':
|
||||||
|
continue
|
||||||
|
for m in o.modifiers:
|
||||||
|
# skip modifier that are not visible in render
|
||||||
|
if not m.show_render:
|
||||||
|
continue
|
||||||
|
m.show_viewport = self.show
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_list_disabled_anims(Operator):
|
||||||
bl_idname = "gp.list_disabled_anims"
|
bl_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"}
|
||||||
|
|
||||||
|
@ -368,25 +506,41 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
|
||||||
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.skip_gp and o.type == 'GREASEPENCIL':
|
||||||
# continue
|
# continue
|
||||||
# if self.skip_obj and o.type != 'GPENCIL':
|
# if self.skip_obj and o.type != 'GREASEPENCIL':
|
||||||
# continue
|
# continue
|
||||||
|
|
||||||
|
if o.type == 'GREASEPENCIL':
|
||||||
|
if o.data.animation_data:
|
||||||
|
gp_act = o.data.animation_data.action
|
||||||
|
if gp_act:
|
||||||
|
for i, fcu in enumerate(gp_act.fcurves):
|
||||||
|
if fcu.mute:
|
||||||
|
if o not in oblist:
|
||||||
|
oblist.append(o)
|
||||||
|
li.append(f'{o.name}:')
|
||||||
|
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||||
|
|
||||||
if not o.animation_data:
|
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'}
|
||||||
|
@ -394,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"
|
||||||
|
@ -449,15 +603,202 @@ class GPTB_OT_overlay_presets(bpy.types.Operator):
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_clear_active_frame(Operator):
|
||||||
|
bl_idname = "gp.clear_active_frame"
|
||||||
|
bl_label = "Clear Active Frame"
|
||||||
|
bl_description = "Delete all strokes in active frames"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
obj = context.object
|
||||||
|
l = obj.data.layers.active
|
||||||
|
if not l:
|
||||||
|
self.report({'ERROR'}, 'No layers')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
f = l.current_frame()
|
||||||
|
if not f:
|
||||||
|
self.report({'ERROR'}, 'No active frame')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
ct = len(f.drawing.strokes)
|
||||||
|
if not ct:
|
||||||
|
self.report({'ERROR'}, 'Active frame already empty')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
for s in reversed(f.drawing.strokes):
|
||||||
|
f.drawing.strokes.remove(s)
|
||||||
|
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_check_canvas_alignement(Operator):
|
||||||
|
bl_idname = "gp.check_canvas_alignement"
|
||||||
|
bl_label = "Check Canvas Alignement"
|
||||||
|
bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
# if lock_axis is 'VIEW' then the draw axis is always aligned
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
|
||||||
|
self.report({'INFO'}, 'Drawing plane use "View" (always aligned)')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
_angle, ret, message = utils.check_angle_from_view(obj=context.object, context=context)
|
||||||
|
if not ret or not message:
|
||||||
|
self.report({'ERROR'}, 'Could not get view angle infos')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
title = 'Aligned \o/' if ret == 'INFO' else "Not aligned !"
|
||||||
|
|
||||||
|
if context.region_data.view_perspective != 'CAMERA':
|
||||||
|
title = title + ' ( not in camera view)'
|
||||||
|
|
||||||
|
utils.show_message_box(_message=message, _title=title, _icon=ret)
|
||||||
|
# self.report({ret}, message)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GPTB_OT_step_select_frames(Operator):
|
||||||
|
bl_idname = "gptb.step_select_frames"
|
||||||
|
bl_label = "Step Select Frame"
|
||||||
|
bl_description = "Select frames by a step frame value"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
start : bpy.props.IntProperty(name='Start Frame',
|
||||||
|
description='Start frame of the step animation',
|
||||||
|
default=100)
|
||||||
|
|
||||||
|
step : bpy.props.IntProperty(name='Step',
|
||||||
|
description='Step of the frame, value of 2 select one frame on two',
|
||||||
|
default=2,
|
||||||
|
min=2)
|
||||||
|
|
||||||
|
strict : bpy.props.BoolProperty(name='Strict',
|
||||||
|
description='Strictly select step frame from start to scene end range\
|
||||||
|
\nElse reset step when a gap exsits already',
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
# TODO: add option to start at cursor (True default)
|
||||||
|
|
||||||
|
def invoke(self, context, execute):
|
||||||
|
## list frame to keep
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||||
|
# return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
col = layout.column()
|
||||||
|
col.prop(self, 'step')
|
||||||
|
|
||||||
|
col.prop(self, 'strict')
|
||||||
|
if self.strict:
|
||||||
|
col.prop(self, 'start')
|
||||||
|
|
||||||
|
## helper (need more work)
|
||||||
|
# col.separator()
|
||||||
|
# range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}"
|
||||||
|
# col.label(text=f'Will keep {range_string}')
|
||||||
|
if not self.strict:
|
||||||
|
col.label(text=f'Each gap will be considered a new step start', icon='INFO')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)]
|
||||||
|
self.to_select = numbers
|
||||||
|
|
||||||
|
## Negative switch : list frames to remove
|
||||||
|
self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers]
|
||||||
|
|
||||||
|
gp = context.object.data
|
||||||
|
|
||||||
|
## Get frame summary (reset start after each existing gaps)
|
||||||
|
key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames]))
|
||||||
|
key_summary.sort()
|
||||||
|
print('key summary: ', key_summary)
|
||||||
|
|
||||||
|
start = key_summary[0]
|
||||||
|
if self.strict:
|
||||||
|
to_select = self.to_select
|
||||||
|
else:
|
||||||
|
to_select = []
|
||||||
|
prev = None
|
||||||
|
for k in key_summary:
|
||||||
|
print(k, prev)
|
||||||
|
if prev is not None and k != prev + 1:
|
||||||
|
## this is a gap ! new start
|
||||||
|
prev = start = k
|
||||||
|
# print('new start', start)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_range = [i for i in range(start, key_summary[-1] + 1, self.step)]
|
||||||
|
# print('new_range: ', new_range)
|
||||||
|
if k not in new_range:
|
||||||
|
to_select.append(k)
|
||||||
|
|
||||||
|
prev = k
|
||||||
|
|
||||||
|
## deselect all
|
||||||
|
for l in gp.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
f.select = False
|
||||||
|
|
||||||
|
print('To select:', to_select)
|
||||||
|
gct = 0
|
||||||
|
for i in to_select:
|
||||||
|
ct = 0
|
||||||
|
for l in gp.layers:
|
||||||
|
frame = next((f for f in l.frames if f.frame_number == i), None)
|
||||||
|
if not frame:
|
||||||
|
continue
|
||||||
|
|
||||||
|
## Select instead of remove
|
||||||
|
frame.select = True
|
||||||
|
## Optionnally remove frames ?
|
||||||
|
# l.frames.remove(frame)
|
||||||
|
|
||||||
|
ct += 1
|
||||||
|
|
||||||
|
# print(f'{i}: Selected {ct} frame(s)')
|
||||||
|
gct += ct
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'Selected {gct} frames')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_open_addon_prefs(Operator):
|
||||||
|
bl_idname = "gptb.open_addon_prefs"
|
||||||
|
bl_label = "Open Addon Prefs"
|
||||||
|
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
utils.open_addon_prefs()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
classes = (
|
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_check_canvas_alignement,
|
||||||
|
GPTB_OT_step_select_frames,
|
||||||
|
GPTB_OT_open_addon_prefs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_list(self, context):
|
||||||
|
'''return (identifier, name, description) of enum content'''
|
||||||
|
if not context:
|
||||||
|
return [('None', 'None','None')]
|
||||||
|
if not context.object:
|
||||||
|
return [('None', 'None','None')]
|
||||||
|
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||||
|
# try:
|
||||||
|
# except:
|
||||||
|
# return [("", "", "")]
|
||||||
|
# return [(i, basename(i), "") for i in blends]
|
||||||
|
# return [(i.path, basename(i.path), "") for i in self.blends]
|
||||||
|
|
||||||
|
class GPTB_OT_duplicate_send_to_layer(Operator) :
|
||||||
|
bl_idname = "gp.duplicate_send_to_layer"
|
||||||
|
bl_label = 'Duplicate Send To Layer'
|
||||||
|
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
|
||||||
|
# important to have the updated enum here as bl_property
|
||||||
|
bl_property = "layers_enum"
|
||||||
|
|
||||||
|
layers_enum : bpy.props.EnumProperty(
|
||||||
|
name="Duplicate to layers",
|
||||||
|
description="Duplicate selected keys in active layer and send them to chosen layer",
|
||||||
|
items=get_layer_list,
|
||||||
|
options={'HIDDEN'},
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(cls, context, properties):
|
||||||
|
if properties.delete_source:
|
||||||
|
return f"Move selected keys in active layer to chosen layer"
|
||||||
|
else:
|
||||||
|
return f"Copy selected keys in active layer and send to chosen layer"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'\
|
||||||
|
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
|
||||||
|
|
||||||
|
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
target_layer = self.layers_enum
|
||||||
|
if not target_layer:
|
||||||
|
self.report({'WARNING'}, 'Target layer not specified')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
gpl = context.object.data.layers
|
||||||
|
target_layer = gpl.get(target_layer)
|
||||||
|
|
||||||
|
act_layer = gpl.active
|
||||||
|
selected_frames = [f for f in act_layer.frames if f.select]
|
||||||
|
|
||||||
|
act_frame_num = [f.frame_number for f in act_layer.frames if f.select]
|
||||||
|
|
||||||
|
to_replace = [f for f in target_layer.frames if f.frame_number in act_frame_num]
|
||||||
|
|
||||||
|
replaced = len(to_replace)
|
||||||
|
|
||||||
|
## Remove overlapping frames
|
||||||
|
for f in reversed(to_replace):
|
||||||
|
target_layer.frames.remove(f.frame_number)
|
||||||
|
|
||||||
|
## Copy original frames
|
||||||
|
for f in selected_frames:
|
||||||
|
utils.copy_frame_at(f, target_layer, f.frame_number)
|
||||||
|
# target_layer.frames.copy(f) # GPv2
|
||||||
|
sent = len(selected_frames)
|
||||||
|
|
||||||
|
## Delete original frames as an option
|
||||||
|
if self.delete_source:
|
||||||
|
for f in reversed(selected_frames):
|
||||||
|
act_layer.frames.remove(f.frame_number)
|
||||||
|
mess = f'{sent} keys moved'
|
||||||
|
else:
|
||||||
|
mess = f'{sent} keys copied'
|
||||||
|
|
||||||
|
if replaced:
|
||||||
|
mess += f' ({replaced} replaced)'
|
||||||
|
|
||||||
|
self.report({'INFO'}, mess)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
gp = context.object.data
|
||||||
|
if not len(gp.layers):
|
||||||
|
self.report({'WARNING'}, 'No layers on current GP object')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
active = gp.layers.active
|
||||||
|
|
||||||
|
if not active:
|
||||||
|
self.report({'WARNING'}, 'No active layer to take keys from')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.selected_frames = [f for f in active.frames if f.select]
|
||||||
|
|
||||||
|
if not self.selected_frames:
|
||||||
|
self.report({'WARNING'}, 'No selected keys in active layer')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
wm = context.window_manager
|
||||||
|
wm.invoke_search_popup(self) # can't specify size... width=500, height=600
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
addon_keymaps = []
|
||||||
|
def register_keymaps():
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
|
||||||
|
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
|
||||||
|
addon_keymaps.append((km,kmi))
|
||||||
|
|
||||||
|
# km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter)
|
||||||
|
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True)
|
||||||
|
kmi.properties.delete_source = True
|
||||||
|
addon_keymaps.append((km,kmi))
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def menu_duplicate_and_send_to_layer(self, context):
|
||||||
|
if context.space_data.ui_mode == 'GPENCIL':
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN'
|
||||||
|
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
|
||||||
|
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_duplicate_send_to_layer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
register_keymaps()
|
||||||
|
bpy.types.DOPESHEET_MT_key.append(menu_duplicate_and_send_to_layer)
|
||||||
|
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
|
||||||
|
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
|
||||||
|
unregister_keymaps()
|
||||||
|
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -1,5 +1,6 @@
|
||||||
import bpy
|
import bpy
|
||||||
from .utils import get_addon_prefs
|
from .utils import get_addon_prefs, is_locked, is_hidden
|
||||||
|
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
|
||||||
|
|
||||||
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||||
bl_idname = "screen.gp_keyframe_jump"
|
bl_idname = "screen.gp_keyframe_jump"
|
||||||
|
@ -9,19 +10,34 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
next : bpy.props.BoolProperty(
|
next : BoolProperty(
|
||||||
name="Next GP keyframe", description="Go to next active GP keyframe", default=True)
|
name="Next GP keyframe", description="Go to next active GP keyframe",
|
||||||
|
default=True, options={'HIDDEN', 'SKIP_SAVE'})
|
||||||
|
|
||||||
target : bpy.props.EnumProperty(
|
target : EnumProperty(
|
||||||
name="Target layer", description="Choose wich layer to evaluate for keyframe change", default='ACTIVE',# options={'ANIMATABLE'}, update=None, get=None, set=None,
|
name="Target layer", description="Choose wich layer to evaluate for keyframe change",
|
||||||
|
default='ACTIVE', options={'HIDDEN', 'SKIP_SAVE'},
|
||||||
items=(
|
items=(
|
||||||
('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0),
|
('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0),
|
||||||
('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1),
|
('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1),
|
||||||
('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2),
|
('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2),
|
||||||
))
|
))
|
||||||
#(key, label, descr, id[, icon])
|
|
||||||
|
keyframe_type : EnumProperty(
|
||||||
|
name="Keyframe Filter", description="Jump to choosen keyframe type, else use the UI jump filter",
|
||||||
|
default='NONE', options={'HIDDEN', 'SKIP_SAVE'},
|
||||||
|
items=(
|
||||||
|
('NONE', 'Use UI Filter', '', 0), # 'KEYFRAME'
|
||||||
|
('ALL', 'All', '', 1),
|
||||||
|
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 2),
|
||||||
|
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 3),
|
||||||
|
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
|
||||||
|
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
|
||||||
|
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
|
||||||
|
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
|
||||||
|
))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if not context.object.data.layers.active:
|
if not context.object.data.layers.active:
|
||||||
|
@ -29,16 +45,21 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
if self.target == 'ACTIVE':
|
if self.target == 'ACTIVE':
|
||||||
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
|
gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
|
||||||
if not context.object.data.layers.active in gpl:
|
if not context.object.data.layers.active in gpl:
|
||||||
gpl.append(context.object.data.layers.active)
|
gpl.append(context.object.data.layers.active)
|
||||||
|
|
||||||
elif self.target == 'VISIBLE':
|
elif self.target == 'VISIBLE':
|
||||||
gpl = [l for l in context.object.data.layers if not l.hide]
|
gpl = [l for l in context.object.data.layers if not is_hidden(l)]
|
||||||
|
|
||||||
elif self.target == 'ACCESSIBLE':
|
elif self.target == 'ACCESSIBLE':
|
||||||
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
|
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||||
|
|
||||||
|
if self.keyframe_type != 'NONE':
|
||||||
|
# use shortcut choice override
|
||||||
|
kftype = self.keyframe_type
|
||||||
|
else:
|
||||||
|
kftype = context.scene.gptoolprops.keyframe_type
|
||||||
|
|
||||||
current = context.scene.frame_current
|
current = context.scene.frame_current
|
||||||
p = n = None
|
p = n = None
|
||||||
|
@ -47,6 +68,10 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||||
maxs = []
|
maxs = []
|
||||||
for l in gpl:
|
for l in gpl:
|
||||||
for f in l.frames:
|
for f in l.frames:
|
||||||
|
# keyframe type filter
|
||||||
|
if kftype != 'ALL' and f.keyframe_type != kftype:
|
||||||
|
continue
|
||||||
|
|
||||||
if f.frame_number < current:
|
if f.frame_number < current:
|
||||||
p = f.frame_number
|
p = f.frame_number
|
||||||
if f.frame_number > current:
|
if f.frame_number > current:
|
||||||
|
@ -64,10 +89,13 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||||
if maxs:
|
if maxs:
|
||||||
n = min(maxs)
|
n = min(maxs)
|
||||||
|
|
||||||
|
## Double the frame set to avoid refresh problem (had one in 2.91.2)
|
||||||
if self.next and n is not None:
|
if self.next and n is not None:
|
||||||
context.scene.frame_set(n)
|
context.scene.frame_set(n)
|
||||||
|
context.scene.frame_current = n
|
||||||
elif not self.next and p is not None:
|
elif not self.next and p is not None:
|
||||||
context.scene.frame_set(p)
|
context.scene.frame_set(p)
|
||||||
|
context.scene.frame_current = p
|
||||||
else:
|
else:
|
||||||
self.report({'INFO'}, 'No keyframe in this direction')
|
self.report({'INFO'}, 'No keyframe in this direction')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
@ -80,10 +108,10 @@ class KFJ_OT_rebinder(bpy.types.Operator):
|
||||||
bl_label = "rebind keyframe jump shortcut"
|
bl_label = "rebind keyframe jump shortcut"
|
||||||
bl_options = {'REGISTER', 'INTERNAL'}
|
bl_options = {'REGISTER', 'INTERNAL'}
|
||||||
|
|
||||||
s_keycode: bpy.props.StringProperty()
|
s_keycode: StringProperty()
|
||||||
s_ctrl: bpy.props.StringProperty()
|
s_ctrl: StringProperty()
|
||||||
s_shift: bpy.props.StringProperty()
|
s_shift: StringProperty()
|
||||||
s_alt: bpy.props.StringProperty()
|
s_alt: StringProperty()
|
||||||
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
@ -144,13 +172,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:
|
||||||
|
|
|
@ -0,0 +1,786 @@
|
||||||
|
from os import error
|
||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
|
||||||
|
from bpy.types import Operator
|
||||||
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
from .utils import get_addon_prefs, is_vector_close
|
||||||
|
|
||||||
|
|
||||||
|
# --- OPS ---
|
||||||
|
|
||||||
|
# PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
|
||||||
|
# pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter
|
||||||
|
# pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator
|
||||||
|
|
||||||
|
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six
|
||||||
|
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
|
||||||
|
# pattern = r'^(?P<tag>[A-Z]{1,6}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # named
|
||||||
|
# pattern = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # group start ' - '
|
||||||
|
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
|
||||||
|
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
|
||||||
|
|
||||||
|
# TODO: allow a more flexible prefix pattern
|
||||||
|
|
||||||
|
def layer_name_build(layer, prefix='', desc='', suffix=''):
|
||||||
|
'''GET a layer and argument to build and assign name
|
||||||
|
return new name
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
sep = prefs.separator
|
||||||
|
name = old = layer.name
|
||||||
|
|
||||||
|
pattern = PATTERN.replace('_', sep) # set separator
|
||||||
|
|
||||||
|
res = re.search(pattern, name.strip())
|
||||||
|
|
||||||
|
# prefix -> tag
|
||||||
|
# prefix2 -> tag2
|
||||||
|
# desc -> name
|
||||||
|
# suffix -> sfix
|
||||||
|
grp = '' if res.group('grp') is None else res.group('grp')
|
||||||
|
tag = '' if res.group('tag') is None else res.group('tag')
|
||||||
|
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
|
||||||
|
name = '' if res.group('name') is None else res.group('name')
|
||||||
|
sfix = '' if res.group('sfix') is None else res.group('sfix')
|
||||||
|
inc = '' if res.group('inc') is None else res.group('inc')
|
||||||
|
|
||||||
|
if grp:
|
||||||
|
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
if prefix == 'prefixkillcode':
|
||||||
|
tag = ''
|
||||||
|
else:
|
||||||
|
tag = prefix.upper().strip() + sep
|
||||||
|
# if prefix2:
|
||||||
|
# tag2 = prefix2.upper().strip() + sep
|
||||||
|
if desc:
|
||||||
|
name = desc
|
||||||
|
|
||||||
|
if suffix:
|
||||||
|
if suffix == 'suffixkillcode':
|
||||||
|
sfix = ''
|
||||||
|
else:
|
||||||
|
sfix = sep + suffix.upper().strip()
|
||||||
|
|
||||||
|
# check if name is available without the increment ending
|
||||||
|
new = f'{grp}{tag}{name}{sfix}'
|
||||||
|
|
||||||
|
layer.name = new
|
||||||
|
|
||||||
|
## update name in modifier targets
|
||||||
|
if old != new:
|
||||||
|
# find objects using this GP datablock
|
||||||
|
for ob_user in [o for o in bpy.data.objects if o.data == layer.id_data]: # bpy.context.scene.objects
|
||||||
|
# maybe a more elegant way exists to find all objects users ?
|
||||||
|
|
||||||
|
# update Gpencil modifier targets
|
||||||
|
for mod in ob_user.modifiers:
|
||||||
|
if not hasattr(mod, 'layer_filter'):
|
||||||
|
continue
|
||||||
|
if mod.layer_filter == old:
|
||||||
|
mod.layer_filter = new
|
||||||
|
|
||||||
|
"""
|
||||||
|
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
||||||
|
'''GET a layer and infos to build name
|
||||||
|
Can take one or two prefix and description/name of the layer)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
sep = prefs.separator
|
||||||
|
name = layer.name
|
||||||
|
|
||||||
|
pattern = pattern.replace('_', sep) # set separator
|
||||||
|
|
||||||
|
res = re.search(pattern, name.strip())
|
||||||
|
p1 = '' if res.group(1) is None else res.group(1)
|
||||||
|
p2 = '' if res.group(2) is None else res.group(2)
|
||||||
|
p3 = '' if res.group(3) is None else res.group(3)
|
||||||
|
p4 = '' if res.group(4) is None else res.group(4)
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
if prefix == 'prefixkillcode':
|
||||||
|
p1 = ''
|
||||||
|
else:
|
||||||
|
p1 = prefix.upper().strip() + sep
|
||||||
|
|
||||||
|
if prefix2:
|
||||||
|
p2 = prefix2.upper().strip() + sep
|
||||||
|
|
||||||
|
if desc:
|
||||||
|
p3 = desc
|
||||||
|
|
||||||
|
if suffix:
|
||||||
|
if suffix == 'suffixkillcode':
|
||||||
|
p4 = ''
|
||||||
|
else:
|
||||||
|
p4 = sep + suffix.upper().strip()
|
||||||
|
|
||||||
|
new = f'{p1}{p2}{p3}{p4}'
|
||||||
|
layer.name = new
|
||||||
|
"""
|
||||||
|
|
||||||
|
## multi-prefix solution (Caps letters)
|
||||||
|
class GPTB_OT_layer_name_build(Operator):
|
||||||
|
bl_idname = "gp.layer_name_build"
|
||||||
|
bl_label = "Layer Name Build"
|
||||||
|
bl_description = "Change prefix of layer name"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
prefix : StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
# prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
desc : StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
suffix : StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
tooltip : StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(cls, context, properties):
|
||||||
|
tag = properties.prefix if properties.prefix else properties.suffix
|
||||||
|
if properties.tooltip:
|
||||||
|
return f"Use prefix: {tag} ({properties.tooltip})"
|
||||||
|
else:
|
||||||
|
return f"Use prefix: {tag}"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
gpl = ob.data.layers
|
||||||
|
act = gpl.active
|
||||||
|
if not act:
|
||||||
|
act = ob.data.layer_groups.active
|
||||||
|
|
||||||
|
if not act:
|
||||||
|
self.report({'ERROR'}, 'No layer active')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
|
||||||
|
|
||||||
|
## /!\ Deactivate multi-selection on layer !
|
||||||
|
## Somethimes it affect a random layer that is still considered selected
|
||||||
|
# for l in gpl:
|
||||||
|
# if l.select or l == act:
|
||||||
|
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
#-## SELECTION MANAGEMENT ##-#
|
||||||
|
|
||||||
|
def activate_channel_group_color(context):
|
||||||
|
if not context.preferences.edit.use_anim_channel_group_colors:
|
||||||
|
context.preferences.edit.use_anim_channel_group_colors = True
|
||||||
|
|
||||||
|
def refresh_areas():
|
||||||
|
for area in bpy.context.screen.areas:
|
||||||
|
area.tag_redraw()
|
||||||
|
|
||||||
|
def build_layers_targets_from_dopesheet(context):
|
||||||
|
'''Return all selected layers on context GP dopesheet according to seelction and filters'''
|
||||||
|
ob = context.object
|
||||||
|
gpl = context.object.data.layers
|
||||||
|
act = gpl.active
|
||||||
|
dopeset = context.space_data.dopesheet
|
||||||
|
|
||||||
|
|
||||||
|
if dopeset.show_only_selected:
|
||||||
|
pool = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
|
||||||
|
else:
|
||||||
|
pool = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
|
||||||
|
if not dopeset.show_hidden:
|
||||||
|
pool = [o for o in pool if o.visible_get()]
|
||||||
|
|
||||||
|
layer_pool = [l for o in pool for l in o.data.layers]
|
||||||
|
layer_pool = list(set(layer_pool)) # remove dupli-layers from same data source with
|
||||||
|
|
||||||
|
# apply search filter
|
||||||
|
if dopeset.filter_text:
|
||||||
|
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.name.lower()) ^ dopeset.use_filter_invert]
|
||||||
|
|
||||||
|
return layer_pool
|
||||||
|
|
||||||
|
def build_dope_gp_list(layer_list):
|
||||||
|
'''Take a list of GP layers return a dict with pairs {gp data : own layer list}'''
|
||||||
|
from collections import defaultdict
|
||||||
|
gps = defaultdict(list)
|
||||||
|
for l in layer_list:
|
||||||
|
gps[l.id_data].append(l)
|
||||||
|
return gps
|
||||||
|
|
||||||
|
class GPTB_OT_select_set_same_prefix(Operator):
|
||||||
|
bl_idname = "gp.select_same_prefix"
|
||||||
|
bl_label = "Select Same Prefix"
|
||||||
|
bl_description = "Select layers that have the same prefix as active\nSet with ctrl+clic"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
|
||||||
|
items=(
|
||||||
|
('SELECT', "Select", "Select layer with same prefix as active"),
|
||||||
|
('SET', "Set", "Set prefix on selected layer to the same as active"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if event.ctrl:
|
||||||
|
self.mode = 'SET'
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
sep = prefs.separator # '_'
|
||||||
|
|
||||||
|
gp = context.object.data
|
||||||
|
act = gp.layers.active
|
||||||
|
|
||||||
|
pool = build_layers_targets_from_dopesheet(context)
|
||||||
|
if not pool:
|
||||||
|
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
gp_dic = build_dope_gp_list(pool)
|
||||||
|
if not act:
|
||||||
|
# Check in other displayed layer if there is an active one
|
||||||
|
for gp, _layer_list in gp_dic.items():
|
||||||
|
if gp.layers.active:
|
||||||
|
# overwrite gp variable at the same time
|
||||||
|
act = gp.layers.active
|
||||||
|
break
|
||||||
|
if not act:
|
||||||
|
self.report({'ERROR'}, 'No active layer to base action')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
|
||||||
|
|
||||||
|
res = re.search(PATTERN, act.name)
|
||||||
|
if not res:
|
||||||
|
self.report({'ERROR'}, f'Error scanning {act.name}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
namespace = res.group('tag')
|
||||||
|
if not namespace:
|
||||||
|
self.report({'WARNING'}, f'No prefix detected in {act.name} with separator {sep}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
if self.mode == 'SELECT':
|
||||||
|
## with split
|
||||||
|
# namespace = act.name.split(sep,1)[0]
|
||||||
|
# namespace_bool_list = [l.name.split(sep,1)[0] == namespace for l in gpl]
|
||||||
|
|
||||||
|
## with reg # only active
|
||||||
|
# namespace_bool_list = [l.name.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||||
|
# gpl.foreach_set('select', namespace_bool_list)
|
||||||
|
|
||||||
|
## don't work Need Foreach set per gp
|
||||||
|
# for l in pool:
|
||||||
|
# l.select = l.name.split(sep,1)[0] + sep == namespace
|
||||||
|
|
||||||
|
for gp, layers in gp_dic.items():
|
||||||
|
# check namespace + restrict selection to visible layers according to filters
|
||||||
|
# TODO : Should use the regex pattern to detect and compare r.group('tag')
|
||||||
|
namespace_bool_list = [(l in layers) and (l.name.split(sep,1)[0] + sep == namespace) for l in gp.layers]
|
||||||
|
gp.layers.foreach_set('select', namespace_bool_list)
|
||||||
|
|
||||||
|
elif self.mode == 'SET':
|
||||||
|
for l in pool:
|
||||||
|
if not l.select or l == act:
|
||||||
|
continue
|
||||||
|
layer_name_build(l, prefix=namespace.strip(sep))
|
||||||
|
|
||||||
|
refresh_areas()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_select_set_same_color(Operator):
|
||||||
|
bl_idname = "gp.select_same_color"
|
||||||
|
bl_label = "Select Same Color"
|
||||||
|
bl_description = "Select layers that have the same color as active\nSet with ctrl+clic"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
|
||||||
|
items=(
|
||||||
|
('SELECT', "Select", "Select layer with same prefix as active"),
|
||||||
|
('SET', "Set", "Set prefix on selected layer to the same as active"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if event.ctrl:
|
||||||
|
self.mode = 'SET'
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
gp = context.object.data
|
||||||
|
act = gp.layers.active
|
||||||
|
|
||||||
|
pool = build_layers_targets_from_dopesheet(context)
|
||||||
|
if not pool:
|
||||||
|
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
gp_dic = build_dope_gp_list(pool)
|
||||||
|
if not act:
|
||||||
|
# Check in other displayed layer if there is an active one
|
||||||
|
for gp, _layer_list in gp_dic.items():
|
||||||
|
if gp.layers.active:
|
||||||
|
# overwrite gp variable at the same time
|
||||||
|
act = gp.layers.active
|
||||||
|
break
|
||||||
|
if not act:
|
||||||
|
self.report({'ERROR'}, 'No active layer to base action')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
|
||||||
|
color = act.channel_color
|
||||||
|
if self.mode == 'SELECT':
|
||||||
|
## NEED FOREACH TO APPLY SELECT
|
||||||
|
|
||||||
|
## Only on active object
|
||||||
|
# same_color_bool = [l.channel_color == act.channel_color for l in gpl]
|
||||||
|
# gpl.foreach_set('select', same_color_bool) # only
|
||||||
|
|
||||||
|
# On multiple objects -- don't work, need foreach
|
||||||
|
# for l in pool:
|
||||||
|
# print(l.id_data.name, l.name, l.channel_color == act.channel_color)
|
||||||
|
# l.select = l.channel_color == act.channel_color
|
||||||
|
|
||||||
|
"""
|
||||||
|
gps = []
|
||||||
|
for l in pool:
|
||||||
|
if l.id_data not in gps:
|
||||||
|
gps.append(l.id_data)
|
||||||
|
for gp in gps:
|
||||||
|
same_color_bool = [(l in pool) and is_vector_close(l.channel_color, color) for l in gp.layers]
|
||||||
|
gp.layers.foreach_set('select', same_color_bool)
|
||||||
|
"""
|
||||||
|
for gp, layers in gp_dic.items():
|
||||||
|
# check color and restrict selection to visible layers according to filters
|
||||||
|
same_color_bool = [(l in layers) and is_vector_close(l.channel_color, color) for l in gp.layers]
|
||||||
|
gp.layers.foreach_set('select', same_color_bool)
|
||||||
|
|
||||||
|
elif self.mode == 'SET':
|
||||||
|
activate_channel_group_color(context)
|
||||||
|
for l in pool: # only on active object use gpl
|
||||||
|
if not l.select or l == act:
|
||||||
|
continue
|
||||||
|
l.channel_color = color
|
||||||
|
|
||||||
|
refresh_areas()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def replace_layer_name(target, replacement, selected_only=True, prefix_only=True, regex=False):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
sep = prefs.separator
|
||||||
|
if not target:
|
||||||
|
return
|
||||||
|
|
||||||
|
gpl = bpy.context.object.data.layers
|
||||||
|
|
||||||
|
if selected_only:
|
||||||
|
lays = [l for l in gpl if l.select] # exclude : l.name != 'background'
|
||||||
|
else:
|
||||||
|
lays = [l for l in gpl] # exclude : if l.name != 'background'
|
||||||
|
|
||||||
|
ct = 0
|
||||||
|
for l in lays:
|
||||||
|
old = l.name
|
||||||
|
if regex:
|
||||||
|
new = re.sub(target, replacement, l.name)
|
||||||
|
if old != new:
|
||||||
|
l.name = new
|
||||||
|
print('rename:', old, '-->', new)
|
||||||
|
ct += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if prefix_only:
|
||||||
|
if not sep in l.name:
|
||||||
|
# only if separator exists
|
||||||
|
continue
|
||||||
|
splited = l.name.split(sep)
|
||||||
|
prefix = splited[0]
|
||||||
|
new_prefix = prefix.replace(target, replacement)
|
||||||
|
if prefix != new_prefix:
|
||||||
|
splited[0] = new_prefix
|
||||||
|
l.name = sep.join(splited)
|
||||||
|
print('rename:', old, '-->', l.name)
|
||||||
|
ct += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
new = l.name.replace(target, replacement)
|
||||||
|
if old != new:
|
||||||
|
l.name = new
|
||||||
|
print('rename:', old, '-->', new)
|
||||||
|
ct += 1
|
||||||
|
return ct
|
||||||
|
|
||||||
|
class GPTB_OT_rename_gp_layer(Operator):
|
||||||
|
'''rename GP layers based on a search and replace'''
|
||||||
|
bl_idname = "gp.rename_gp_layers"
|
||||||
|
bl_label = "Rename Gp Layers"
|
||||||
|
bl_description = "Search/Replace string in all GP layers"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
|
||||||
|
replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
|
||||||
|
selected: BoolProperty(name="Selected Only", description="Affect only selected layers", default=False)
|
||||||
|
prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skip layer without separator in name)", default=False)
|
||||||
|
use_regex: BoolProperty(name="Regex", description="use regular expression (advanced), equivalent to python re.sub()", default=False)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
count = replace_layer_name(self.find, self.replace, selected_only=self.selected, prefix_only=self.prefix, regex=self.use_regex)
|
||||||
|
if count:
|
||||||
|
mess = str(count) + ' layers renamed'
|
||||||
|
self.report({'INFO'}, mess)
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, 'No text found !')
|
||||||
|
|
||||||
|
return{'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
row = layout.row()
|
||||||
|
row_a= row.row()
|
||||||
|
row_a.prop(self, "selected")
|
||||||
|
|
||||||
|
row_b= row.row()
|
||||||
|
row_b.prop(self, "prefix")
|
||||||
|
row_c= row.row()
|
||||||
|
|
||||||
|
row_c.prop(self, "use_regex")
|
||||||
|
row_b.active = not self.use_regex
|
||||||
|
|
||||||
|
layout.prop(self, "find")
|
||||||
|
layout.prop(self, "replace")
|
||||||
|
|
||||||
|
|
||||||
|
## --- UI layer panel---
|
||||||
|
|
||||||
|
def layer_name_builder_ui(self, context):
|
||||||
|
'''appended to DATA_PT_grease_pencil_layers'''
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
if not prefs.show_prefix_buttons:
|
||||||
|
return
|
||||||
|
if not len(prefs.prefixes.namespaces) and not len(prefs.suffixes.namespaces):
|
||||||
|
return
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||||
|
# layout.separator()
|
||||||
|
col = layout.column()
|
||||||
|
|
||||||
|
|
||||||
|
line_limit = 8
|
||||||
|
|
||||||
|
if len(prefs.prefixes.namespaces):
|
||||||
|
ct = 0
|
||||||
|
# can't use enumerate cause there can be hided prefix
|
||||||
|
for namespace in prefs.prefixes.namespaces:
|
||||||
|
if namespace.hide:
|
||||||
|
continue
|
||||||
|
if ct % line_limit == 0:
|
||||||
|
row = col.row(align=True)
|
||||||
|
ct += 1
|
||||||
|
op = row.operator("gp.layer_name_build", text=namespace.tag)
|
||||||
|
op.prefix = namespace.tag
|
||||||
|
op.tooltip = namespace.name
|
||||||
|
|
||||||
|
if ct > 0:
|
||||||
|
row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'
|
||||||
|
|
||||||
|
## old single string prefix method
|
||||||
|
"""
|
||||||
|
if prefs.prefixes:
|
||||||
|
p = prefs.prefixes.split(',')
|
||||||
|
for i, prefix in enumerate(all_prefixes):
|
||||||
|
if i % line_limit == 0:
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("gp.layer_name_build", text=prefix.upper() ).prefix = prefix
|
||||||
|
row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'
|
||||||
|
## secondary prefix ?
|
||||||
|
|
||||||
|
|
||||||
|
if prefs.suffixes:
|
||||||
|
all_suffixes = prefs.suffixes.split(',')
|
||||||
|
for i, suffix in enumerate(all_suffixes):
|
||||||
|
if i % line_limit == 0:
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix
|
||||||
|
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
|
||||||
|
"""
|
||||||
|
|
||||||
|
## name (description of layer content)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(context.scene.gptoolprops, 'layer_name', text='')
|
||||||
|
|
||||||
|
## mimic groups using dash (disabled for now)
|
||||||
|
# row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW')
|
||||||
|
# row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||||
|
## no need for desc ops, already trigerred from update
|
||||||
|
# row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name
|
||||||
|
|
||||||
|
if len(prefs.suffixes.namespaces):
|
||||||
|
ct = 0
|
||||||
|
# can't use enumerate cause there can be hided prefix
|
||||||
|
for namespace in prefs.suffixes.namespaces:
|
||||||
|
if namespace.hide:
|
||||||
|
continue
|
||||||
|
if ct % line_limit == 0:
|
||||||
|
row = col.row(align=True)
|
||||||
|
ct += 1
|
||||||
|
op = row.operator("gp.layer_name_build", text=namespace.tag)
|
||||||
|
op.suffix = namespace.tag
|
||||||
|
op.tooltip = namespace.name
|
||||||
|
|
||||||
|
if ct > 0:
|
||||||
|
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
|
||||||
|
|
||||||
|
## --- UI dopesheet ---
|
||||||
|
|
||||||
|
def gpencil_dopesheet_header(self, context):
|
||||||
|
'''to append in DOPESHEET_HT_header'''
|
||||||
|
layout = self.layout
|
||||||
|
st = context.space_data
|
||||||
|
if st.mode != 'GREASEPENCIL':
|
||||||
|
return
|
||||||
|
|
||||||
|
row = layout.row(align=True)
|
||||||
|
# row.operator('gp.active_channel_color_to_selected', text='', icon='RESTRICT_COLOR_ON')
|
||||||
|
row.operator('gp.select_same_prefix', text='', icon='SYNTAX_OFF') # SORTALPHA, SMALL_CAPS
|
||||||
|
row.operator('gp.select_same_color', text='', icon='RESTRICT_COLOR_ON')
|
||||||
|
|
||||||
|
|
||||||
|
## --- UI context menu ---
|
||||||
|
|
||||||
|
def gpencil_layer_dropdown_menu(self, context):
|
||||||
|
'''to append in GPENCIL_MT_layer_context_menu'''
|
||||||
|
self.layout.operator('gp.create_empty_frames', icon='KEYFRAME')
|
||||||
|
self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')
|
||||||
|
|
||||||
|
## handler and msgbus
|
||||||
|
|
||||||
|
def obj_layer_name_callback():
|
||||||
|
'''assign layer name properties so user an tweak it'''
|
||||||
|
ob = bpy.context.object
|
||||||
|
if not ob or ob.type != 'GREASEPENCIL':
|
||||||
|
return
|
||||||
|
if not ob.data.layers.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
## Set selection to active object ot avoid un-sync selection on Layers stack
|
||||||
|
## (happen when an objet is selected but not active with 'lock object mode')
|
||||||
|
for l in ob.data.layers:
|
||||||
|
l.select = l == ob.data.layers.active
|
||||||
|
|
||||||
|
res = re.search(PATTERN, ob.data.layers.active.name.strip())
|
||||||
|
if not res:
|
||||||
|
return
|
||||||
|
if not res.group('name'):
|
||||||
|
return
|
||||||
|
# print('grp:', res.group('grp'))
|
||||||
|
# print('tag:', res.group('tag'))
|
||||||
|
# print('name:', res.group('name'))
|
||||||
|
# print('sfix:', res.group('sfix'))
|
||||||
|
# print('inc:', res.group('inc'))
|
||||||
|
bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
|
||||||
|
|
||||||
|
## old gpv2
|
||||||
|
# def subscribe_layer_change():
|
||||||
|
# subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
|
||||||
|
# bpy.msgbus.subscribe_rna(
|
||||||
|
# key=subscribe_to,
|
||||||
|
# # owner of msgbus subcribe (for clearing later)
|
||||||
|
# # owner=handle,
|
||||||
|
# owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
|
||||||
|
# # Args passed to callback function (tuple)
|
||||||
|
# args=(),
|
||||||
|
# # Callback function for property update
|
||||||
|
# notify=obj_layer_name_callback,
|
||||||
|
# options={'PERSISTENT'},
|
||||||
|
# )
|
||||||
|
|
||||||
|
def subscribe_layer_change():
|
||||||
|
subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
|
||||||
|
bpy.msgbus.subscribe_rna(
|
||||||
|
key=subscribe_to,
|
||||||
|
# owner of msgbus subcribe (for clearing later)
|
||||||
|
# owner=handle,
|
||||||
|
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime...
|
||||||
|
# Args passed to callback function (tuple)
|
||||||
|
args=(),
|
||||||
|
# Callback function for property update
|
||||||
|
notify=obj_layer_name_callback,
|
||||||
|
options={'PERSISTENT'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def subscribe_layer_change_handler(dummy):
|
||||||
|
subscribe_layer_change()
|
||||||
|
|
||||||
|
##--- Add layers
|
||||||
|
|
||||||
|
class GPTB_PT_layer_name_ui(bpy.types.Panel):
|
||||||
|
bl_space_type = 'TOPBAR' # dummy
|
||||||
|
bl_region_type = 'HEADER'
|
||||||
|
bl_options = {'INSTANCED'}
|
||||||
|
bl_label = 'Layer Rename'
|
||||||
|
bl_ui_units_x = 14
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# all_addons_l = get_modifier_list()
|
||||||
|
wm = context.window_manager
|
||||||
|
wm.invoke_props_dialog(self) # , width=600
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
# def row_with_icon(layout, icon):
|
||||||
|
# # Edit first editable button in popup
|
||||||
|
# row = layout.row()
|
||||||
|
# row.activate_init = True
|
||||||
|
# row.label(icon=icon)
|
||||||
|
# return row
|
||||||
|
# row = row_with_icon(layout, 'OUTLINER_DATA_GP_LAYER')
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.activate_init = True
|
||||||
|
row.label(icon='OUTLINER_DATA_GP_LAYER')
|
||||||
|
row.prop(context.object.data.layers.active, 'name', text='')
|
||||||
|
|
||||||
|
def add_layer(context):
|
||||||
|
bpy.ops.gpencil.layer_add()
|
||||||
|
context.object.data.layers.active.use_lights = False
|
||||||
|
|
||||||
|
class GPTB_OT_add_gp_layer_with_rename(Operator):
|
||||||
|
bl_idname = "gp.add_layer_rename"
|
||||||
|
bl_label = "Add Rename GPencil Layer"
|
||||||
|
bl_description = "Create a new gp layer with use light toggled off and popup a rename box"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
add_layer(context)
|
||||||
|
bpy.ops.wm.call_panel(name="GPTB_PT_layer_name_ui", keep_open = False)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class GPTB_OT_add_gp_layer(Operator):
|
||||||
|
bl_idname = "gp.add_layer"
|
||||||
|
bl_label = "Add GPencil Layer"
|
||||||
|
bl_description = "Create a new gp layer with use light toggled off"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
add_layer(context)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
addon_keymaps = []
|
||||||
|
def register_keymaps():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
|
||||||
|
##---# Insert Layers
|
||||||
|
## Insert new gp layer (with no use_light)
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
|
||||||
|
kmi = km.keymap_items.new('gp.add_layer', type='INSERT', value='PRESS')
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
## Insert new gp layer (with no use_light and immediately pop up a box to rename)
|
||||||
|
# km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
|
||||||
|
kmi = km.keymap_items.new('gp.add_layer_rename', type='INSERT', value='PRESS', shift=True)
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
##---# F2 rename calls
|
||||||
|
## Direct rename active layer in Paint mode
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
|
||||||
|
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
|
||||||
|
kmi.properties.name = 'GPTB_PT_layer_name_ui'
|
||||||
|
kmi.properties.keep_open = False
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
## Same in edit mode
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil Stroke Edit Mode", space_type = "EMPTY")
|
||||||
|
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
|
||||||
|
kmi.properties.name = 'GPTB_PT_layer_name_ui'
|
||||||
|
kmi.properties.keep_open = False
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
GPTB_OT_rename_gp_layer,
|
||||||
|
GPTB_OT_layer_name_build,
|
||||||
|
GPTB_OT_select_set_same_prefix,
|
||||||
|
GPTB_OT_select_set_same_color,
|
||||||
|
|
||||||
|
## Layer add and pop-up rename
|
||||||
|
GPTB_PT_layer_name_ui, # pop-up
|
||||||
|
GPTB_OT_add_gp_layer_with_rename, # shift+Ins
|
||||||
|
GPTB_OT_add_gp_layer, # Ins
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui)
|
||||||
|
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
|
||||||
|
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.append(gpencil_layer_dropdown_menu)
|
||||||
|
bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
# Directly set msgbus to work at first addon activation
|
||||||
|
bpy.app.timers.register(subscribe_layer_change, first_interval=1)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
unregister_keymaps()
|
||||||
|
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
|
||||||
|
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.remove(gpencil_layer_dropdown_menu)
|
||||||
|
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
|
||||||
|
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui)
|
||||||
|
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
# Delete layer index trigger
|
||||||
|
# /!\ can remove msgbus made for other functions or other addons using same owner
|
||||||
|
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
|
@ -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)
|
|
@ -0,0 +1,152 @@
|
||||||
|
import bpy
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
class GPT_OT_layer_nav(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.layer_nav"
|
||||||
|
bl_label = "GP Layer Navigator"
|
||||||
|
bl_description = "Change active GP layer and highlight active for a moment"
|
||||||
|
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
|
||||||
|
|
||||||
|
direction : bpy.props.EnumProperty(
|
||||||
|
name='direction',
|
||||||
|
items=(('UP', 'Up', ''),('DOWN', 'Down', ''), ('NONE', 'None', '')),
|
||||||
|
default='UP',
|
||||||
|
description='Direction to change layer in active GPencil stack',
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
## hardcoded values
|
||||||
|
# interval = 0.04 # 0.1
|
||||||
|
# limit = 1.8
|
||||||
|
# fade_val = 0.35
|
||||||
|
# use_fade_in = True
|
||||||
|
# fade_in_time = 0.5
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
## initialise vvalue from prefs
|
||||||
|
prefs = utils.get_addon_prefs()
|
||||||
|
if not prefs.nav_use_fade:
|
||||||
|
if self.direction == 'DOWN':
|
||||||
|
utils.iterate_active_layer(context.grease_pencil, -1)
|
||||||
|
# utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'name') # gpv2
|
||||||
|
|
||||||
|
if self.direction == 'UP':
|
||||||
|
utils.iterate_active_layer(context.grease_pencil, 1)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
## get up and down keys for use in modal
|
||||||
|
self.up_keys = []
|
||||||
|
self.down_keys = []
|
||||||
|
for km in context.window_manager.keyconfigs.user.keymaps:
|
||||||
|
for kmi in km.keymap_items:
|
||||||
|
if kmi.idname == 'gp.layer_nav':
|
||||||
|
if kmi.properties.direction == 'UP':
|
||||||
|
self.up_keys.append(kmi.type)
|
||||||
|
elif kmi.properties.direction == 'DOWN':
|
||||||
|
self.down_keys.append(kmi.type)
|
||||||
|
|
||||||
|
self.interval = prefs.nav_interval
|
||||||
|
self.limit = prefs.nav_limit
|
||||||
|
self.fade_val = prefs.nav_fade_val
|
||||||
|
self.use_fade_in = prefs.nav_use_fade_in
|
||||||
|
self.fade_in_time = prefs.nav_fade_in_time
|
||||||
|
|
||||||
|
self.lapse = 0
|
||||||
|
wm = context.window_manager
|
||||||
|
args = (self, context)
|
||||||
|
|
||||||
|
if context.space_data.overlay.use_gpencil_fade_layers:
|
||||||
|
self.fade_target = context.space_data.overlay.gpencil_fade_layer
|
||||||
|
else:
|
||||||
|
self.fade_target = 1.0
|
||||||
|
|
||||||
|
self.fade_start = self.limit - self.fade_in_time
|
||||||
|
|
||||||
|
self.first = True
|
||||||
|
self._timer = wm.event_timer_add(self.interval, window=context.window)
|
||||||
|
wm.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def store_settings(self, context):
|
||||||
|
self.org_use_gpencil_fade_layers = context.space_data.overlay.use_gpencil_fade_layers
|
||||||
|
self.org_gpencil_fade_layer = context.space_data.overlay.gpencil_fade_layer
|
||||||
|
context.space_data.overlay.use_gpencil_fade_layers = True
|
||||||
|
context.space_data.overlay.gpencil_fade_layer = self.fade_val
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
trigger = False
|
||||||
|
if event.type in {'RIGHTMOUSE', 'ESC', 'LEFTMOUSE'}:
|
||||||
|
self.stop_mod(context)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if event.type == 'TIMER':
|
||||||
|
self.lapse += self.interval
|
||||||
|
|
||||||
|
if self.lapse >= self.limit:
|
||||||
|
self.stop_mod(context)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
## Fade
|
||||||
|
if self.use_fade_in and (self.lapse > self.fade_start):
|
||||||
|
fade = utils.transfer_value(self.lapse, self.fade_start, self.limit, self.fade_val, self.fade_target)
|
||||||
|
# print(f'lapse {self.lapse} - fade {fade}')
|
||||||
|
context.space_data.overlay.gpencil_fade_layer = fade
|
||||||
|
|
||||||
|
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
|
||||||
|
_val = utils.iterate_active_layer(context.grease_pencil, -1)
|
||||||
|
trigger = True
|
||||||
|
|
||||||
|
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
|
||||||
|
_val = utils.iterate_active_layer(context.grease_pencil, 1)
|
||||||
|
trigger = True
|
||||||
|
|
||||||
|
if trigger:
|
||||||
|
self.direction = 'NONE'
|
||||||
|
if self.first:
|
||||||
|
self.store_settings(context)
|
||||||
|
self.first=False
|
||||||
|
|
||||||
|
if self.use_fade_in:
|
||||||
|
# reset fade to wanted value
|
||||||
|
context.space_data.overlay.gpencil_fade_layer = self.fade_val
|
||||||
|
|
||||||
|
self.lapse = 0 # reset counter
|
||||||
|
return {'RUNNING_MODAL'}#running modal prevent original usage to be triggered (capture keys)
|
||||||
|
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
def stop_mod(self, context):
|
||||||
|
# restore fade
|
||||||
|
context.space_data.overlay.use_gpencil_fade_layers = self.org_use_gpencil_fade_layers
|
||||||
|
context.space_data.overlay.gpencil_fade_layer = self.org_gpencil_fade_layer
|
||||||
|
wm = context.window_manager
|
||||||
|
wm.event_timer_remove(self._timer)
|
||||||
|
|
||||||
|
|
||||||
|
addon_keymaps = []
|
||||||
|
|
||||||
|
def register_keymap():
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS')
|
||||||
|
kmi.properties.direction = 'UP'
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_DOWN', value='PRESS')
|
||||||
|
kmi.properties.direction = 'DOWN'
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister_keymap():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(GPT_OT_layer_nav)
|
||||||
|
register_keymap()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
unregister_keymap()
|
||||||
|
bpy.utils.unregister_class(GPT_OT_layer_nav)
|
|
@ -0,0 +1,168 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
import mathutils
|
||||||
|
from mathutils import Vector, Matrix, geometry
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
from time import time
|
||||||
|
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
|
||||||
|
|
||||||
|
class GP_OT_pick_closest_layer(Operator):
|
||||||
|
bl_idname = "gp.pick_closest_layer"
|
||||||
|
bl_label = "Get Closest Stroke Layer"
|
||||||
|
bl_description = "Pick closest stroke layer"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||||
|
|
||||||
|
stroke_filter : bpy.props.EnumProperty(name='Target',
|
||||||
|
default='STROKE',
|
||||||
|
items=(
|
||||||
|
('STROKE', 'Stroke', 'Target only Stroke', 0),
|
||||||
|
('FILL', 'Fill', 'Target only Fill', 1),
|
||||||
|
('ALL', 'All', 'All stroke types', 2),
|
||||||
|
),
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
def filter_stroke(self, context):
|
||||||
|
kd = mathutils.kdtree.KDTree(len(self.point_pair))
|
||||||
|
for i, pair in enumerate(self.point_pair):
|
||||||
|
kd.insert(pair[0], i)
|
||||||
|
kd.balance()
|
||||||
|
|
||||||
|
mouse_vec3 = Vector((*self.init_mouse, 0))
|
||||||
|
co, index, _dist = kd.find(mouse_vec3)
|
||||||
|
layer = self.point_pair[index][1]
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.t0 = time()
|
||||||
|
self.limit = self.t0 + 0.2 # 200 miliseconds
|
||||||
|
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
self.idx = None
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
if context.object.data.layers.active:
|
||||||
|
layout.label(text=f'Layer: {context.object.data.layers.active.name}')
|
||||||
|
layout.prop(self, 'stroke_filter')
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if time() > self.limit:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised)
|
||||||
|
if time() > self.limit:
|
||||||
|
# dont know if condition is neeed
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return self.execute(context)
|
||||||
|
# return {'FINISHED'}
|
||||||
|
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
# return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
t1 = time()
|
||||||
|
# self.prefs = get_addon_prefs()
|
||||||
|
self.ob = context.object
|
||||||
|
mat = self.ob.matrix_world
|
||||||
|
gp = self.ob.data
|
||||||
|
|
||||||
|
self.inv_mat = self.ob.matrix_world.inverted()
|
||||||
|
self.point_pair = []
|
||||||
|
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||||
|
for layer in gp.layers:
|
||||||
|
if is_hidden(layer):
|
||||||
|
continue
|
||||||
|
for f in layer.frames:
|
||||||
|
if not f.select:
|
||||||
|
continue
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
|
||||||
|
continue
|
||||||
|
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
|
||||||
|
continue
|
||||||
|
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||||
|
for layer in gp.layers:
|
||||||
|
if is_hidden(layer) or not layer.current_frame():
|
||||||
|
continue
|
||||||
|
for s in layer.current_frame().drawing.strokes:
|
||||||
|
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
|
||||||
|
continue
|
||||||
|
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
|
||||||
|
continue
|
||||||
|
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
|
||||||
|
|
||||||
|
if not self.point_pair:
|
||||||
|
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
layer_target = self.filter_stroke(context)
|
||||||
|
if isinstance(layer_target, str):
|
||||||
|
self.report({'ERROR'}, layer_target)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
del self.point_pair # auto garbage collected ?
|
||||||
|
|
||||||
|
self.ob.data.layers.active = layer_target
|
||||||
|
|
||||||
|
## debug show trigger time
|
||||||
|
# print(f'Trigger time {time() - self.t0:.3f}')
|
||||||
|
# print(f'Search time {time() - t1:.3f}')
|
||||||
|
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.name}')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
addon_keymaps = []
|
||||||
|
def register_keymaps():
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new(
|
||||||
|
# name="",
|
||||||
|
idname="gp.pick_closest_layer",
|
||||||
|
type="W",
|
||||||
|
value="PRESS",
|
||||||
|
)
|
||||||
|
kmi.properties.stroke_filter = 'STROKE'
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new(
|
||||||
|
# name="",
|
||||||
|
idname="gp.pick_closest_layer",
|
||||||
|
type="W",
|
||||||
|
value="PRESS",
|
||||||
|
alt = True,
|
||||||
|
)
|
||||||
|
kmi.properties.stroke_filter = 'FILL'
|
||||||
|
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
GP_OT_pick_closest_layer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
unregister_keymaps()
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,183 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
import mathutils
|
||||||
|
from mathutils import Vector, Matrix, geometry
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
# def get_layer_list(self, context):
|
||||||
|
# '''return (identifier, name, description) of enum content'''
|
||||||
|
# if not context:
|
||||||
|
# return [('None', 'None','None')]
|
||||||
|
# if not context.object:
|
||||||
|
# return [('None', 'None','None')]
|
||||||
|
# return [(l.name, l.name, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
|
||||||
|
|
||||||
|
## in Class
|
||||||
|
# bl_property = "layers_enum"
|
||||||
|
|
||||||
|
# layers_enum : bpy.props.EnumProperty(
|
||||||
|
# name="Send Material To Layer",
|
||||||
|
# description="Send active material to layer",
|
||||||
|
# items=get_layer_list,
|
||||||
|
# options={'HIDDEN'},
|
||||||
|
# )
|
||||||
|
|
||||||
|
class GPTB_OT_move_material_to_layer(Operator) :
|
||||||
|
bl_idname = "gp.move_material_to_layer"
|
||||||
|
bl_label = 'Move Material To Layer'
|
||||||
|
bl_description = 'Move active material to an existing or new layer'
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
layer_name : bpy.props.StringProperty(
|
||||||
|
name='Layer Name', default='', options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
copy : bpy.props.BoolProperty(
|
||||||
|
name='Copy to layer', default=False,
|
||||||
|
description='Copy strokes to layer instead of moving',
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if self.layer_name:
|
||||||
|
return self.execute(context)
|
||||||
|
if not len(context.object.data.layers):
|
||||||
|
self.report({'WARNING'}, 'No layers on current GP object')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
mat = context.object.data.materials[context.object.active_material_index]
|
||||||
|
self.mat_name = mat.name
|
||||||
|
|
||||||
|
# wm.invoke_search_popup(self)
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
# layout.operator_context = "INVOKE_DEFAULT"
|
||||||
|
layout.prop(self, 'copy', text='Copy Strokes')
|
||||||
|
action_label = 'Copy' if self.copy else 'Move'
|
||||||
|
layout.label(text=f'{action_label} material "{self.mat_name}" to layer:', icon='MATERIAL')
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
col.prop(self, 'layer_name', text='', icon='ADD')
|
||||||
|
# if self.layer_name:
|
||||||
|
# col.label(text='Ok/Enter to create new layer', icon='INFO')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
for l in reversed(context.object.data.layers):
|
||||||
|
|
||||||
|
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
|
||||||
|
row = col.row()
|
||||||
|
row.alignment = 'LEFT'
|
||||||
|
op = col.operator('gp.move_material_to_layer', text=l.name, icon=icon, emboss=False)
|
||||||
|
op.layer_name = l.name
|
||||||
|
op.copy = self.copy
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if not self.layer_name:
|
||||||
|
print('Out')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
## Active + selection
|
||||||
|
pool = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||||||
|
if not context.object in pool:
|
||||||
|
pool.append(context.object)
|
||||||
|
|
||||||
|
mat = context.object.data.materials[context.object.active_material_index]
|
||||||
|
|
||||||
|
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
|
||||||
|
# import time
|
||||||
|
# t = time.time() # Dbg
|
||||||
|
total = 0
|
||||||
|
oct = 0
|
||||||
|
|
||||||
|
for ob in pool:
|
||||||
|
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
|
||||||
|
if mat_index is None:
|
||||||
|
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
|
||||||
|
continue
|
||||||
|
|
||||||
|
gpl = ob.data.layers
|
||||||
|
|
||||||
|
if not (target_layer := gpl.get(self.layer_name)):
|
||||||
|
target_layer = gpl.new(self.layer_name)
|
||||||
|
|
||||||
|
## List existing frames
|
||||||
|
key_dict = {f.frame_number : f for f in target_layer.frames}
|
||||||
|
|
||||||
|
### Move Strokes to a new key (or existing key if comming for yet another layer)
|
||||||
|
fct = 0
|
||||||
|
sct = 0
|
||||||
|
for layer in gpl:
|
||||||
|
if layer == target_layer:
|
||||||
|
## ! infinite loop if target layer is included
|
||||||
|
continue
|
||||||
|
for fr in layer.frames:
|
||||||
|
## skip if no stroke has active material
|
||||||
|
if not next((s for s in fr.drawing.strokes if s.material_index == mat_index), None):
|
||||||
|
continue
|
||||||
|
## Get/Create a destination frame and keep a reference to it
|
||||||
|
if not (dest_key := key_dict.get(fr.frame_number)):
|
||||||
|
dest_key = target_layer.frames.new(fr.frame_number)
|
||||||
|
key_dict[dest_key.frame_number] = dest_key
|
||||||
|
|
||||||
|
print(f'{ob.name} : frame {fr.frame_number}')
|
||||||
|
## Replicate strokes in dest_keys
|
||||||
|
stroke_to_delete = []
|
||||||
|
for s_idx, s in enumerate(fr.drawing.strokes):
|
||||||
|
if s.material_index == mat_index:
|
||||||
|
utils.copy_stroke_to_frame(s, dest_key)
|
||||||
|
stroke_to_delete.append(s_idx)
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
# if time.time() - t > 10:
|
||||||
|
# print('TIMEOUT')
|
||||||
|
# return {'CANCELLED'}
|
||||||
|
|
||||||
|
sct += len(stroke_to_delete)
|
||||||
|
|
||||||
|
## Remove from source frame (fr)
|
||||||
|
if not self.copy:
|
||||||
|
# print('Removing frames') # Dbg
|
||||||
|
if stroke_to_delete:
|
||||||
|
fr.drawing.remove_strokes(indices=stroke_to_delete)
|
||||||
|
|
||||||
|
## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame
|
||||||
|
|
||||||
|
fct += 1
|
||||||
|
|
||||||
|
|
||||||
|
if fct:
|
||||||
|
oct += 1
|
||||||
|
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
|
||||||
|
|
||||||
|
total += fct
|
||||||
|
|
||||||
|
report_type = 'INFO' if total else 'WARNING'
|
||||||
|
if self.copy:
|
||||||
|
self.report({report_type}, f'Copied {total} frames accross {oct} object(s)')
|
||||||
|
else:
|
||||||
|
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
# def menu_duplicate_and_send_to_layer(self, context):
|
||||||
|
# if context.space_data.ui_mode == 'GPENCIL':
|
||||||
|
# self.layout.operator_context = 'INVOKE_REGION_WIN'
|
||||||
|
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
|
||||||
|
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_move_material_to_layer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,346 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
import mathutils
|
||||||
|
from mathutils import Vector, Matrix, geometry
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
from time import time
|
||||||
|
from .utils import (get_gp_draw_plane,
|
||||||
|
location_to_region,
|
||||||
|
region_to_location,
|
||||||
|
is_locked,
|
||||||
|
is_hidden)
|
||||||
|
|
||||||
|
|
||||||
|
### passing by 2D projection
|
||||||
|
def get_3d_coord_on_drawing_plane_from_2d(context, co):
|
||||||
|
plane_co, plane_no = get_gp_draw_plane()
|
||||||
|
rv3d = context.region_data
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
if not plane_no:
|
||||||
|
plane_no = Vector((0,0,1))
|
||||||
|
plane_no.rotate(view_mat)
|
||||||
|
depth_3d = view_mat @ Vector((0, 0, -1000))
|
||||||
|
org = region_to_location(co, view_mat.to_translation())
|
||||||
|
view_point = region_to_location(co, depth_3d)
|
||||||
|
hit = geometry.intersect_line_plane(org, view_point, plane_co, plane_no)
|
||||||
|
|
||||||
|
if hit and plane_no:
|
||||||
|
return context.object, hit, plane_no
|
||||||
|
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
"""
|
||||||
|
class GP_OT_pick_closest_material(Operator):
|
||||||
|
bl_idname = "gp.pick_closest_material"
|
||||||
|
bl_label = "Get Closest Stroke Material"
|
||||||
|
bl_description = "Pick closest stroke material"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||||
|
|
||||||
|
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
def filter_stroke(self, context):
|
||||||
|
# get stroke under mouse using kdtree
|
||||||
|
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||||
|
|
||||||
|
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||||
|
for i, pair in enumerate(point_pair):
|
||||||
|
kd.insert(pair[0], i)
|
||||||
|
kd.balance()
|
||||||
|
|
||||||
|
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing
|
||||||
|
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||||
|
|
||||||
|
if not hit:
|
||||||
|
return 'No hit on drawing plane', None
|
||||||
|
|
||||||
|
mouse_3d = hit
|
||||||
|
mouse_local = self.inv_mat @ mouse_3d # local space
|
||||||
|
co, index, _dist = kd.find(mouse_local) # local space
|
||||||
|
# co, index, _dist = kd.find(mouse_3d) # world space
|
||||||
|
# context.scene.cursor.location = co # world space
|
||||||
|
s = point_pair[index][1]
|
||||||
|
|
||||||
|
## find point index in stroke
|
||||||
|
self.idx = None
|
||||||
|
for i, p in enumerate(s.points):
|
||||||
|
if p.position == co:
|
||||||
|
self.idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
del point_pair
|
||||||
|
return s, self.ob.matrix_world @ co
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# self.prefs = get_addon_prefs()
|
||||||
|
self.ob = context.object
|
||||||
|
self.gp = self.ob.data
|
||||||
|
|
||||||
|
self.stroke_list = []
|
||||||
|
self.inv_mat = self.ob.matrix_world.inverted()
|
||||||
|
|
||||||
|
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||||
|
for l in self.gp.layers:
|
||||||
|
if is_hidden(l):# is_locked(l) or
|
||||||
|
continue
|
||||||
|
for f in l.frames:
|
||||||
|
if not f.select:
|
||||||
|
continue
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
self.stroke_list.append(s)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||||
|
for l in self.gp.layers:
|
||||||
|
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||||
|
continue
|
||||||
|
for s in l.current_frame().drawing.strokes:
|
||||||
|
self.stroke_list.append(s)
|
||||||
|
|
||||||
|
if self.fill_only:
|
||||||
|
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
|
||||||
|
|
||||||
|
if not self.stroke_list:
|
||||||
|
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
self.stroke, self.coord = self.filter_stroke(context)
|
||||||
|
if isinstance(self.stroke, str):
|
||||||
|
self.report({'ERROR'}, self.stroke)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
del self.stroke_list
|
||||||
|
|
||||||
|
if self.idx is None:
|
||||||
|
self.report({'WARNING'}, 'No coord found')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position
|
||||||
|
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
|
||||||
|
## directly use world position ?
|
||||||
|
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
|
||||||
|
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||||
|
self.plen = len(self.stroke.points)
|
||||||
|
|
||||||
|
# context.scene.cursor.location = self.coord #Dbg
|
||||||
|
return self.execute(context)
|
||||||
|
# context.window_manager.modal_handler_add(self)
|
||||||
|
# return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
self.ob.active_material_index = self.stroke.material_index
|
||||||
|
# self.report({'INFO'}, f'Mat: {self.ob.data.materials[self.stroke.material_index].name}')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
# def modal(self, context, event):
|
||||||
|
# if event.type == 'MOUSEMOVE':
|
||||||
|
# mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
# delta = mouse - self.init_mouse
|
||||||
|
|
||||||
|
# if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
||||||
|
# print(f'{self.stroke}, points num {len(self.stroke.points)}, material index:{self.stroke.material_index}')
|
||||||
|
# return {'FINISHED'}
|
||||||
|
|
||||||
|
# if event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||||
|
# # for i, p in enumerate(self.stroke.points): # reset position
|
||||||
|
# # self.stroke.points[i].position = self.init_pos[i]
|
||||||
|
# context.area.tag_redraw()
|
||||||
|
# return {'CANCELLED'}
|
||||||
|
|
||||||
|
# return {'RUNNING_MODAL'}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class GP_OT_pick_closest_material(Operator):
|
||||||
|
bl_idname = "gp.pick_closest_material"
|
||||||
|
bl_label = "Get Closest Stroke Material"
|
||||||
|
bl_description = "Pick closest stroke material"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||||
|
|
||||||
|
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||||
|
stroke_filter : bpy.props.EnumProperty(default='FILL',
|
||||||
|
items=(
|
||||||
|
('FILL', 'Fill', 'Target only Fill materials', 0),
|
||||||
|
('STROKE', 'Stroke', 'Target only Stroke materials', 1),
|
||||||
|
('ALL', 'All', 'All material', 2),
|
||||||
|
),
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
def filter_stroke(self, context):
|
||||||
|
# get stroke under mouse using kdtree
|
||||||
|
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||||
|
|
||||||
|
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||||
|
for i, pair in enumerate(point_pair):
|
||||||
|
kd.insert(pair[0], i)
|
||||||
|
kd.balance()
|
||||||
|
|
||||||
|
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
|
||||||
|
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||||
|
|
||||||
|
if not hit:
|
||||||
|
return 'No hit on drawing plane', None
|
||||||
|
|
||||||
|
mouse_3d = hit
|
||||||
|
mouse_local = self.inv_mat @ mouse_3d # local space
|
||||||
|
co, index, _dist = kd.find(mouse_local) # local space
|
||||||
|
# co, index, _dist = kd.find(mouse_3d) # world space
|
||||||
|
# context.scene.cursor.location = co # world space
|
||||||
|
s = point_pair[index][1]
|
||||||
|
|
||||||
|
## find point index in stroke
|
||||||
|
self.idx = None
|
||||||
|
for i, p in enumerate(s.points):
|
||||||
|
if p.position == co:
|
||||||
|
self.idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
del point_pair
|
||||||
|
return s, self.ob.matrix_world @ co
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.t0 = time()
|
||||||
|
self.limit = self.t0 + 0.2 # 200 miliseconds
|
||||||
|
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
self.idx = None
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if time() > self.limit:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised)
|
||||||
|
if time() > self.limit:
|
||||||
|
# dont know if condition is neeed
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return self.execute(context)
|
||||||
|
# return {'FINISHED'}
|
||||||
|
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
# return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# self.prefs = get_addon_prefs()
|
||||||
|
self.ob = context.object
|
||||||
|
gp = self.ob.data
|
||||||
|
|
||||||
|
self.stroke_list = []
|
||||||
|
self.inv_mat = self.ob.matrix_world.inverted()
|
||||||
|
|
||||||
|
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||||
|
for l in gp.layers:
|
||||||
|
if is_hidden(l):# is_locked(l) or
|
||||||
|
continue
|
||||||
|
for f in l.frames:
|
||||||
|
if not f.select:
|
||||||
|
continue
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
self.stroke_list.append(s)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||||
|
for l in gp.layers:
|
||||||
|
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||||
|
continue
|
||||||
|
for s in l.current_frame().drawing.strokes:
|
||||||
|
self.stroke_list.append(s)
|
||||||
|
|
||||||
|
if self.stroke_filter == 'FILL':
|
||||||
|
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
|
||||||
|
elif self.stroke_filter == 'STROKE':
|
||||||
|
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_stroke]
|
||||||
|
# else ALL (no filter)
|
||||||
|
|
||||||
|
if not self.stroke_list:
|
||||||
|
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
stroke, self.coord = self.filter_stroke(context)
|
||||||
|
if isinstance(stroke, str):
|
||||||
|
self.report({'ERROR'}, stroke)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
del self.stroke_list
|
||||||
|
|
||||||
|
if self.idx is None:
|
||||||
|
self.report({'WARNING'}, 'No coord found')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position
|
||||||
|
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated
|
||||||
|
# self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||||
|
# self.plen = len(stroke.points)
|
||||||
|
|
||||||
|
self.ob.active_material_index = stroke.material_index
|
||||||
|
## debug show trigger time
|
||||||
|
# print(f'Trigger time {time() - self.t0:.3f}')
|
||||||
|
self.report({'INFO'}, f'Mat: {self.ob.data.materials[stroke.material_index].name}')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
addon_keymaps = []
|
||||||
|
def register_keymaps():
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||||
|
km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW')
|
||||||
|
kmi = km.keymap_items.new(
|
||||||
|
# name="",
|
||||||
|
idname="gp.pick_closest_material",
|
||||||
|
type="S", # type="LEFTMOUSE",
|
||||||
|
value="PRESS",
|
||||||
|
# key_modifier='S', # S like Sample
|
||||||
|
)
|
||||||
|
kmi.properties.stroke_filter = 'FILL'
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new(
|
||||||
|
# name="",
|
||||||
|
idname="gp.pick_closest_material",
|
||||||
|
type="S", # type="LEFTMOUSE",
|
||||||
|
value="PRESS",
|
||||||
|
alt = True,
|
||||||
|
# key_modifier='S', # S like Sample
|
||||||
|
)
|
||||||
|
kmi.properties.stroke_filter = 'STROKE'
|
||||||
|
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
GP_OT_pick_closest_material,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
unregister_keymaps()
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
201
OP_palettes.py
201
OP_palettes.py
|
@ -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):
|
||||||
|
@ -43,7 +42,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
|
||||||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# Start Clean (delete unuesed sh*t)
|
# Start Clean (delete unuesed sh*t)
|
||||||
|
@ -83,7 +82,7 @@ class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
|
||||||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
filename_ext = '.json'
|
filename_ext = '.json'
|
||||||
|
|
||||||
|
@ -111,7 +110,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
||||||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
|
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||||
|
|
||||||
|
@ -130,6 +129,8 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
||||||
dic = {}
|
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,12 +166,10 @@ 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 != 'GREASEPENCIL':
|
||||||
print(f'{ob.name} not a GP object')
|
print(f'{ob.name} not a GP object')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -225,7 +224,7 @@ class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
|
||||||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
filename_ext = '.blend'
|
filename_ext = '.blend'
|
||||||
|
|
||||||
|
@ -253,7 +252,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
||||||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type == 'GPENCIL'
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ob = context.object
|
ob = context.object
|
||||||
|
@ -261,7 +260,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
||||||
self.report({'ERROR'}, 'No materials to transfer')
|
self.report({'ERROR'}, 'No materials to transfer')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
|
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
|
||||||
|
|
||||||
if not selection:
|
if not selection:
|
||||||
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
|
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
|
||||||
|
@ -282,12 +281,192 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.clean_material_stack"
|
||||||
|
bl_label = "Clean Material Stack"
|
||||||
|
bl_description = "Clean materials duplication in active GP object stack"
|
||||||
|
bl_options = {"REGISTER", "UNDO"} # , "INTERNAL"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use_clean_mats : bpy.props.BoolProperty(name="Remove Duplication",
|
||||||
|
description="All duplicated material (with suffix .001, .002 ...) will be replaced by the material with clean name (if found in scene)" ,
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
skip_different_materials : bpy.props.BoolProperty(name="Skip Different Material",
|
||||||
|
description="Will not touch duplication if color settings are different (and show infos about skipped materials)",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
use_fuses_mats : bpy.props.BoolProperty(name="Fuse Materials Slots",
|
||||||
|
description="Fuse materials slots when multiple uses same materials",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
remove_empty_slots : bpy.props.BoolProperty(name="Remove Empty Slots",
|
||||||
|
description="Remove slots that haven't any material attached ",
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
# skip_binded_empty_slots : bpy.props.BoolProperty(name="Skip Binded Empty slots",
|
||||||
|
# description="Remove only empty slots that haven't any material attached",
|
||||||
|
# default=False)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.ob = context.object
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, 'use_clean_mats')
|
||||||
|
if self.use_clean_mats:
|
||||||
|
box.prop(self, 'skip_different_materials')
|
||||||
|
|
||||||
|
# layout.separator()
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, 'use_fuses_mats')
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, 'remove_empty_slots')
|
||||||
|
# if self.remove_empty_slots:
|
||||||
|
# box.prop(self, 'skip_binded_empty_slots')
|
||||||
|
|
||||||
|
|
||||||
|
def different_gp_mat(self, mata, matb):
|
||||||
|
a = mata.grease_pencil
|
||||||
|
b = matb.grease_pencil
|
||||||
|
if a.color[:] != b.color[:]:
|
||||||
|
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke color is different'
|
||||||
|
if a.fill_color[:] != b.fill_color[:]:
|
||||||
|
return f'! {self.ob.name}: {mata.name} and {matb.name} fill_color color is different'
|
||||||
|
if a.show_stroke != b.show_stroke:
|
||||||
|
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke has different state'
|
||||||
|
if a.show_fill != b.show_fill:
|
||||||
|
return f'! {self.ob.name}: {mata.name} and {matb.name} fill has different state'
|
||||||
|
|
||||||
|
## Clean dups
|
||||||
|
|
||||||
|
def clean_mats_duplication(self, ob):
|
||||||
|
import re
|
||||||
|
diff_ct = 0
|
||||||
|
todel = []
|
||||||
|
if ob.type != 'GREASEPENCIL':
|
||||||
|
return
|
||||||
|
if not hasattr(ob, 'material_slots'):
|
||||||
|
return
|
||||||
|
for i, ms in enumerate(ob.material_slots):
|
||||||
|
mat = ms.material
|
||||||
|
if not mat:
|
||||||
|
continue
|
||||||
|
match = re.search(r'(.*)\.\d{3}$', mat.name)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
basemat = bpy.data.materials.get(match.group(1))
|
||||||
|
if not basemat:
|
||||||
|
continue
|
||||||
|
diff = self.different_gp_mat(mat, basemat)
|
||||||
|
if diff:
|
||||||
|
print(diff)
|
||||||
|
diff_ct += 1
|
||||||
|
if self.skip_different_materials:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mat not in todel:
|
||||||
|
todel.append(mat)
|
||||||
|
ms.material = basemat
|
||||||
|
print(f'{ob.name} : slot {i} >> replaced {mat.name}')
|
||||||
|
mat.use_fake_user = False
|
||||||
|
|
||||||
|
### delete (only when using on all objects loop, else can delete another objects mat...)
|
||||||
|
## for m in reversed(todel):
|
||||||
|
## bpy.data.materials.remove(m)
|
||||||
|
|
||||||
|
if diff_ct:
|
||||||
|
return('INFO', f'{diff_ct} mat skipped >> same name but different color settings!')
|
||||||
|
|
||||||
|
## fuse
|
||||||
|
|
||||||
|
def fuse_object_mats(self, ob):
|
||||||
|
for i in range(len(ob.material_slots))[::-1]:
|
||||||
|
ms = ob.material_slots[i]
|
||||||
|
mat = ms.material
|
||||||
|
# if not mat:
|
||||||
|
# # remove empty slots
|
||||||
|
# if self.remove_empty_slots:
|
||||||
|
# ob.active_material_index = i
|
||||||
|
# bpy.ops.object.material_slot_remove()
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# update mat list
|
||||||
|
mlist = [ms.material for ms in ob.material_slots if ms.material]
|
||||||
|
if mlist.count(mat) > 1:
|
||||||
|
# get first material in list
|
||||||
|
new_mat_id = mlist.index(mat)
|
||||||
|
|
||||||
|
# iterate in all strokes and replace with new_mat_id
|
||||||
|
for l in ob.data.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
if s.material_index == i:
|
||||||
|
s.material_index = new_mat_id
|
||||||
|
|
||||||
|
# delete slot (or add to the remove_slot list
|
||||||
|
ob.active_material_index = i
|
||||||
|
bpy.ops.object.material_slot_remove()
|
||||||
|
|
||||||
|
def delete_empty_material_slots(self, ob):
|
||||||
|
for i in range(len(ob.material_slots))[::-1]:
|
||||||
|
ms = ob.material_slots[i]
|
||||||
|
mat = ms.material
|
||||||
|
if not mat:
|
||||||
|
# is_binded=False
|
||||||
|
# if self.skip_binded_empty_slots:
|
||||||
|
# for l in ob.data.layers:
|
||||||
|
# for f in l.frames:
|
||||||
|
# for s in f.drawing.strokes:
|
||||||
|
# if s.material_index == i:
|
||||||
|
# is_binded = True
|
||||||
|
# break
|
||||||
|
# if is_binded:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
ob.active_material_index = i
|
||||||
|
bpy.ops.object.material_slot_remove()
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
info = None
|
||||||
|
|
||||||
|
if not self.use_clean_mats and not self.use_fuses_mats and not self.remove_empty_slots:
|
||||||
|
self.report({'ERROR'}, 'At least one operation should be selected')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
if self.use_clean_mats:
|
||||||
|
info = self.clean_mats_duplication(ob)
|
||||||
|
if self.use_fuses_mats:
|
||||||
|
self.fuse_object_mats(ob)
|
||||||
|
if self.remove_empty_slots:
|
||||||
|
self.delete_empty_material_slots(ob)
|
||||||
|
|
||||||
|
if info:
|
||||||
|
self.report({info[0]}, info[1])
|
||||||
|
# else:
|
||||||
|
# self.report({'WARNING'}, '')
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
GPTB_OT_load_palette,
|
GPTB_OT_load_palette,
|
||||||
GPTB_OT_save_palette,
|
GPTB_OT_save_palette,
|
||||||
GPTB_OT_load_default_palette,
|
GPTB_OT_load_default_palette,
|
||||||
GPTB_OT_load_blend_palette,
|
GPTB_OT_load_blend_palette,
|
||||||
GPTB_OT_copy_active_to_selected_palette,
|
GPTB_OT_copy_active_to_selected_palette,
|
||||||
|
GPTB_OT_clean_material_stack,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
|
|
@ -0,0 +1,581 @@
|
||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||||
|
from pathlib import Path
|
||||||
|
from . import utils
|
||||||
|
# from . import blendfile
|
||||||
|
|
||||||
|
from bpy.types import (
|
||||||
|
Panel,
|
||||||
|
Operator,
|
||||||
|
PropertyGroup,
|
||||||
|
UIList,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bpy.props import (
|
||||||
|
IntProperty,
|
||||||
|
BoolProperty,
|
||||||
|
StringProperty,
|
||||||
|
FloatProperty,
|
||||||
|
EnumProperty,
|
||||||
|
PointerProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
#--- OPERATORS
|
||||||
|
def print_materials_sources(ob):
|
||||||
|
for m in ob.data.materials:
|
||||||
|
if m.library:
|
||||||
|
print(f'{m.name} - {Path(m.library.filepath).name}')
|
||||||
|
else:
|
||||||
|
print(m.name)
|
||||||
|
|
||||||
|
def replace_mat_slots(src_mat, obj):
|
||||||
|
for ms in obj.material_slots:
|
||||||
|
if ms.material.name == src_mat.name:
|
||||||
|
# Only on different linked, else mat.name differ (.001))
|
||||||
|
ms.material = src_mat
|
||||||
|
|
||||||
|
class GPTB_OT_import_obj_palette(Operator):
|
||||||
|
bl_idname = "gp.import_obj_palette"
|
||||||
|
bl_label = "Import Object Palette"
|
||||||
|
bl_description = "Import object palette from blend"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
## get targets
|
||||||
|
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
|
||||||
|
if not selection:
|
||||||
|
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
prefs = utils.get_addon_prefs()
|
||||||
|
exclusions = [name.strip() for name in prefs.mat_link_exclude.split(',')] if prefs.mat_link_exclude else []
|
||||||
|
|
||||||
|
# Avoid looping on linked duplicate
|
||||||
|
objs = []
|
||||||
|
datas = []
|
||||||
|
for o in selection:
|
||||||
|
if o.data in datas:
|
||||||
|
continue
|
||||||
|
objs.append(o)
|
||||||
|
datas.append(o.data)
|
||||||
|
del datas # datas.clear()
|
||||||
|
|
||||||
|
pl_prop = context.scene.bl_palettes_props
|
||||||
|
blend_path = pl_prop.blends[pl_prop.bl_idx].blend_path
|
||||||
|
target_objs = [pl_prop.objects[pl_prop.ob_idx].name]
|
||||||
|
# Future improvement
|
||||||
|
# target_objs = [o.name for o in pl_prop.objects if o.select]
|
||||||
|
if not target_objs:
|
||||||
|
self.report({'ERROR'}, 'Need at least one palette source selected')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
mode = pl_prop.import_type
|
||||||
|
|
||||||
|
if mode == 'LINK' and not bpy.data.is_saved: # autorise for absolute path
|
||||||
|
self.report({'ERROR'}, 'Blend file must be saved to use link mode')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
if mode != 'LINK':
|
||||||
|
self.report({'ERROR'}, 'Not supported yet, use link')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if not Path(blend_path).exists():
|
||||||
|
utils.show_message_box([['gp.palettes_reload_blends', 'Invalid blend path! Click here to refresh source blends', 'FILE_REFRESH']], 'Invalid Palette', 'ERROR')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# get relative path
|
||||||
|
blend_path = bpy.path.relpath(blend_path)
|
||||||
|
|
||||||
|
# TODO append object to list all material that belongs to it...
|
||||||
|
|
||||||
|
linked_objs = utils.link_objects_in_blend(blend_path, target_objs, link=True)
|
||||||
|
|
||||||
|
if not linked_objs:
|
||||||
|
self.report({'ERROR'}, f'Could not link/append obj from {blend_path}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
|
||||||
|
if linked_objs[i].type != 'GREASEPENCIL':
|
||||||
|
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
|
||||||
|
bpy.data.objects.remove(linked_objs.pop(i))
|
||||||
|
|
||||||
|
if not linked_objs:
|
||||||
|
self.report({'ERROR'}, f'Linked object was not a Grease Pencil')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|
||||||
|
print('blend_path: ', blend_path)
|
||||||
|
# if materials have been renamed, there must be already be appended / linked
|
||||||
|
|
||||||
|
# to_clear = []
|
||||||
|
ct = 0
|
||||||
|
for src_ob in linked_objs:
|
||||||
|
ct += len(src_ob.data.materials)
|
||||||
|
|
||||||
|
if mode == 'LINK': # link new mats and update already linked ones
|
||||||
|
## link mats
|
||||||
|
for ob in objs:
|
||||||
|
for src_ob in linked_objs:
|
||||||
|
for src_mat in src_ob.data.materials:
|
||||||
|
## filter mat
|
||||||
|
if src_mat.name in exclusions:
|
||||||
|
continue
|
||||||
|
mat = ob.data.materials.get(src_mat.name)
|
||||||
|
|
||||||
|
if mat and mat.library == src_mat.library:
|
||||||
|
# print('already exists')
|
||||||
|
continue # same material, skip
|
||||||
|
|
||||||
|
elif mat:
|
||||||
|
# print('already but not same lib')
|
||||||
|
## same material but not from same lib
|
||||||
|
## remap_user will replace this mat in all objects blend...
|
||||||
|
mat.user_remap(src_mat)
|
||||||
|
## (we might want to keep links in other objects untouched ?)
|
||||||
|
## else use a basic material slot swap (loop, can be added on multiple slots)
|
||||||
|
# replace_mat_slots(ob, src_mat)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# print('Not in dest')
|
||||||
|
## material not in dest, append
|
||||||
|
ob.data.materials.append(src_mat)
|
||||||
|
|
||||||
|
elif mode == 'APPEND':
|
||||||
|
## append, overwrite all already existing materials with new ones
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ct = 0
|
||||||
|
# for o in selection:
|
||||||
|
# for mat in ob.data.materials:
|
||||||
|
# if mat in o.data.materials[:]:
|
||||||
|
# continue
|
||||||
|
# o.data.materials.append(mat)
|
||||||
|
# ct += 1
|
||||||
|
|
||||||
|
elif mode == 'APPEND_REUSE':
|
||||||
|
## append, Skip existing material
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ct:
|
||||||
|
self.report({'INFO'}, f'{ct} Materials appended')
|
||||||
|
# else:
|
||||||
|
# self.report({'WARNING'}, 'All materials are already in other selected object')
|
||||||
|
|
||||||
|
# unlink objects and their gp data
|
||||||
|
for src_ob in linked_objs:
|
||||||
|
bpy.data.grease_pencils.remove(src_ob.data)
|
||||||
|
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_palette_fuzzy_search_obj(Operator):
|
||||||
|
bl_idname = "gptb.palette_fuzzy_search_obj"
|
||||||
|
bl_label = "Palette Fuzzy Match"
|
||||||
|
bl_description = "Try to find a palette with name closest to active object"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if not context.object:
|
||||||
|
self.report({'ERROR'}, 'No active object to search name from')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bl_props = context.scene.bl_palettes_props
|
||||||
|
|
||||||
|
final_ratio = 0
|
||||||
|
new_idx = None
|
||||||
|
for i, o in enumerate(bl_props.objects):
|
||||||
|
ratio = utils.fuzzy_match_ratio(context.object.name, o.name, case_sensitive=False)
|
||||||
|
if ratio > final_ratio:
|
||||||
|
new_idx = i
|
||||||
|
final_ratio = ratio
|
||||||
|
|
||||||
|
limit = 0.3
|
||||||
|
if final_ratio < limit:
|
||||||
|
self.report({'ERROR'}, f'Could not find a name matching at least {limit*100:.0f}% "{context.object.name}"')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
if new_idx is None:
|
||||||
|
self.report({'ERROR'}, f'Could not find match')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bl_props.ob_idx = new_idx
|
||||||
|
self.report({'INFO'}, f'Select {bl_props.objects[bl_props.ob_idx].name} (match at {final_ratio*100:.1f}% with "{context.object.name}")')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
## Unused for now, all libs are linked to one library data. need to replace material links one by one.
|
||||||
|
class GPTB_OT_palette_version_update(Operator):
|
||||||
|
bl_idname = "gptb.palette_version_update"
|
||||||
|
bl_label = "Update Palette Version"
|
||||||
|
bl_description = "Update linked material to selected palette version if curent link has same basename"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
mat_scope : EnumProperty(
|
||||||
|
name='Targeted Materials',
|
||||||
|
items=(('ALL', "All Materials", "Update all linked material in file to next version"),
|
||||||
|
('SELECTED', "Selected Objects", "Update all linked material on selected gp objects"),
|
||||||
|
),
|
||||||
|
default='ALL',
|
||||||
|
description='Choose material targeted for library update'
|
||||||
|
)
|
||||||
|
|
||||||
|
mat_type : EnumProperty(
|
||||||
|
name='Materials Type',
|
||||||
|
items=(('ALL', "All Materials", "Update both gp and obj materials"),
|
||||||
|
('GP', "Gpencil Materials", "update only grease pencil materials"),
|
||||||
|
('OBJ', "Non-Gpencil Materials", "update only non-gpencil objects materials"),
|
||||||
|
),
|
||||||
|
default='GP',
|
||||||
|
description='Filter material type for library update'
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.bl_props = context.scene.bl_palettes_props
|
||||||
|
if not self.bl_props.blends or not self.bl_props.blends[0].blend_path:
|
||||||
|
self.report({'ERROR'}, 'No blend selected')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.label(text=f'Update links path to palette: {self.bl_props.blends[self.bl_props.bl_idx].blend_name}', icon='LINK_BLEND')
|
||||||
|
self.bl_props
|
||||||
|
layout.prop(self, 'mat_scope')
|
||||||
|
layout.prop(self, 'mat_type')
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text='Does not check if material exists in target blend', icon='INFO')
|
||||||
|
col.label(text='Just change source filepath if different version of same source name is found')
|
||||||
|
# col.label(text='version of same source name is found')
|
||||||
|
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if self.mat_scope == 'SELECTED' and not context.selected_objects:
|
||||||
|
self.report({'ERROR'}, 'No selected objects')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bl_props = context.scene.bl_palettes_props
|
||||||
|
bl = bl_props.blends[bl_props.bl_idx]
|
||||||
|
bl_name, bl_path = bl.blend_name, bl.blend_path
|
||||||
|
|
||||||
|
if not Path(bl_path).exists():
|
||||||
|
self.report({'ERROR'}, f'Current selected blend source seem unreachable, try to refresh\ninvalid path: {bl_path}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
reversion = re.compile(r'\d{2,4}$') # version padding from 2 to 4
|
||||||
|
bl_relpath = bpy.path.relpath(bl_path)
|
||||||
|
|
||||||
|
if self.mat_scope == 'SELECTED':
|
||||||
|
pool = []
|
||||||
|
for o in context.selected_objects:
|
||||||
|
for m in o.data.materials:
|
||||||
|
pool.append(m)
|
||||||
|
|
||||||
|
elif self.mat_scope == 'ALL':
|
||||||
|
pool = [m for m in bpy.data.materials]
|
||||||
|
|
||||||
|
ct = 0
|
||||||
|
for m in pool:
|
||||||
|
if not m.library:
|
||||||
|
continue
|
||||||
|
if self.mat_type == 'GP' and not m.is_grease_pencil:
|
||||||
|
continue
|
||||||
|
if self.mat_type == 'OBJ' and m.is_grease_pencil:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur_fp = m.library.filepath
|
||||||
|
if not cur_fp:
|
||||||
|
print(f'! {m.name} has an empty library filepath !')
|
||||||
|
continue
|
||||||
|
|
||||||
|
p_cur_fp = Path(cur_fp)
|
||||||
|
if p_cur_fp.stem == bl_name:
|
||||||
|
continue # already good
|
||||||
|
|
||||||
|
if reversion.sub('', p_cur_fp.stem) != reversion.sub('', bl_name):
|
||||||
|
continue # not same stem base
|
||||||
|
|
||||||
|
# Same stem without version, can update to this one
|
||||||
|
print(f'{m.name}: {p_cur_fp} >> {bl_relpath}')
|
||||||
|
ct += 1
|
||||||
|
m.library.filepath = bl_relpath
|
||||||
|
|
||||||
|
if ct:
|
||||||
|
self.report({'INFO'}, f'{ct} material link path updated')
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, 'No material path updated')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
#--- UI LIST
|
||||||
|
|
||||||
|
class GPTB_UL_blend_list(UIList):
|
||||||
|
# order_by_distance : BoolProperty(default=True)
|
||||||
|
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
layout.label(text=item.blend_name)
|
||||||
|
|
||||||
|
def draw_filter(self, context, layout):
|
||||||
|
row = layout.row()
|
||||||
|
subrow = row.row(align=True)
|
||||||
|
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use ‘*’ as wildcard)
|
||||||
|
|
||||||
|
# reverse order
|
||||||
|
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
|
||||||
|
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
|
||||||
|
|
||||||
|
def filter_items(self, context, data, propname):
|
||||||
|
# example : https://docs.blender.org/api/blender_python_api_current/bpy.types.UIList.html
|
||||||
|
# This function gets the collection property (as the usual tuple (data, propname)), and must return two lists:
|
||||||
|
# * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the
|
||||||
|
# matching item as filtered (i.e. to be shown), and 31 other bits are free for custom needs. Here we use the
|
||||||
|
# * The second one is for reordering, it must return a list containing the new indices of the items (which
|
||||||
|
# gives us a mapping org_idx -> new_idx).
|
||||||
|
# Please note that the default UI_UL_list defines helper functions for common tasks (see its doc for more info).
|
||||||
|
# If you do not make filtering and/or ordering, return empty list(s) (this will be more efficient than
|
||||||
|
# returning full lists doing nothing!).
|
||||||
|
|
||||||
|
collec = getattr(data, propname)
|
||||||
|
helper_funcs = bpy.types.UI_UL_list
|
||||||
|
|
||||||
|
# Default return values.
|
||||||
|
flt_flags = []
|
||||||
|
flt_neworder = []
|
||||||
|
|
||||||
|
|
||||||
|
# Filtering by name #not working damn !
|
||||||
|
if self.filter_name:
|
||||||
|
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
|
||||||
|
reverse=self.use_filter_sort_reverse)#self.use_filter_name_reverse)
|
||||||
|
|
||||||
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
class GPTB_UL_object_list(UIList):
|
||||||
|
# order_by_distance : BoolProperty(default=True)
|
||||||
|
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
self.use_filter_show = True # force open the search feature
|
||||||
|
layout.label(text=item.name)
|
||||||
|
|
||||||
|
def draw_filter(self, context, layout):
|
||||||
|
row = layout.row()
|
||||||
|
subrow = row.row(align=True)
|
||||||
|
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use ‘*’ as wildcard)
|
||||||
|
# reverse order
|
||||||
|
subrow.operator('gptb.palette_fuzzy_search_obj', text='', icon='ZOOM_SELECTED') # built-in reverse
|
||||||
|
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
|
||||||
|
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
|
||||||
|
|
||||||
|
def filter_items(self, context, data, propname):
|
||||||
|
collec = getattr(data, propname)
|
||||||
|
helper_funcs = bpy.types.UI_UL_list
|
||||||
|
# Default return values.
|
||||||
|
flt_flags = []
|
||||||
|
flt_neworder = []
|
||||||
|
if self.filter_name:
|
||||||
|
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
|
||||||
|
reverse=self.use_filter_sort_reverse)
|
||||||
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
|
||||||
|
def reload_blends(self, context):
|
||||||
|
scn = context.scene
|
||||||
|
pl_prop = scn.bl_palettes_props
|
||||||
|
uilist = scn.bl_palettes_props.blends
|
||||||
|
uilist.clear()
|
||||||
|
pl_prop['bl_idx'] = 0
|
||||||
|
|
||||||
|
prefs = utils.get_addon_prefs()
|
||||||
|
|
||||||
|
if pl_prop.use_project_path:
|
||||||
|
palette_fp = prefs.palette_path
|
||||||
|
else:
|
||||||
|
palette_fp = pl_prop.custom_dir
|
||||||
|
|
||||||
|
if not palette_fp: # singular
|
||||||
|
item = uilist.add()
|
||||||
|
item.blend_name = 'No Palette Path Specified'
|
||||||
|
reload_objects(self, context)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
palettes_dir = Path(os.path.abspath(bpy.path.abspath(palette_fp)))
|
||||||
|
if not palettes_dir.exists():
|
||||||
|
item = uilist.add()
|
||||||
|
item.blend_name = 'Palette Path not found'
|
||||||
|
reload_objects(self, context)
|
||||||
|
return
|
||||||
|
|
||||||
|
# list blends
|
||||||
|
pattern = r'[vV](\d{2,3})' # rightest = r'[vV](\d+)(?!.*[vV]\d)'
|
||||||
|
blends = [] # recursive
|
||||||
|
for root, _dirs, files in os.walk(palettes_dir):
|
||||||
|
for f in files:
|
||||||
|
fp = Path(root) / f
|
||||||
|
if not f.endswith('.blend'):
|
||||||
|
continue
|
||||||
|
if not re.search(pattern, f):
|
||||||
|
continue
|
||||||
|
if not fp.is_file():
|
||||||
|
continue
|
||||||
|
blends.append((str(fp), fp.stem, ""))
|
||||||
|
|
||||||
|
## only in palette folder.
|
||||||
|
# blends = [(o.path, Path(o).stem, "") for o in os.scandir(palettes_dir)
|
||||||
|
# if o.is_file()
|
||||||
|
# and o.name.endswith('.blend')
|
||||||
|
# and re.search(pattern, o.name)]
|
||||||
|
|
||||||
|
# blends.sort(key=lambda x: x[1], reverse=False) # sort alphabetically
|
||||||
|
blends.sort(key=lambda x: int(re.search(pattern, x[1]).group(1)), reverse=False) # sort by version
|
||||||
|
# print('blends found', len(blends))
|
||||||
|
|
||||||
|
for bl in blends: # populate list
|
||||||
|
item = uilist.add()
|
||||||
|
|
||||||
|
scn.bl_palettes_props['bl_idx'] = len(uilist) - 1 # don't trigger updates
|
||||||
|
item.blend_path = bl[0]
|
||||||
|
item.blend_name = bl[1]
|
||||||
|
|
||||||
|
scn.bl_palettes_props.bl_idx = len(uilist) - 1 # trigger update ()
|
||||||
|
# reload_objects(self, context) # triggered by above assignation
|
||||||
|
|
||||||
|
# return len(blends) # return value must be None
|
||||||
|
|
||||||
|
class GPTB_OT_palettes_reload_blends(Operator):
|
||||||
|
bl_idname = "gp.palettes_reload_blends"
|
||||||
|
bl_label = "Reload Palette Blends"
|
||||||
|
bl_description = "Reload the blends in UI list of palettes linker"
|
||||||
|
bl_options = {"REGISTER"} # , "INTERNAL"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
reload_blends(self, context)
|
||||||
|
# ret = reload_blends(self, context)
|
||||||
|
# if ret is None:
|
||||||
|
# self.report({'ERROR'}, 'No blend scanned, check palette path')
|
||||||
|
# else:
|
||||||
|
# self.report({'INFO'}, f'{ret} blends found')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def reload_objects(self, context):
|
||||||
|
scn = context.scene
|
||||||
|
prefs = utils.get_addon_prefs()
|
||||||
|
pal_prop = scn.bl_palettes_props
|
||||||
|
blend_uil = pal_prop.blends
|
||||||
|
obj_uil = pal_prop.objects
|
||||||
|
obj_uil.clear()
|
||||||
|
pal_prop['ob_idx'] = 0
|
||||||
|
|
||||||
|
file_libs = [l.filepath for l in bpy.data.libraries if l.filepath]
|
||||||
|
|
||||||
|
if not len(blend_uil) or (len(blend_uil) == 1 and not bool(blend_uil[0].blend_path)):
|
||||||
|
item = obj_uil.add()
|
||||||
|
item.name = 'No blend to list object'
|
||||||
|
return
|
||||||
|
|
||||||
|
if not blend_uil[pal_prop.bl_idx].blend_path:
|
||||||
|
item = obj_uil.add()
|
||||||
|
item.name = 'Selected blend has no path'
|
||||||
|
return
|
||||||
|
|
||||||
|
path_to_blend = Path(blend_uil[pal_prop.bl_idx].blend_path)
|
||||||
|
|
||||||
|
## get list of string of all object except camera
|
||||||
|
ob_list = utils.check_objects_in_blend(str(path_to_blend), avoid_camera=True)
|
||||||
|
|
||||||
|
ob_list.sort(reverse=False) # filter object by name
|
||||||
|
|
||||||
|
for ob_name in ob_list: # populate list
|
||||||
|
item = obj_uil.add()
|
||||||
|
item.name = ob_name
|
||||||
|
# print('path_to_blend: ', path_to_blend)
|
||||||
|
item.path = str(path_to_blend / 'Object' / ob_name)
|
||||||
|
|
||||||
|
pal_prop.ob_idx = len(obj_uil) - 1
|
||||||
|
|
||||||
|
## those temp libraries are not saved (auto-cleared)
|
||||||
|
## But best to keep library list tidy while file is opened
|
||||||
|
for lib in reversed(bpy.data.libraries):
|
||||||
|
if lib.filepath and not lib.users_id:
|
||||||
|
if lib.filepath not in file_libs:
|
||||||
|
bpy.data.libraries.remove(lib)
|
||||||
|
|
||||||
|
# return len(ob_list) # must return None if used in update
|
||||||
|
del ob_list
|
||||||
|
|
||||||
|
|
||||||
|
#--- PROPERTIES
|
||||||
|
|
||||||
|
class GPTB_PG_blend_prop(PropertyGroup):
|
||||||
|
blend_name : StringProperty() # stem of the path
|
||||||
|
blend_path : StringProperty() # full path
|
||||||
|
|
||||||
|
class GPTB_PG_object_prop(PropertyGroup):
|
||||||
|
name : StringProperty() # stem of the path
|
||||||
|
path : StringProperty() # Object / Material ?
|
||||||
|
## select feature to get multiple at once
|
||||||
|
# select : BoolProperty(default=False) # Object / Material ?
|
||||||
|
|
||||||
|
class GPTB_PG_palette_settings(PropertyGroup):
|
||||||
|
bl_idx : IntProperty(update=reload_objects) # update_on_index_change to reload object
|
||||||
|
blends : bpy.props.CollectionProperty(type=GPTB_PG_blend_prop)
|
||||||
|
|
||||||
|
ob_idx : IntProperty()
|
||||||
|
objects : bpy.props.CollectionProperty(type=GPTB_PG_object_prop)
|
||||||
|
|
||||||
|
use_project_path : BoolProperty(name='Use Project Palettes',
|
||||||
|
default=True, description='Use palettes directory specified in gp toolbox addon preferences',
|
||||||
|
update=reload_blends)
|
||||||
|
|
||||||
|
show_path : BoolProperty(name='Show path',
|
||||||
|
default=True, description='Show Palette directoty filepath')
|
||||||
|
|
||||||
|
custom_dir : StringProperty(name='Custom Palettes Directory', subtype='DIR_PATH',
|
||||||
|
description='Use choosen directory to load blend palettes',
|
||||||
|
update=reload_blends)
|
||||||
|
|
||||||
|
import_type : EnumProperty(
|
||||||
|
name="Import Type", description="Choose inmport type: link, append, append reuse (keep existing materials)",
|
||||||
|
default='LINK', options={'ANIMATABLE'}, update=None, get=None, set=None,
|
||||||
|
items=(
|
||||||
|
('LINK', 'Link', 'Link materials to selected object', 0),
|
||||||
|
('APPEND', 'Append', 'Append materials to selected objects', 1),
|
||||||
|
('APPEND_REUSE', 'Append (Reuse)', 'Append materials to selected objects\nkeep those already there', 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# fav_blend: StringProperty() ## mark a blend as prefered ? (need to be stored in prefereneces to restore in other blend...)
|
||||||
|
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
# blend list
|
||||||
|
GPTB_PG_blend_prop,
|
||||||
|
GPTB_UL_blend_list,
|
||||||
|
GPTB_OT_palettes_reload_blends,
|
||||||
|
|
||||||
|
# object in blend list
|
||||||
|
GPTB_OT_palette_fuzzy_search_obj,
|
||||||
|
GPTB_PG_object_prop,
|
||||||
|
GPTB_UL_object_list,
|
||||||
|
|
||||||
|
# prop containing two above
|
||||||
|
GPTB_PG_palette_settings,
|
||||||
|
|
||||||
|
GPTB_OT_import_obj_palette,
|
||||||
|
# GPTB_OT_palette_version_update,
|
||||||
|
|
||||||
|
# TEST_OT_import_obj_palette_test,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
bpy.types.Scene.bl_palettes_props = bpy.props.PointerProperty(type=GPTB_PG_palette_settings)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
del bpy.types.Scene.bl_palettes_props
|
|
@ -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,26 +82,66 @@ 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():
|
||||||
### can add propeties for personalisation as toolsetting props
|
### can add propeties for personalisation as toolsetting props
|
||||||
|
|
||||||
rd.resolution_percentage = res_factor
|
rd.resolution_percentage = res_factor
|
||||||
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
|
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage
|
||||||
rd.resolution_x = rd.resolution_x + 1
|
rd.resolution_x = rd.resolution_x + 1
|
||||||
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
|
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage
|
||||||
rd.resolution_y = rd.resolution_y + 1
|
rd.resolution_y = rd.resolution_y + 1
|
||||||
|
|
||||||
rd.image_settings.file_format = 'FFMPEG'
|
rd.image_settings.file_format = 'FFMPEG'
|
||||||
ff.format = 'MPEG4'
|
ff.format = 'MPEG4'
|
||||||
ff.codec = 'H264'
|
ff.codec = 'H264'
|
||||||
ff.constant_rate_factor = 'HIGH'# MEDIUM
|
ff.constant_rate_factor = 'HIGH' # MEDIUM
|
||||||
ff.ffmpeg_preset = 'REALTIME'
|
ff.ffmpeg_preset = 'REALTIME'
|
||||||
ff.gopsize = 10
|
ff.gopsize = 10
|
||||||
ff.audio_codec = 'AAC'
|
ff.audio_codec = 'AAC'
|
||||||
|
@ -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
|
||||||
|
|
|
@ -21,15 +21,20 @@ exclude = (
|
||||||
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
|
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_file(filepath) :
|
def delete_file(filepath):
|
||||||
try:
|
fp = Path(filepath)
|
||||||
if os.path.isfile(filepath) :
|
if fp.exists() and fp.is_file():
|
||||||
print('removing', filepath)
|
try:
|
||||||
os.remove(filepath)
|
print('removing', fp)
|
||||||
|
fp.unlink(missing_ok=False)
|
||||||
|
# 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
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'Impossible to remove (file not found error): {fp}')
|
||||||
|
return False
|
||||||
|
|
||||||
# render function
|
# render function
|
||||||
def render_function(cmd, total_frame, scene) :
|
def render_function(cmd, total_frame, scene) :
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
'''
|
'''
|
||||||
|
@ -76,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
||||||
# namespace_order
|
# namespace_order
|
||||||
namespaces=[]
|
namespaces=[]
|
||||||
for l in gpl:
|
for l in gpl:
|
||||||
ns= l.info.lower().split(separator, 1)[0]
|
ns= l.name.lower().split(separator, 1)[0]
|
||||||
if ns not in namespaces:
|
if ns not in namespaces:
|
||||||
namespaces.append(ns)
|
namespaces.append(ns)
|
||||||
|
|
||||||
|
@ -90,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
||||||
### step from 0.1 to 0.9
|
### step from 0.1 to 0.9
|
||||||
|
|
||||||
for i, l in enumerate(gpl):
|
for i, l in enumerate(gpl):
|
||||||
if l.info.lower() not in ('background',):
|
if l.name.lower() not in ('background',):
|
||||||
print()
|
print()
|
||||||
print('>', l.info)
|
print('>', l.name)
|
||||||
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
|
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
|
||||||
print("namespace", ns)#Dbg
|
print("namespace", ns)#Dbg
|
||||||
|
|
||||||
if context.scene.gptoolprops.autotint_namespace:
|
if context.scene.gptoolprops.autotint_namespace:
|
||||||
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
|
h = get_hue_by_name(ns, hue_offset)#l.name == individuels
|
||||||
|
|
||||||
else:
|
else:
|
||||||
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
|
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
|
||||||
|
@ -127,3 +125,10 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
||||||
def invoke(self, context, event):
|
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)
|
|
@ -0,0 +1,437 @@
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
from math import pi
|
||||||
|
from time import time
|
||||||
|
from mathutils.geometry import intersect_line_plane
|
||||||
|
from . import utils
|
||||||
|
from .utils import is_hidden, is_locked
|
||||||
|
|
||||||
|
def get_scale_matrix(scale):
|
||||||
|
# recreate a neutral mat scale
|
||||||
|
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
||||||
|
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
||||||
|
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
||||||
|
matscale = matscale_x @ matscale_y @ matscale_z
|
||||||
|
return matscale
|
||||||
|
|
||||||
|
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||||
|
'''Reproject - ops method
|
||||||
|
:all_stroke: affect hidden, locked layers
|
||||||
|
'''
|
||||||
|
|
||||||
|
if restore_frame:
|
||||||
|
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.drawing.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 layer in obj.data.layers:
|
||||||
|
if not all_strokes:
|
||||||
|
if not layer.select:
|
||||||
|
continue
|
||||||
|
if is_hidden(layer) or is_locked(layer):
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame = next((f for f in layer.frames if f.frame_number == i), None)
|
||||||
|
if frame is None:
|
||||||
|
print(layer.name, 'Not found')
|
||||||
|
# FIXME: some strokes are ignored
|
||||||
|
# print(frame'skip {layer.name}, no frame at {i}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
for s in frame.drawing.strokes:
|
||||||
|
# print(layer.name, s.material_index)
|
||||||
|
|
||||||
|
## Batch matrix apply (Here is slower than list comprehension).
|
||||||
|
# nb_points = len(s.points)
|
||||||
|
# coords = np.empty(nb_points * 3, dtype='float64')
|
||||||
|
# s.points.foreach_get('co', coords)
|
||||||
|
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
|
||||||
|
|
||||||
|
## list comprehension method
|
||||||
|
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
|
||||||
|
|
||||||
|
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
|
||||||
|
|
||||||
|
# Basic method (Slower than foreach_set and compatible with GPv3)
|
||||||
|
## TODO: use low level api with curve offsets...
|
||||||
|
for pt_index, point in enumerate(s.points):
|
||||||
|
point.position = matrix_inv @ new_world_co_3d[pt_index]
|
||||||
|
|
||||||
|
## GPv2: ravel and use foreach_set
|
||||||
|
## Ravel new coordinate on the fly
|
||||||
|
## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
|
||||||
|
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
|
||||||
|
# s.points.foreach_set('co', new_local_coords)
|
||||||
|
|
||||||
|
if restore_frame:
|
||||||
|
bpy.context.scene.frame_current = oframe
|
||||||
|
|
||||||
|
## Update the layer and redraw all viewports
|
||||||
|
obj.data.layers.update()
|
||||||
|
utils.refresh_areas()
|
||||||
|
|
||||||
|
def align_global(reproject=True, ref=None, all_strokes=True):
|
||||||
|
|
||||||
|
if not ref:
|
||||||
|
ref = bpy.context.scene.camera
|
||||||
|
|
||||||
|
o = bpy.context.object
|
||||||
|
# if o.matrix_basis != o.matrix_world and not o.parent:
|
||||||
|
|
||||||
|
ref = bpy.context.scene.camera
|
||||||
|
ref_mat = ref.matrix_world
|
||||||
|
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||||
|
|
||||||
|
if o.parent:
|
||||||
|
mat = o.matrix_world
|
||||||
|
else:
|
||||||
|
mat = o.matrix_basis
|
||||||
|
|
||||||
|
o_loc, o_rot, o_scale = mat.decompose()
|
||||||
|
|
||||||
|
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||||
|
|
||||||
|
loc_mat = Matrix.Translation(o_loc)
|
||||||
|
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||||
|
scale_mat = get_scale_matrix(o_scale)
|
||||||
|
|
||||||
|
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||||
|
|
||||||
|
# world_coords = []
|
||||||
|
for l in o.data.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
## foreach
|
||||||
|
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||||
|
|
||||||
|
## GPv2
|
||||||
|
# s.points.foreach_set('co', [co for v in coords for co in v])
|
||||||
|
# # s.points.update() # seem to works # but adding/deleting a point is "safer"
|
||||||
|
# ## force update
|
||||||
|
# s.points.add(1)
|
||||||
|
# s.points.pop()
|
||||||
|
|
||||||
|
for p in s.points:
|
||||||
|
## GOOD :
|
||||||
|
# world_co = mat @ p.position
|
||||||
|
# p.position = new_mat.inverted() @ world_co
|
||||||
|
|
||||||
|
## GOOD :
|
||||||
|
p.position = p.position @ mat.inverted() @ new_mat
|
||||||
|
|
||||||
|
if o.parent:
|
||||||
|
o.matrix_world = new_mat
|
||||||
|
else:
|
||||||
|
o.matrix_basis = new_mat
|
||||||
|
|
||||||
|
if reproject:
|
||||||
|
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
|
||||||
|
|
||||||
|
|
||||||
|
def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
||||||
|
|
||||||
|
print('aligning all frames...')
|
||||||
|
|
||||||
|
o = bpy.context.object
|
||||||
|
if not ref:
|
||||||
|
ref = bpy.context.scene.camera
|
||||||
|
|
||||||
|
# get all rot
|
||||||
|
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
||||||
|
|
||||||
|
## double list keys
|
||||||
|
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||||
|
|
||||||
|
## normal iter
|
||||||
|
# for fcu in o.animation_data.action.fcurves:
|
||||||
|
# if fcu.data_path != chanel :
|
||||||
|
# continue
|
||||||
|
# for k in fcu.keyframe_points():
|
||||||
|
# rot_keys.append(k.co.x)
|
||||||
|
|
||||||
|
rot_keys = list(set(rot_keys))
|
||||||
|
|
||||||
|
# TODO # TOTHINK
|
||||||
|
# for now the rotation of the object is adjusted at every frame....
|
||||||
|
# might be better to check camera rotation of the current frame only (stored as copy).
|
||||||
|
# else the object rotate following the cameraview vector (not constant)...
|
||||||
|
|
||||||
|
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||||
|
|
||||||
|
for l in o.data.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
# set the frame to dedicated
|
||||||
|
bpy.context.scene.frame_set(f.frame_number)
|
||||||
|
|
||||||
|
ref_mat = ref.matrix_world
|
||||||
|
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||||
|
|
||||||
|
if o.parent:
|
||||||
|
mat = o.matrix_world
|
||||||
|
else:
|
||||||
|
mat = o.matrix_basis
|
||||||
|
|
||||||
|
o_loc, o_rot, o_scale = mat.decompose()
|
||||||
|
loc_mat = Matrix.Translation(o_loc)
|
||||||
|
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||||
|
scale_mat = get_scale_matrix(o_scale)
|
||||||
|
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||||
|
|
||||||
|
for s in f.drawing.strokes:
|
||||||
|
## foreach
|
||||||
|
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||||
|
# print('coords: ', coords)
|
||||||
|
# print([co for v in coords for co in v])
|
||||||
|
s.points.foreach_set('co', [co for v in coords for co in v])
|
||||||
|
# s.points.update() # seem to works
|
||||||
|
## force update
|
||||||
|
s.points.add(1)
|
||||||
|
s.points.pop()
|
||||||
|
|
||||||
|
for fnum in rot_keys:
|
||||||
|
bpy.context.scene.frame_set(fnum)
|
||||||
|
#/update calculation block
|
||||||
|
ref_mat = ref.matrix_world
|
||||||
|
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||||
|
|
||||||
|
if o.parent:
|
||||||
|
mat = o.matrix_world
|
||||||
|
else:
|
||||||
|
mat = o.matrix_basis
|
||||||
|
|
||||||
|
o_loc, o_rot, o_scale = mat.decompose()
|
||||||
|
loc_mat = Matrix.Translation(o_loc)
|
||||||
|
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||||
|
scale_mat = get_scale_matrix(o_scale)
|
||||||
|
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||||
|
# update calculation block/
|
||||||
|
|
||||||
|
if o.parent:
|
||||||
|
o.matrix_world = new_mat
|
||||||
|
else:
|
||||||
|
o.matrix_basis = new_mat
|
||||||
|
|
||||||
|
o.keyframe_insert(chanel, index=-1, frame=bpy.context.scene.frame_current, options={'INSERTKEY_AVAILABLE'})
|
||||||
|
|
||||||
|
|
||||||
|
if reproject:
|
||||||
|
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_realign(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.realign"
|
||||||
|
bl_label = "Realign GP"
|
||||||
|
bl_description = "Realign the grease pencil front axis with active camera"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
reproject : bpy.props.BoolProperty(
|
||||||
|
name='Reproject', default=True,
|
||||||
|
description='Reproject stroke on the new alignment')
|
||||||
|
|
||||||
|
all_strokes : bpy.props.BoolProperty(
|
||||||
|
name='All Strokes', default=True,
|
||||||
|
description='Hided and locked layer will also be reprojected')
|
||||||
|
|
||||||
|
set_draw_axis : bpy.props.BoolProperty(
|
||||||
|
name='Set draw axis to Front', default=True,
|
||||||
|
description='Set the gpencil draw plane axis to Front')
|
||||||
|
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||||
|
self.report({'ERROR'}, 'Does not work in Multiframe mode')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
self.alert = ''
|
||||||
|
o = context.object
|
||||||
|
if o.animation_data and o.animation_data.action:
|
||||||
|
act = o.animation_data.action
|
||||||
|
for chan in ('rotation_euler', 'rotation_quaternion'):
|
||||||
|
if act.fcurves.find(chan):
|
||||||
|
self.alert = 'Animated Rotation (CONSTANT interpolation)'
|
||||||
|
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
|
||||||
|
if interpos:
|
||||||
|
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
|
||||||
|
break
|
||||||
|
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.label(text='Realign the GP object : Front axis facing active camera')
|
||||||
|
if self.alert:
|
||||||
|
layout.label(text=self.alert, icon='ERROR')
|
||||||
|
layout.label(text='(rotations key will be overwritten to face camera)')
|
||||||
|
|
||||||
|
# layout.separator()
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, "reproject")
|
||||||
|
if self.reproject:
|
||||||
|
box.label(text='After Realigning, reproject each frames on front axis')
|
||||||
|
if not context.region_data.view_perspective == 'CAMERA':
|
||||||
|
box.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||||
|
box.prop(self, "all_strokes")
|
||||||
|
if not self.all_strokes:
|
||||||
|
box.label(text='Only visible and unlocked layers will be reprojected', icon='INFO')
|
||||||
|
|
||||||
|
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
|
||||||
|
if axis != 'AXIS_Y':
|
||||||
|
orient = {
|
||||||
|
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
|
||||||
|
# 'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], #
|
||||||
|
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'],
|
||||||
|
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'],
|
||||||
|
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
|
||||||
|
}
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text=f'Current drawing plane : {orient[axis][0]}', icon=orient[axis][1])
|
||||||
|
box.prop(self, "set_draw_axis")
|
||||||
|
|
||||||
|
|
||||||
|
def exit(self, context, frame):
|
||||||
|
context.scene.frame_current = frame
|
||||||
|
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y' and self.set_draw_axis:
|
||||||
|
context.scene.tool_settings.gpencil_sculpt.lock_axis = 'AXIS_Y'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
t0 = time()
|
||||||
|
oframe = context.scene.frame_current
|
||||||
|
|
||||||
|
o = bpy.context.object
|
||||||
|
if o.animation_data and o.animation_data.action:
|
||||||
|
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'):
|
||||||
|
align_all_frames(reproject=self.reproject)
|
||||||
|
print(f'\nAnim realign ({time()-t0:.2f}s)')
|
||||||
|
self.exit(context, oframe)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
align_global(reproject=self.reproject)
|
||||||
|
print(f'\nGlobal Realign ({time()-t0:.2f}s)')
|
||||||
|
self.exit(context, oframe)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
||||||
|
bl_idname = "gp.batch_reproject_all_frames"
|
||||||
|
bl_label = "Reproject All Frames"
|
||||||
|
bl_description = "Reproject all frames of active object."
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GREASEPENCIL'
|
||||||
|
|
||||||
|
all_strokes : bpy.props.BoolProperty(
|
||||||
|
name='All Strokes', default=True,
|
||||||
|
description='Hided and locked layer will also be reprojected')
|
||||||
|
|
||||||
|
type : bpy.props.EnumProperty(name='Type',
|
||||||
|
items=(('CURRENT', "Current", ""),
|
||||||
|
('FRONT', "Front", ""),
|
||||||
|
('SIDE', "Side", ""),
|
||||||
|
('TOP', "Top", ""),
|
||||||
|
('VIEW', "View", ""),
|
||||||
|
('CURSOR', "Cursor", ""),
|
||||||
|
# ('SURFACE', "Surface", ""),
|
||||||
|
),
|
||||||
|
default='CURRENT')
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||||
|
self.report({'ERROR'}, 'Does not work in Multi-edit')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
if not context.region_data.view_perspective == 'CAMERA':
|
||||||
|
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||||
|
layout.label(text='Reprojection is made from camera', icon='ERROR')
|
||||||
|
layout.prop(self, "all_strokes")
|
||||||
|
layout.prop(self, "type", text='Project Axis')
|
||||||
|
|
||||||
|
## Hint show axis
|
||||||
|
if self.type == 'CURRENT':
|
||||||
|
## Show as prop
|
||||||
|
# row = layout.row()
|
||||||
|
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
|
||||||
|
# row.enabled = False
|
||||||
|
|
||||||
|
orient = {
|
||||||
|
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
|
||||||
|
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
|
||||||
|
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
|
||||||
|
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
|
||||||
|
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
|
||||||
|
}
|
||||||
|
box = layout.box()
|
||||||
|
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
|
||||||
|
box.label(text=orient[axis][0], icon=orient[axis][1])
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
t0 = time()
|
||||||
|
orient = self.type
|
||||||
|
if self.type == 'CURRENT':
|
||||||
|
orient = None
|
||||||
|
|
||||||
|
batch_reproject(context.object, proj_type=orient, all_strokes=self.all_strokes, restore_frame=True)
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
### -- MENU ENTRY --
|
||||||
|
|
||||||
|
def reproject_clean_menu(self, context):
|
||||||
|
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||||
|
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||||
|
|
||||||
|
def reproject_context_menu(self, context):
|
||||||
|
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||||
|
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||||
|
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GPTB_OT_realign,
|
||||||
|
GPTB_OT_batch_reproject_all_frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cl in classes:
|
||||||
|
bpy.utils.register_class(cl)
|
||||||
|
|
||||||
|
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
|
||||||
|
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
|
||||||
|
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
|
||||||
|
|
||||||
|
for cl in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cl)
|
482
OP_render.py
482
OP_render.py
|
@ -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() """
|
|
|
@ -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()
|
||||||
|
|
327
README.md
327
README.md
|
@ -2,11 +2,18 @@
|
||||||
|
|
||||||
Blender addon - Various tool to help with grease pencil in animation productions.
|
Blender addon - Various tool to help with grease pencil in animation productions.
|
||||||
|
|
||||||
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
|
### /!\ Main branch is currently broken, in migration to gpv3
|
||||||
|
|
||||||
<!-- ### [Demo Youtube]() -->
|
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||||
|
|
||||||
**[Readme Doc in French (Documentation en Français et plus détaillée)](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/blob/master/README_FR.md)**
|
**[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
|
||||||
|
|
||||||
|
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||||
|
|
||||||
|
**[Readme Doc in French](README_FR.md)**
|
||||||
|
|
||||||
|
It is recommended to enable _Grease Pencil Tools_ native Blender addon. Brings essentials features:
|
||||||
|
Canvas rotation, Box deform, Timeline scrubbing in viewport, Quick layer navigator.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -29,9 +36,38 @@ Set path to the palette folder (there is a json palette IO but you an also put a
|
||||||
Note about palette : For now the importer is not working with linked palette as it's not easy for animator (there are properties of the material you cannot access and the link grey-out fade the real color in UIlist preview)
|
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.
|
An "on save" Handler that trigger relative remap of all path can be enabled in addon prefs (disabled by default).
|
||||||
|
|
||||||
### function
|
### function
|
||||||
|
|
||||||
|
@ -71,7 +107,7 @@ Store strokes in os'clipboard (easier cross blend copy)
|
||||||
cutting is use a more user friendly (leave boundary points of left strokes untouched).
|
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,11 +120,44 @@ 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
|
||||||
|
|
||||||
|
<!--
|
||||||
## Todo:
|
## Todo:
|
||||||
|
|
||||||
- Allow to render resolution from cam name
|
- Allow to render resolution from cam name
|
||||||
|
@ -105,251 +174,9 @@ Panel in sidebar : 3D view > sidebar 'N' > Gpencil
|
||||||
|
|
||||||
- Move automatically view to match GP Front (depending on Gpencil view settings)
|
- Move automatically view to match GP Front (depending on Gpencil view settings)
|
||||||
|
|
||||||
- move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures)
|
- move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures) -->
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog:
|
Consult [Changelog here](CHANGELOG.md)
|
||||||
|
|
||||||
1.0.3:
|
|
||||||
|
|
||||||
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
|
|
||||||
|
|
||||||
1.0.2:
|
|
||||||
|
|
||||||
- pref: Added option to disable always remap relative on save in addon-preference
|
|
||||||
|
|
||||||
1.0.1:
|
|
||||||
|
|
||||||
- fix: copy paste problems
|
|
||||||
- Get points uv_properties (used for brushed points)
|
|
||||||
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
|
|
||||||
|
|
||||||
1.0.0:
|
|
||||||
|
|
||||||
- Compatible with official grease pencil tools
|
|
||||||
- removed box deform and rotate canvas that existed in other
|
|
||||||
|
|
||||||
0.9.3:
|
|
||||||
|
|
||||||
- feat: keyframe jump keys are now auto-binded
|
|
||||||
- UI: added keyframe jump customisation in addon pref
|
|
||||||
- code: split keyframe jump in a separate file with his new key updater
|
|
||||||
|
|
||||||
0.9.2:
|
|
||||||
|
|
||||||
- doc: Correct download link (important, bugged the addon install) + update
|
|
||||||
- code: added tracker url
|
|
||||||
- updater: remove updater temp file, reset minimum version, turn off verbose mode
|
|
||||||
|
|
||||||
0.9.1:
|
|
||||||
|
|
||||||
- Public release
|
|
||||||
- prefs: added fps as part of project settings
|
|
||||||
- check file use pref fps value (previously used harcoded 24fps value)
|
|
||||||
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
|
|
||||||
- update: apply changes in integrated copy-paste from the last version of standalone addon
|
|
||||||
- doc: Added fully-detailed french readme
|
|
||||||
|
|
||||||
|
|
||||||
0.8.0:
|
|
||||||
|
|
||||||
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
|
|
||||||
- stripped associated properties from properties.py and passed as wm props.
|
|
||||||
|
|
||||||
0.7.2:
|
|
||||||
|
|
||||||
- fix: Palette importer bug
|
|
||||||
|
|
||||||
0.7.0:
|
|
||||||
|
|
||||||
- feat: auto create empty frame on color layer
|
|
||||||
|
|
||||||
0.6.3:
|
|
||||||
|
|
||||||
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
|
|
||||||
|
|
||||||
0.6.2:
|
|
||||||
|
|
||||||
- feat: colorisation, Option to change stop lines length
|
|
||||||
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
|
|
||||||
- Minor refactor for submodule register
|
|
||||||
|
|
||||||
0.6.1:
|
|
||||||
|
|
||||||
- feat: render objects grouped, one anim render with all ticked object using manual output name
|
|
||||||
|
|
||||||
0.6.0:
|
|
||||||
|
|
||||||
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
|
|
||||||
|
|
||||||
0.5.9:
|
|
||||||
|
|
||||||
- feat: render exporter
|
|
||||||
- Render a selection of GP object isolated from the rest
|
|
||||||
- added exclusions names for GP object listing
|
|
||||||
- setup settings and output according to a name
|
|
||||||
- open render folder
|
|
||||||
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
|
|
||||||
- check file: set scene resolution to settings in prefs (default 2048x1080)
|
|
||||||
|
|
||||||
0.5.8:
|
|
||||||
|
|
||||||
- feat: GP material append on active object from single blend file
|
|
||||||
|
|
||||||
0.5.7:
|
|
||||||
|
|
||||||
- Added warning message for cursor snapping
|
|
||||||
|
|
||||||
0.5.5 - 0.5.6:
|
|
||||||
|
|
||||||
- check file: added check for placement an projection mode for Gpencil.
|
|
||||||
- add a slider to change edit_lines_opacity globally for all GP data at once
|
|
||||||
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
|
|
||||||
|
|
||||||
0.5.4:
|
|
||||||
|
|
||||||
- feat: anim manager in his own GP_toolbox submenu:
|
|
||||||
- button to list disabled anim (allow to quickly check state of the scene)
|
|
||||||
- disable/enable all fcurve in for GP object or other object separately to paint
|
|
||||||
- shift clic to target selection only
|
|
||||||
- check file: added disabled fcurved counter alert with detail in console
|
|
||||||
|
|
||||||
0.5.3:
|
|
||||||
|
|
||||||
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
|
|
||||||
- check file option: change select active tool (choice added in addon preferences)
|
|
||||||
|
|
||||||
0.5.2:
|
|
||||||
|
|
||||||
- Revert back obj_cam operator for following object (native lock view follow only translation)
|
|
||||||
- Changed method for canvas rotation to more robust rotate axis.
|
|
||||||
- Add operators on link checker to open containing folder/file of link
|
|
||||||
- Refactor: file checkers in their own file
|
|
||||||
|
|
||||||
0.5.1:
|
|
||||||
|
|
||||||
- fix: error when empty material slot on GP object.
|
|
||||||
- fix: cursor snap on GP canvas when GP is parented
|
|
||||||
- change: Deleted obj cam (and related set view) operator
|
|
||||||
- change: blacker note background for playblast (stamp_background)
|
|
||||||
- feat: Always playblast from main camera (if in draw_cam)
|
|
||||||
- feat: Handler added to Remap relative on save (pre)
|
|
||||||
- ops: Check for broken links with porposition to find missing files
|
|
||||||
- ops: Added basic hardcoded file checker
|
|
||||||
- Lock main cam
|
|
||||||
- set scene percentage at 100
|
|
||||||
- set show slider and sync range
|
|
||||||
- set fps to 24
|
|
||||||
|
|
||||||
0.4.6:
|
|
||||||
|
|
||||||
- feat: basic Palette manager with base material check and warning
|
|
||||||
|
|
||||||
0.4.5:
|
|
||||||
|
|
||||||
- open blender config folder from addon preference
|
|
||||||
- fix: obj cam parent on selected object
|
|
||||||
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
|
|
||||||
- freeview : bug when rotating free viewfrom cardianl views
|
|
||||||
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
|
|
||||||
|
|
||||||
|
|
||||||
0.4.4:
|
|
||||||
|
|
||||||
- feat: added cursor follow handlers and UI toggle
|
|
||||||
|
|
||||||
0.4.3:
|
|
||||||
|
|
||||||
- change playblast out to 'images' and add playblast as name prefix
|
|
||||||
|
|
||||||
0.4.2:
|
|
||||||
|
|
||||||
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
|
|
||||||
- fix: canvas rotate works with parented camera !
|
|
||||||
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
|
|
||||||
|
|
||||||
0.4.1:
|
|
||||||
|
|
||||||
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
|
|
||||||
|
|
||||||
0.4.0:
|
|
||||||
|
|
||||||
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
|
|
||||||
|
|
||||||
0.3.8:
|
|
||||||
|
|
||||||
- UI: expose onion skin in interface
|
|
||||||
- UI: expose autolock in interface
|
|
||||||
- UI : putted tint layers in a submenu
|
|
||||||
- code: refactor, pushed most of class register in their owner file
|
|
||||||
- tool: tool to rename current or all grease pencil datablock with different name than container object
|
|
||||||
|
|
||||||
0.3.7:
|
|
||||||
|
|
||||||
- UI: new interface with tabs for addon preferences
|
|
||||||
- UI: possible to disable color panel from preference (might be deleted if unusable)
|
|
||||||
- docs: change readme changelog format and correct doc
|
|
||||||
|
|
||||||
0.3.6:
|
|
||||||
|
|
||||||
- UI: Stoplines : add a button for quickly set stoplines visibility.
|
|
||||||
|
|
||||||
0.3.5:
|
|
||||||
|
|
||||||
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
|
|
||||||
- Fix: Enter key added to valid object-breakdown modal.
|
|
||||||
|
|
||||||
0.3.3:
|
|
||||||
|
|
||||||
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
|
|
||||||
|
|
||||||
0.3.3:
|
|
||||||
|
|
||||||
- version 1 beta of gmic colorize
|
|
||||||
- variant of `screen.gp_keyframe_jump` through keymap seetings
|
|
||||||
|
|
||||||
0.3.0:
|
|
||||||
|
|
||||||
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
|
|
||||||
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
|
|
||||||
|
|
||||||
0.2.3:
|
|
||||||
|
|
||||||
- add operator to `screen.gp_keyframe_jump`
|
|
||||||
- add shortcut to rotate canvas
|
|
||||||
- fix duplicate class
|
|
||||||
|
|
||||||
0.2.2:
|
|
||||||
|
|
||||||
- separated props resolution_percentage parameter
|
|
||||||
- playblast options for launching folder and opening folder
|
|
||||||
|
|
||||||
0.2.1:
|
|
||||||
|
|
||||||
- playblast feature
|
|
||||||
- Button to go zoom 100% or fit screen
|
|
||||||
- display scene resolution with res indicator
|
|
||||||
- Fix reference panel : works with video and display in a box layout.
|
|
||||||
- close pseudo-color panel by default (plan to move it to Gpencil tab)
|
|
||||||
|
|
||||||
0.2.0:
|
|
||||||
|
|
||||||
- UI: Toggle camera background images from Toolbox panel
|
|
||||||
- UI: quick access to passepartout
|
|
||||||
- Feature: option to use namespace for pseudo color
|
|
||||||
|
|
||||||
0.1.5:
|
|
||||||
|
|
||||||
- added CGC-auto-updater
|
|
||||||
|
|
||||||
0.1.3:
|
|
||||||
|
|
||||||
- flip cam x
|
|
||||||
- inital stage of overlay toggle (need pref/multiple pref)
|
|
||||||
|
|
||||||
0.1.2:
|
|
||||||
|
|
||||||
- subpanel of GP data (instead of direct append)
|
|
||||||
- initial commit with GP pseudo color
|
|
120
README_FR.md
120
README_FR.md
|
@ -2,14 +2,33 @@
|
||||||
|
|
||||||
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
|
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
|
||||||
|
|
||||||
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
|
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||||
|
|
||||||
> 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é)
|
**[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
|
||||||
|
|
||||||
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.
|
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||||
|
|
||||||
|
**[English Readme Doc](README.md)**
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -21,7 +40,7 @@ Expose les options suivantes:
|
||||||
- Zoom fit - Ajuste la vue pour que la cam soit vue en totalité dans l'écran (ops `view3d.view_center_camera`)
|
- 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.
|
||||||
|
|
||||||
|
@ -53,16 +72,19 @@ 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:
|
||||||
- Lock main cam
|
la liste est visible et modifiable dans l'onglet "Check list" des preférences d'addons.
|
||||||
- set scene res to def project res (specified in addon prefs)
|
`Ctrl + Clic` sur le bonton permet de lister les changement sans les appliquer
|
||||||
- set scene percentage at 100:
|
Voilà quelques exemples:
|
||||||
- set show slider and sync range in opened dopesheet
|
- Lock main cam
|
||||||
- set fps to 24 (need generalisation with addonpref choice)
|
- set scene res to def project res (specified in addon prefs)
|
||||||
- set select cursor type (according to prefs ?)
|
- set scene percentage at 100:
|
||||||
- GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
- set show slider and sync range in opened dopesheet
|
||||||
- GP stroke placement/projection check (just warn if your not in 'Front')
|
- set fps to 24 (need generalisation with addonpref choice)
|
||||||
- Warn if there are some disabled animation (and list datapath)
|
- set select cursor type (according to prefs ?)
|
||||||
- Set onion skin filter to 'All type' (this became default in blender 2.91, guess who asked ;) )
|
- GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
||||||
|
- GP stroke placement/projection check (just warn if your not in 'Front')
|
||||||
|
- Warn if there are some disabled animation (and list datapath)
|
||||||
|
- Set onion skin filter to 'All type'
|
||||||
|
|
||||||
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.
|
||||||
|
@ -86,64 +108,72 @@ 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)
|
---
|
||||||
|
|
||||||
|
[Liste des changements ici](CHANGELOG.md)
|
|
@ -0,0 +1,68 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import WorkSpaceTool
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
from time import time
|
||||||
|
from .utils import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_WT_eraser(WorkSpaceTool):
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_context_mode = 'PAINT_GREASE_PENCIL'
|
||||||
|
|
||||||
|
# The prefix of the idname should be your add-on name.
|
||||||
|
bl_idname = "gp.eraser_tool"
|
||||||
|
bl_label = "Eraser"
|
||||||
|
bl_description = (
|
||||||
|
"This is a tooltip\n"
|
||||||
|
"with multiple lines"
|
||||||
|
)
|
||||||
|
bl_icon = "brush.paint_vertex.draw"
|
||||||
|
bl_widget = None
|
||||||
|
bl_keymap = (
|
||||||
|
("gp.eraser", {"type": 'LEFTMOUSE', "value": 'PRESS'},
|
||||||
|
{"properties": []}),
|
||||||
|
("wm.radial_control", {"type": 'F', "value": 'PRESS'},
|
||||||
|
{"properties": [("data_path_primary", 'scene.gptoolprops.eraser_radius')]}),
|
||||||
|
)
|
||||||
|
|
||||||
|
bl_cursor = 'DOT'
|
||||||
|
|
||||||
|
'''
|
||||||
|
def draw_cursor(context, tool, xy):
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
|
||||||
|
radius = context.scene.gptoolprops.eraser_radius
|
||||||
|
draw_circle_2d(xy, (0.75, 0.25, 0.35, 0.85), radius, 32)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def draw_settings(context, layout, tool):
|
||||||
|
layout.prop(context.scene.gptoolprops, "eraser_radius")
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
## --- KEYMAP
|
||||||
|
addon_keymaps = []
|
||||||
|
def register_keymaps():
|
||||||
|
addon = bpy.context.window_manager.keyconfigs.addon
|
||||||
|
|
||||||
|
km = addon.keymaps.new(name="Grease Pencil Stroke Paint (Draw brush)", space_type="EMPTY", region_type='WINDOW')
|
||||||
|
kmi = km.keymap_items.new("gp.eraser", type='LEFTMOUSE', value="PRESS", ctrl=True)
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
kmi.active = prefs.use_precise_eraser
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_tool(GPTB_WT_eraser, after={"builtin.cursor"})
|
||||||
|
#bpy.context.window_manager.keyconfigs.default.keymaps['Grease Pencil Stroke Paint (Draw brush)'].keymap_items[3].idname = 'gp.eraser'
|
||||||
|
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_tool(GPTB_WT_eraser)
|
||||||
|
unregister_keymaps()
|
728
UI_tools.py
728
UI_tools.py
|
@ -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'
|
||||||
|
@ -16,7 +20,7 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
|
||||||
# bl_category = "Tool"
|
# bl_category = "Tool"
|
||||||
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
|
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
|
||||||
bl_label = "Pseudo color"# title
|
bl_label = "Pseudo color"# title
|
||||||
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
|
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
@ -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,28 @@ 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.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
|
||||||
|
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
|
||||||
|
|
||||||
|
row = col.row(align=True)
|
||||||
|
|
||||||
|
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
|
||||||
|
if context.scene.camera and context.scene.camera.scale.x < 0:
|
||||||
row.label(text='',icon='LOOP_BACK')
|
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')
|
||||||
|
@ -81,10 +90,16 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
||||||
if context.scene.camera:
|
if context.scene.camera:
|
||||||
row = layout.row(align=True)# .split(factor=0.5)
|
row = layout.row(align=True)# .split(factor=0.5)
|
||||||
row.label(text='Passepartout')
|
row.label(text='Passepartout')
|
||||||
row.prop(context.scene.camera.data, 'show_passepartout',text='', icon ='OBJECT_HIDDEN' )
|
if context.scene.camera.name == 'draw_cam':
|
||||||
|
row.prop(context.scene.gptoolprops, 'drawcam_passepartout', text='', icon ='OBJECT_HIDDEN')
|
||||||
|
else:
|
||||||
|
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
|
||||||
row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
|
row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
|
||||||
|
# row = layout.row(align=True)
|
||||||
|
# row.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')
|
||||||
|
|
||||||
## background images/videos
|
## background images/videos
|
||||||
|
@ -99,84 +114,72 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
||||||
for bg_img in context.scene.camera.data.background_images:
|
for bg_img in context.scene.camera.data.background_images:
|
||||||
if bg_img.source == 'IMAGE' and bg_img.image:
|
if bg_img.source == 'IMAGE' and bg_img.image:
|
||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.label(text=bg_img.image.name, icon='IMAGE_RGB')# FILE_IMAGE
|
row.prop(bg_img, 'show_background_image', text='', icon='IMAGE_RGB')
|
||||||
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
|
row.prop(bg_img, 'alpha', text=bg_img.image.name) # options={'HIDDEN'}
|
||||||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
# row.label(icon='IMAGE_RGB')
|
||||||
|
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
|
||||||
|
# row.prop(bg_img, 'show_background_image', text='', icon=icon)
|
||||||
|
|
||||||
if bg_img.source == 'MOVIE_CLIP' and bg_img.clip:
|
if bg_img.source == 'MOVIE_CLIP' and bg_img.clip:
|
||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.label(text=bg_img.clip.name, icon='FILE_MOVIE')
|
row.prop(bg_img, 'show_background_image', text='', icon='FILE_MOVIE')
|
||||||
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
|
row.prop(bg_img, 'alpha', text=bg_img.clip.name) # options={'HIDDEN'}
|
||||||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
# row.label(icon='FILE_MOVIE')
|
||||||
|
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
|
||||||
## playblast params
|
# row.prop(bg_img, 'show_background_image', text='', icon=icon)
|
||||||
layout.separator()
|
|
||||||
layout.label(text = 'Playblast:')
|
|
||||||
row = layout.row(align=False)#split(factor=0.6)
|
|
||||||
row.label(text = f'{rd.resolution_x * context.scene.gptoolprops.resolution_percentage // 100} x {rd.resolution_y * context.scene.gptoolprops.resolution_percentage // 100}')
|
|
||||||
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
|
|
||||||
# row.prop(rd, 'resolution_percentage', text='')#real percent scene percentage
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
|
|
||||||
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
|
|
||||||
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
layout.label(text='No camera !', icon = 'ERROR')
|
layout.label(text='No camera !', icon = 'ERROR')
|
||||||
|
|
||||||
|
## Straight line ops from official greasepencil_tools addon if enabled.
|
||||||
|
# if any(x in context.preferences.addons.keys() for x in ('greasepencil_tools', 'greasepencil-addon')): # check enabled addons
|
||||||
|
if hasattr(bpy.types, 'GP_OT_straight_stroke'): # check if operator exists : bpy.ops.gp.straight_stroke.idname()
|
||||||
|
layout.operator(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:')
|
||||||
# row = layout.row(align=False)
|
|
||||||
## maybe remove cursor_follow icon that look like
|
|
||||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
|
||||||
layout.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
|
||||||
layout.prop(context.space_data.overlay, 'use_gpencil_onion_skin')
|
|
||||||
|
|
||||||
|
## Kf Jump filter
|
||||||
|
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
|
||||||
|
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
|
||||||
|
|
||||||
if context.object and context.object.type == 'GPENCIL':
|
if context.object and context.object.type == 'GREASEPENCIL':
|
||||||
layout.prop(context.object.data, 'use_autolock_layers')
|
# col.prop(context.object.data, 'use_autolock_layers') # not often used
|
||||||
layout.prop(context.object, 'show_in_front', text='X-ray')#default text "In Front"
|
col.prop(context.object, 'show_in_front') # text='In Front'
|
||||||
|
|
||||||
## rename datablock temporary layout
|
## rename datablock temporary layout
|
||||||
if context.object.name != context.object.data.name:
|
if context.object.name != context.object.data.name:
|
||||||
box = layout.box()
|
box = col.box()
|
||||||
box.label(text='different name for object and data:', icon='INFO')
|
box.label(text='different name for object and data:', icon='INFO')
|
||||||
row = box.row(align=False)
|
row = box.row(align=False)
|
||||||
row.operator('gp.rename_data_from_obj').rename_all = False
|
row.operator('gp.rename_data_from_obj').rename_all = False
|
||||||
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 not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
|
if prefs.warn_base_palette and prefs.palette_path:
|
||||||
box = layout.box()
|
if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
|
||||||
box.label(text='Missing base material setup', icon='INFO')
|
box = col.box()
|
||||||
box.operator('gp.load_default_palette')
|
box.label(text='Missing base material setup', icon='INFO')
|
||||||
|
box.operator('gp.load_default_palette')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
layout.label(text='No GP object selected')
|
col.label(text='No GP object selected')
|
||||||
|
|
||||||
|
|
||||||
layout.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
## Gpv3: not more edit line (use Curve lines)
|
||||||
|
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
||||||
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
|
|
||||||
layout.operator('gp.create_empty_frames', icon = 'DECORATE_KEYFRAME')
|
|
||||||
|
|
||||||
## File checker
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
|
|
||||||
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
|
|
||||||
|
|
||||||
# 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"
|
||||||
|
@ -186,48 +189,131 @@ 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 ('GREASEPENCIL', 'CAMERA')]
|
||||||
|
# gps = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
|
||||||
|
# cams = [o for o in context.scene.objects if o.type == 'CAMERA']
|
||||||
|
objs = []
|
||||||
|
gps = []
|
||||||
|
cams = []
|
||||||
|
for o in context.scene.objects:
|
||||||
|
if o.type not in ('GREASEPENCIL', 'CAMERA'):
|
||||||
|
objs.append(o)
|
||||||
|
elif o.type == 'GREASEPENCIL':
|
||||||
|
gps.append(o)
|
||||||
|
elif o.type == 'CAMERA':
|
||||||
|
cams.append(o)
|
||||||
|
|
||||||
|
# print(f'{time.perf_counter() - t0:.8f}s')
|
||||||
|
|
||||||
|
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams}
|
||||||
|
|
||||||
|
|
||||||
def draw(self, context):
|
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)
|
|
||||||
row.label(text='Obj anims:')
|
|
||||||
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
|
|
||||||
ops.skip_gp = True
|
|
||||||
ops.skip_obj = False
|
|
||||||
ops.mute = False
|
|
||||||
|
|
||||||
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
|
col.operator('gp.list_disabled_anims')
|
||||||
ops.skip_gp = True
|
|
||||||
ops.skip_obj = False
|
|
||||||
ops.mute = True
|
|
||||||
## Gps
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.label(text='Gp anims:')
|
|
||||||
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
|
|
||||||
ops.skip_gp = False
|
|
||||||
ops.skip_obj = True
|
|
||||||
ops.mute = False
|
|
||||||
|
|
||||||
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
|
## Show Enable / Disable anims
|
||||||
ops.skip_gp = False
|
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]:
|
||||||
ops.skip_obj = True
|
on_icon, off_icon = anim_status(obj_types[cat_type])
|
||||||
ops.mute = True
|
|
||||||
|
|
||||||
class GPTB_PT_tint_layers(bpy.types.Panel):
|
subcol = col.column()
|
||||||
bl_label = "Tint layers"
|
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||||
|
row = subcol.row(align=True)
|
||||||
|
row.label(text=cat)
|
||||||
|
|
||||||
|
ops = row.operator('gp.toggle_mute_animation', text='ON', icon=on_icon)
|
||||||
|
ops.mode = cat_type
|
||||||
|
ops.mute = False
|
||||||
|
|
||||||
|
ops = row.operator('gp.toggle_mute_animation', text='OFF', icon=off_icon)
|
||||||
|
ops.mode = cat_type
|
||||||
|
ops.mute = True
|
||||||
|
|
||||||
|
## GP modifiers
|
||||||
|
subcol = col.column()
|
||||||
|
|
||||||
|
row = subcol.row(align=True)
|
||||||
|
row.label(text='Gp modifiers:')
|
||||||
|
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
|
||||||
|
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||||
|
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
|
||||||
|
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
|
||||||
|
|
||||||
|
## Step Select Frames
|
||||||
|
col.operator('gptb.step_select_frames')
|
||||||
|
|
||||||
|
## Follow curve path
|
||||||
|
col = col.column()
|
||||||
|
row = col.row(align=True)
|
||||||
|
|
||||||
|
if context.object:
|
||||||
|
if context.object.type == 'CURVE' and context.mode in ('OBJECT', 'EDIT_CURVE'):
|
||||||
|
row.operator('object.object_from_curve', text='Back To Object', icon='LOOP_BACK')
|
||||||
|
|
||||||
|
elif (follow_const := context.object.constraints.get('Follow Path')) and follow_const.target:
|
||||||
|
row.operator('object.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE')
|
||||||
|
row.operator('object.remove_follow_path', text='', icon='X')
|
||||||
|
col.label(text=f'{context.object.name} -> {follow_const.target.name}', icon='CON_FOLLOWPATH')
|
||||||
|
if follow_const.use_fixed_location:
|
||||||
|
col.prop(follow_const, 'offset_factor')
|
||||||
|
else:
|
||||||
|
col.prop(follow_const, 'offset')
|
||||||
|
if context.object.location.length != 0: # context.object.location[:] != (0,0,0):
|
||||||
|
col.operator('object.location_clear', text='Offseted Location! Reset', icon='ERROR')
|
||||||
|
# ? Check if object location is animated ? (can be intentional...)
|
||||||
|
|
||||||
|
else:
|
||||||
|
col.operator('object.create_follow_path_curve', text='Create Follow Curve', icon='CURVE_BEZCURVE')
|
||||||
|
|
||||||
|
|
||||||
|
## This can go in an extra category...
|
||||||
|
col = layout.column()
|
||||||
|
col.use_property_split = False
|
||||||
|
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||||
|
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||||
|
if context.scene.gptoolprops.cursor_follow:
|
||||||
|
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_PT_toolbox_playblast(Panel):
|
||||||
|
bl_label = "Playblast"
|
||||||
bl_space_type = "VIEW_3D"
|
bl_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 draw(self, context):
|
||||||
def poll(cls, context):
|
layout = self.layout
|
||||||
return context.scene.camera
|
rd = context.scene.render
|
||||||
|
row = layout.row(align=False) # split(factor=0.6)
|
||||||
|
percent = context.scene.gptoolprops.resolution_percentage
|
||||||
|
row.label(text = f'{rd.resolution_x * percent // 100} x {rd.resolution_y * percent // 100}')
|
||||||
|
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
|
||||||
|
# row.prop(rd, 'resolution_percentage', text='') # real percent scene percentage
|
||||||
|
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
|
||||||
|
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
|
||||||
|
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
|
||||||
|
|
||||||
|
class GPTB_PT_tint_layers(Panel):
|
||||||
|
bl_label = "Tint Layers"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Gpencil"
|
||||||
|
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
# 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="")
|
||||||
|
@ -238,50 +324,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 = Path(bpy.data.filepath)
|
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
|
||||||
out = blend.parents[1] / "compo" / "base"
|
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
|
||||||
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"
|
||||||
|
@ -307,36 +425,375 @@ 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
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||||
layout.separator()
|
layout.separator()
|
||||||
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.separator()
|
||||||
|
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
|
||||||
|
|
||||||
|
|
||||||
|
def expose_use_channel_color_pref(self, context):
|
||||||
|
# add in GreasePencilLayerDisplayPanel (gp dopesheet View > Display)
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
layout.label(text='Use Channel Colors (User preferences):')
|
||||||
|
layout.prop(context.preferences.edit, 'use_anim_channel_group_colors')
|
||||||
|
|
||||||
|
|
||||||
|
#--- Palette Linker Panels
|
||||||
|
|
||||||
|
def palettes_path_ui(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
scn = bpy.context.scene
|
||||||
|
pl_prop = scn.bl_palettes_props
|
||||||
|
col= layout.column()
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
## Here put the path thing (only to use a non-library)
|
||||||
|
|
||||||
|
# maybe in submenu...
|
||||||
|
row = col.row()
|
||||||
|
# expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT'
|
||||||
|
# row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False)
|
||||||
|
row.prop(pl_prop, 'use_project_path', text='Use Project Palettes')
|
||||||
|
# row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||||
|
|
||||||
|
if pl_prop.use_project_path:
|
||||||
|
## gp toolbox addon prefs path
|
||||||
|
if not prefs.palette_path:
|
||||||
|
col.label(text='GP toolbox Palette Directory Needed', icon='INFO')
|
||||||
|
col.operator('gptb.open_addon_prefs', icon='PREFERENCES')
|
||||||
|
|
||||||
|
# if not prefs.palette_path: # or pl_prop.show_path
|
||||||
|
# col.prop(prefs, 'palette_path', text='Project Dir')
|
||||||
|
#col.label(text='(saved with preferences)')
|
||||||
|
else:
|
||||||
|
## local path
|
||||||
|
if not pl_prop.custom_dir:
|
||||||
|
col.label(text='Need to specify directory')
|
||||||
|
col.prop(pl_prop, 'custom_dir', text='Custom Dir')
|
||||||
|
# if not pl_prop.custom_dir or pl_prop.show_path:
|
||||||
|
# col.prop(pl_prop, 'custom_dir', text='Custom Dir')
|
||||||
|
|
||||||
|
# col.operator('gptb.palette_version_update', text='Update Palette Version') # when update is ready
|
||||||
|
|
||||||
|
|
||||||
|
def palettes_lists_ui(self, context, popup=False):
|
||||||
|
layout = self.layout
|
||||||
|
scn = bpy.context.scene
|
||||||
|
pl_prop = scn.bl_palettes_props
|
||||||
|
col= layout.column()
|
||||||
|
row=col.row()
|
||||||
|
# refresh button
|
||||||
|
txt = 'Project Palettes' if pl_prop.use_project_path else 'Custom Palettes'
|
||||||
|
row.label(text=txt)
|
||||||
|
row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||||
|
|
||||||
|
col= layout.column()
|
||||||
|
row = col.row()
|
||||||
|
|
||||||
|
if popup:
|
||||||
|
blends_minimum_row = 5
|
||||||
|
objects_minimum_row = 25
|
||||||
|
else:
|
||||||
|
blends_minimum_row = 2
|
||||||
|
objects_minimum_row = 4
|
||||||
|
row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx",
|
||||||
|
rows=blends_minimum_row)
|
||||||
|
# side panel
|
||||||
|
# subcol = row.column(align=True)
|
||||||
|
# subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||||
|
|
||||||
|
## Show object UI list only once blend Uilist is filled ?
|
||||||
|
if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)):
|
||||||
|
col.label(text='Select blend refresh available objects')
|
||||||
|
|
||||||
|
row = col.row()
|
||||||
|
row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx",
|
||||||
|
rows=objects_minimum_row)
|
||||||
|
|
||||||
|
## Show link button in the border of the UI list ?
|
||||||
|
# col.prop(pl_prop, 'import_type')
|
||||||
|
split = col.split(align=True, factor=0.4)
|
||||||
|
split.prop(pl_prop, 'import_type', text='')
|
||||||
|
|
||||||
|
split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path)
|
||||||
|
split.operator('gp.import_obj_palette', text='Palette')
|
||||||
|
|
||||||
|
# button to launch link with combined props (active only if the two items are valids)
|
||||||
|
# str(Path(self.blends) / 'Object' / self.objects
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_PT_palettes_linker_main_ui(Panel):
|
||||||
|
bl_space_type = 'TOPBAR' # dummy
|
||||||
|
bl_region_type = 'HEADER'
|
||||||
|
# bl_space_type = "VIEW_3D"
|
||||||
|
# bl_region_type = "UI"
|
||||||
|
# bl_category = "Gpencil"
|
||||||
|
bl_label = "Palettes Mat Linker"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
## link button for tests
|
||||||
|
# layout.operator('gp.import_obj_palette', text='Palette')
|
||||||
|
|
||||||
|
# Subpanel are appended to this main UI
|
||||||
|
|
||||||
|
## Or just as One fat panel
|
||||||
|
# palettes_path_ui(self, context)
|
||||||
|
# palettes_lists_ui(self, context)
|
||||||
|
|
||||||
|
class GPTB_PT_palettes_path_ui(Panel):
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Gpencil"
|
||||||
|
bl_label = "Palettes Source" # Source Path
|
||||||
|
# bl_parent_id = "GPTB_PT_palettes_linker_main_ui"
|
||||||
|
bl_parent_id = "GPTB_PT_color"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
palettes_path_ui(self, context)
|
||||||
|
# layout.label()
|
||||||
|
|
||||||
|
# pop-up version of object lists
|
||||||
|
class GPTB_PT_palettes_list_popup(Panel):
|
||||||
|
bl_space_type = 'TOPBAR' # dummy
|
||||||
|
bl_region_type = 'HEADER'
|
||||||
|
bl_category = "Gpencil"
|
||||||
|
bl_label = "Palettes Lists"
|
||||||
|
bl_ui_units_x = 18
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
palettes_lists_ui(self, context, popup=True)
|
||||||
|
|
||||||
|
class GPTB_PT_palettes_list_ui(Panel):
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Gpencil"
|
||||||
|
bl_label = "Palettes Lists"
|
||||||
|
bl_parent_id = "GPTB_PT_color"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
# layout.label(text="My Select Panel")
|
||||||
|
layout.operator("wm.call_panel", text="", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
palettes_lists_ui(self, context, popup=False)
|
||||||
|
|
||||||
|
|
||||||
|
## bl 3+ UI
|
||||||
|
def asset_browser_ui(self, context):
|
||||||
|
'''Only shows in blender >= 3.0.0'''
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
asset_file_handle = context.asset_file_handle
|
||||||
|
if asset_file_handle is None:
|
||||||
|
# layout.label(text="No asset selected", icon='INFO')
|
||||||
|
layout.label(text='No object/material selected', icon='INFO')
|
||||||
|
return
|
||||||
|
if asset_file_handle.id_type not in ('OBJECT', 'MATERIAL'):
|
||||||
|
layout.label(text='No object/material selected', icon='INFO')
|
||||||
|
return
|
||||||
|
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
asset_library_ref = context.asset_library_ref
|
||||||
|
## Path to blend
|
||||||
|
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
|
||||||
|
path_to_obj = Path(asset_lib_path) / 'Objects' / asset_file_handle.name
|
||||||
|
|
||||||
|
## respect header choice ?
|
||||||
|
## import_type in (LINK, APPEND, APPEND_REUSE)
|
||||||
|
imp_type = context.space_data.params.import_type
|
||||||
|
if imp_type == 'APPEND':
|
||||||
|
imp_txt = 'Append'
|
||||||
|
elif imp_type == 'APPEND_REUSE':
|
||||||
|
imp_txt = 'Append (Reuse)'
|
||||||
|
else:
|
||||||
|
imp_txt = 'Link'
|
||||||
|
|
||||||
|
if asset_file_handle.id_type == 'MATERIAL':
|
||||||
|
layout.label(text=f'From Mat: {asset_file_handle.name}')
|
||||||
|
if asset_file_handle.id_type == 'OBJECT':
|
||||||
|
layout.label(text=f'From Obj: {asset_file_handle.name}')
|
||||||
|
layout.label(text=f'{imp_txt} Materials To GP Object')
|
||||||
|
layout.operator('gp.palette_linker', text=f'{imp_txt} Materials To GP Object') ## ops
|
||||||
|
|
||||||
|
# layout.label(text='Link Materials to GP Object')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Put back pop-over UI for Grease Pencil stroke interpolation tools native pop hover panel from 2.92
|
||||||
|
class GPTB_PT_tools_grease_pencil_interpolate(Panel):
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'HEADER'
|
||||||
|
bl_label = "Interpolate"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.gpencil_data is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
gpd = context.gpencil_data
|
||||||
|
valid_mode = bool(gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode)
|
||||||
|
return bool(context.editable_gpencil_strokes) and valid_mode
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
|
||||||
|
## access active tool settings
|
||||||
|
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
|
||||||
|
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate')
|
||||||
|
|
||||||
|
## custom curve access (still in gp interpolate tools)
|
||||||
|
interpolate_settings = context.tool_settings.gpencil_interpolate
|
||||||
|
# ex : interpolate_settings.interpolation_curve.curves[0].points[1].location
|
||||||
|
|
||||||
|
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text="Interpolate Strokes")
|
||||||
|
col.operator("gpencil.interpolate", text="Interpolate")
|
||||||
|
col.operator("gpencil.interpolate_sequence", text="Sequence")
|
||||||
|
col.operator("gpencil.interpolate_reverse", text="Remove Breakdowns")
|
||||||
|
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text="Options:")
|
||||||
|
# col.prop(settings, "interpolate_all_layers") # now the enum "layers"
|
||||||
|
gpd = context.gpencil_data
|
||||||
|
if gpd.use_stroke_edit_mode:
|
||||||
|
col.prop(settings, "interpolate_selected_only")
|
||||||
|
col.prop(settings, "layers")
|
||||||
|
col.prop(settings, "flip")
|
||||||
|
col.prop(settings, "smooth_factor")
|
||||||
|
col.prop(settings, "smooth_steps")
|
||||||
|
|
||||||
|
|
||||||
|
'''## Sequence Options
|
||||||
|
seq_settings = context.window_manager.operators.get('GPENCIL_OT_interpolate_sequence')
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text="Sequence Options:")
|
||||||
|
if not seq_settings:
|
||||||
|
# col.label(text='Launch Interpolate Sequence Once')
|
||||||
|
# col.operator('gpencil.interpolate_sequence',text='Interpolate Sequence Once')
|
||||||
|
col.label(text='Interpolate sequence', icon='INFO')
|
||||||
|
col.label(text='must be launched')
|
||||||
|
col.label(text="once per session")
|
||||||
|
col.label(text="to expose it's properties")
|
||||||
|
return
|
||||||
|
|
||||||
|
col.prop(seq_settings, "step")
|
||||||
|
col.prop(seq_settings, "layers")
|
||||||
|
col.prop(seq_settings, "interpolate_selected_only")
|
||||||
|
col.prop(seq_settings, "flip")
|
||||||
|
col.prop(seq_settings, "smooth_factor")
|
||||||
|
col.prop(seq_settings, "smooth_steps")
|
||||||
|
col.prop(seq_settings, "type")
|
||||||
|
if seq_settings.type == 'CUSTOM':
|
||||||
|
# TODO: Options for loading/saving curve presets?
|
||||||
|
col.template_curve_mapping(interpolate_settings, "interpolation_curve", brush=True,
|
||||||
|
use_negative_slope=True)
|
||||||
|
elif seq_settings.type != 'LINEAR':
|
||||||
|
col.prop(seq_settings, "easing")
|
||||||
|
|
||||||
|
if seq_settings.type == 'BACK':
|
||||||
|
layout.prop(seq_settings, "back")
|
||||||
|
elif seq_settings.type == 'ELASTIC':
|
||||||
|
sub = layout.column(align=True)
|
||||||
|
sub.prop(seq_settings, "amplitude")
|
||||||
|
sub.prop(seq_settings, "period")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## recreate property group from operator options
|
||||||
|
# inspect context.window_manager.operators['GPENCIL_OT_interpolate_sequence']
|
||||||
|
# separate options from single interpolation and sequence interpolation
|
||||||
|
|
||||||
|
# class GPTB_PG_interpolate_sequence_prop(bpy.types.PropertyGroup):
|
||||||
|
# interpolate_selected_only : BoolProperty(
|
||||||
|
# name="Selected Only",
|
||||||
|
# description="",
|
||||||
|
# default=True,
|
||||||
|
# options={'HIDDEN'})
|
||||||
|
|
||||||
|
def interpolate_header_ui(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
obj = context.active_object
|
||||||
|
|
||||||
|
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data:
|
||||||
|
gpd = context.gpencil_data
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode:
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.popover(
|
||||||
|
panel="GPTB_PT_tools_grease_pencil_interpolate",
|
||||||
|
text="Interpolate",
|
||||||
|
)
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
GPTB_PT_sidebar_panel,
|
GPTB_PT_sidebar_panel,
|
||||||
|
GPTB_PT_checker,
|
||||||
GPTB_PT_anim_manager,
|
GPTB_PT_anim_manager,
|
||||||
|
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_grease_pencil_mode.append(expose_use_channel_color_pref)
|
||||||
|
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||||
|
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||||
|
|
||||||
|
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
|
||||||
|
|
||||||
|
# if bpy.app.version >= (3,0,0):
|
||||||
|
# bpy.types.ASSETBROWSER_PT_metadata.append(asset_browser_ui)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
|
||||||
|
|
||||||
|
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
|
||||||
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||||
|
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||||
|
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||||
|
|
||||||
|
|
||||||
|
# if bpy.app.version >= (3,0,0):
|
||||||
|
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
|
||||||
|
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
@ -357,36 +814,3 @@ def GPdata_toolbox_panel(self, context):
|
||||||
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
|
col.operator("gp.auto_tint_gp_layers", 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") """
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,22 @@
|
||||||
# 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",
|
"author": "Samuel Bernou, Christophe Seux",
|
||||||
"version": (1, 0, 3),
|
"version": (4, 0, 4),
|
||||||
"blender": (2, 91, 0),
|
"blender": (4, 3, 0),
|
||||||
"location": "sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
|
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
|
||||||
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
|
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
|
||||||
"category": "3D View",
|
"category": "3D View",
|
||||||
}
|
}
|
||||||
|
|
||||||
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,24 +30,47 @@ 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_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_flat_reproject # Disabled
|
||||||
|
from . import OP_depth_move
|
||||||
|
from . import OP_key_duplicate_send
|
||||||
|
from . import OP_layer_manager
|
||||||
|
from . import OP_layer_picker
|
||||||
|
from . import OP_layer_nav
|
||||||
|
from . import OP_material_picker
|
||||||
|
from . import OP_git_update
|
||||||
|
from . import OP_layer_namespace
|
||||||
|
from . import OP_pseudo_tint
|
||||||
|
from . import OP_follow_curve
|
||||||
|
from . import OP_material_move_to_layer
|
||||||
|
# from . import OP_eraser_brush
|
||||||
|
# from . import TOOL_eraser_brush
|
||||||
|
from . import handler_draw_cam
|
||||||
from . import keymaps
|
from . 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
|
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
|
||||||
|
@ -73,11 +85,14 @@ from pathlib import Path
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def remap_relative(dummy):
|
def remap_relative(dummy):
|
||||||
|
# try:
|
||||||
all_path = [lib for lib in bpy.utils.blend_paths(local=True)]
|
all_path = [lib for lib in bpy.utils.blend_paths(local=True)]
|
||||||
bpy.ops.file.make_paths_relative()
|
bpy.ops.file.make_paths_relative()
|
||||||
for i, lib in enumerate(bpy.utils.blend_paths(local=True)):
|
for i, lib in enumerate(bpy.utils.blend_paths(local=True)):
|
||||||
if all_path[i] != lib:
|
if all_path[i] != lib:
|
||||||
print('Remapped:', all_path[i], '\n>> ', lib)
|
print('Remapped:', all_path[i], '\n>> ', lib)
|
||||||
|
# except Exception as e:
|
||||||
|
# print(e)
|
||||||
|
|
||||||
def remap_on_save_update(self, context):
|
def remap_on_save_update(self, context):
|
||||||
pref = get_addon_prefs()
|
pref = get_addon_prefs()
|
||||||
|
@ -89,74 +104,56 @@ def remap_on_save_update(self, context):
|
||||||
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
||||||
bpy.app.handlers.save_pre.remove(remap_relative)
|
bpy.app.handlers.save_pre.remove(remap_relative)
|
||||||
|
|
||||||
|
|
||||||
|
## precise eraser
|
||||||
|
# def update_use_precise_eraser(self, context):
|
||||||
|
# km, kmi = TOOL_eraser_brush.addon_keymaps[0]
|
||||||
|
# kmi.active = self.use_precise_eraser
|
||||||
|
|
||||||
class GPTB_prefs(bpy.types.AddonPreferences):
|
class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
bl_idname = __name__
|
bl_idname = __name__
|
||||||
|
|
||||||
|
## precise eraser
|
||||||
|
# use_precise_eraser : BoolProperty(
|
||||||
|
# name='Precise Eraser',
|
||||||
|
# default=False,
|
||||||
|
# update=update_use_precise_eraser)
|
||||||
|
|
||||||
## tabs
|
## tabs
|
||||||
|
|
||||||
pref_tabs : EnumProperty(
|
pref_tabs : EnumProperty(
|
||||||
items=(('PREF', "Preferences", "Change some preferences of the modal"),
|
items=(('PREF', "Preferences", "Change some preferences of the modal"),
|
||||||
('MAN_OPS', "Operator", "Operator to add Manually"),
|
('KEYS', "Shortcuts", "Customize addon shortcuts"),
|
||||||
|
('MAN_OPS', "Operators", "Operator to add Manually"),
|
||||||
|
('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"),
|
||||||
|
# ('UPDATE', "Update", "Check and apply updates"),
|
||||||
# ('TUTO', "Tutorial", "How to use the tool"),
|
# ('TUTO', "Tutorial", "How to use the tool"),
|
||||||
('UPDATE', "Update", "Check and apply updates"),
|
|
||||||
# ('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(
|
||||||
name="Relative Remap On Save",
|
name="Relative Remap On Save",
|
||||||
description="Always remap all external path to relative when saving\nNeed blender restart if changed",
|
description="Always remap all external path to relative when saving\nNeed blender restart if changed",
|
||||||
default=True,
|
default=False,
|
||||||
update=remap_on_save_update
|
update=remap_on_save_update
|
||||||
)
|
)
|
||||||
|
|
||||||
fps : IntProperty(
|
fps : IntProperty(
|
||||||
name='Frame Rate',
|
name='Frame Rate',
|
||||||
description="Fps of the project, Used to conform the file when you use Check file operator",
|
description="Fps of the project, Used to conform the file when you use Check file operator",
|
||||||
default=25,
|
default=24,
|
||||||
min=1,
|
min=1,
|
||||||
max=10000
|
max=10000
|
||||||
)
|
)
|
||||||
|
@ -175,16 +172,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
|
||||||
|
|
||||||
|
warn_base_palette : BoolProperty(
|
||||||
|
name="Warn if base palette isn't loaded",
|
||||||
|
description="Display a button to load palette base.json if current grease pencil has a no 'line' and 'invisible' materials",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mat_link_exclude : StringProperty(
|
||||||
|
name="Materials Link Exclude",
|
||||||
|
description="List of material name to exclude when using palette linker (separate multiple value with comma, ex: line, rough)",
|
||||||
|
default="line,", maxlen=0)
|
||||||
|
|
||||||
|
use_env_brushes : BoolProperty(
|
||||||
|
name="Use Project Brushes",
|
||||||
|
description="Load the brushes path in environnement at startup (key 'BRUSHES')",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
brush_path : StringProperty(
|
||||||
|
name="Brushes directory",
|
||||||
|
description="Path to brushes containing the blends holding the brushes",
|
||||||
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
|
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",
|
||||||
|
@ -198,19 +256,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",
|
||||||
|
@ -235,7 +280,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
## KF jumper
|
## KF jumper
|
||||||
kfj_use_shortcut: BoolProperty(
|
kfj_use_shortcut: BoolProperty(
|
||||||
name = "Use Keyframe Jump Shortcut",
|
name = "Use Keyframe Jump Shortcut",
|
||||||
description = "Auto bind shotcut for keyframe jump (else you can bien manually using 'screen.gp_keyframe_jump' id_name)",
|
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
|
||||||
default = True)
|
default = True)
|
||||||
|
|
||||||
kfj_prev_keycode : StringProperty(
|
kfj_prev_keycode : StringProperty(
|
||||||
|
@ -278,12 +323,68 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
description = "add ctrl",
|
description = "add ctrl",
|
||||||
default = False)
|
default = False)
|
||||||
|
|
||||||
|
|
||||||
|
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings)
|
||||||
|
|
||||||
|
## GP Layer navigator
|
||||||
|
|
||||||
|
nav_use_fade : BoolProperty(
|
||||||
|
name='Fade Inactive Layers',
|
||||||
|
description='Fade Inactive layers to determine active layer in a glimpse',
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
nav_fade_val : FloatProperty(
|
||||||
|
name='Fade Value',
|
||||||
|
description='Fade value for other layers when navigating (0=invisible)',
|
||||||
|
default=0.1, min=0.0, max=0.95, step=1, precision=2)
|
||||||
|
|
||||||
|
nav_limit : FloatProperty(
|
||||||
|
name='Fade Duration',
|
||||||
|
description='Time of other layer faded when using layer navigation',
|
||||||
|
default=1.4, min=0.1, max=5, step=2, precision=1, subtype='TIME', unit='TIME')
|
||||||
|
|
||||||
|
nav_use_fade_in : BoolProperty(
|
||||||
|
name='Progressive Fade Back',
|
||||||
|
description='Use a fade on other layer when navigating',
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
nav_fade_in_time : FloatProperty(
|
||||||
|
name='Fade-In Time',
|
||||||
|
description='Duration of the fade',
|
||||||
|
default=0.5, min=0.1, max=5, step=2, precision=2, subtype='TIME', unit='TIME')
|
||||||
|
|
||||||
|
nav_interval : FloatProperty(
|
||||||
|
name='Refresh Rate',
|
||||||
|
description='Refresh rate for fade updating (upper value means stepped fade)',
|
||||||
|
default=0.04, min=0.01, max=0.5, step=3, precision=2, subtype='TIME', unit='TIME')
|
||||||
|
|
||||||
## Temp cutter
|
## Temp cutter
|
||||||
# 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
|
||||||
|
@ -303,13 +404,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='Palette library folder:')
|
subbox.prop(self, 'use_env_palettes', text='Use Palettes Environnement Path')
|
||||||
box.prop(self, 'palette_path')
|
subbox.prop(self, 'palette_path')
|
||||||
|
subbox.prop(self, 'warn_base_palette')
|
||||||
|
|
||||||
|
subbox.prop(self, 'mat_link_exclude')
|
||||||
|
|
||||||
|
## Brushes
|
||||||
|
subbox.prop(self, 'use_env_brushes', text='Use Brushes Environnement Path')
|
||||||
|
subbox.prop(self, 'brush_path')
|
||||||
|
|
||||||
## render output
|
## 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
|
||||||
|
|
||||||
|
@ -318,60 +459,136 @@ 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 option:')
|
|
||||||
|
|
||||||
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])
|
# box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
|
||||||
text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
|
# if self.kfj_use_shortcut:
|
||||||
text = f'Jump Keyframe Prev: {text} (Click to change)'
|
# prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
|
||||||
|
# if self.kfj_prev_keycode:
|
||||||
|
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_prev_ctrl), ('Shift', self.kfj_prev_shift), ('Alt', self.kfj_prev_alt)] if b])
|
||||||
|
# text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
|
||||||
|
# text = f'Jump Keyframe Prev: {text} (Click to change)'
|
||||||
|
# else:
|
||||||
|
# text = prompt
|
||||||
|
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||||
|
# ops.s_keycode = 'kfj_prev_keycode'
|
||||||
|
# ops.s_ctrl = 'kfj_prev_ctrl'
|
||||||
|
# ops.s_shift = 'kfj_prev_shift'
|
||||||
|
# ops.s_alt = 'kfj_prev_alt'
|
||||||
|
|
||||||
|
# if self.kfj_next_keycode:
|
||||||
|
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
||||||
|
# text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
|
||||||
|
# text = f'Jump Keyframe Next: {text} (Click to change)'
|
||||||
|
# else:
|
||||||
|
# text = prompt
|
||||||
|
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||||
|
# ops.s_keycode = 'kfj_next_keycode'
|
||||||
|
# ops.s_ctrl = 'kfj_next_ctrl'
|
||||||
|
# ops.s_shift = 'kfj_next_shift'
|
||||||
|
# ops.s_alt = 'kfj_next_alt'
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
||||||
|
# box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text='Tools options:')
|
||||||
|
|
||||||
|
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:
|
else:
|
||||||
text = prompt
|
box.label(text='Toolbox can be updated using git')
|
||||||
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
row = box.row()
|
||||||
ops.s_keycode = 'kfj_prev_keycode'
|
row.operator('wm.url_open', text='Download and install git here', icon='URL').url = 'https://git-scm.com/download/'
|
||||||
ops.s_ctrl = 'kfj_prev_ctrl'
|
row.label(text='then restart blender')
|
||||||
ops.s_shift = 'kfj_prev_shift'
|
|
||||||
ops.s_alt = 'kfj_prev_alt'
|
|
||||||
|
|
||||||
if self.kfj_next_keycode:
|
if self.pref_tabs == 'KEYS':
|
||||||
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
# layout.label(text='Shortcuts :')
|
||||||
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'
|
|
||||||
|
|
||||||
else:
|
|
||||||
box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
|
||||||
box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
|
||||||
|
|
||||||
## Active tool
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text='Autofix check button options:')
|
box.label(text='Shortcuts added by GP toolbox with context scope:')
|
||||||
box.prop(self, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
## 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)
|
||||||
|
]:
|
||||||
|
|
||||||
box.prop(self, "render_obj_exclusion", icon='FILTER')#
|
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
|
||||||
|
|
||||||
## random color character separator
|
## numbering hack, need a better way to find multi idname user keymaps
|
||||||
box = layout.box()
|
id_ct = 0
|
||||||
box.label(text='Random color options:')
|
for km_item in km.keymap_items:
|
||||||
box.prop(self, 'separator')
|
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
|
||||||
# layout.label(text='Notes:')
|
# layout.label(text='Notes:')
|
||||||
layout.label(text='Following operators ID have to be set manually :')
|
layout.label(text='Following operators ID have to be set manually in keymap if needed :')
|
||||||
|
|
||||||
## keyframe jump
|
## keyframe jump
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
|
@ -392,13 +609,162 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap'
|
row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap'
|
||||||
box.label(text='Or just create a new shortcut using cursor_snap')
|
box.label(text='Or just create a new shortcut using cursor_snap')
|
||||||
|
|
||||||
|
## Clear keyframe
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text='Clear active frame (delete all strokes without deleting the frame)')
|
||||||
|
row = box.row()
|
||||||
|
row.label(text='gp.clear_active_frame')
|
||||||
|
row.operator('wm.copytext', icon='COPYDOWN').text = 'gp.clear_active_frame'
|
||||||
|
|
||||||
## user prefs
|
## user prefs
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text='Note: You can access user pref file and startup file in config folder')
|
box.label(text='Note: You can access user pref file and startup file in config folder')
|
||||||
box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG')
|
box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG')
|
||||||
|
|
||||||
if self.pref_tabs == 'UPDATE':
|
if self.pref_tabs == 'CHECKS':
|
||||||
addon_updater_ops.update_settings_ui(self, context)
|
layout.label(text='Following checks will be made when clicking "Check File" button:')
|
||||||
|
col = layout.column()
|
||||||
|
col.use_property_split = True
|
||||||
|
# col.prop(self.fixprops, 'check_only')
|
||||||
|
col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO')
|
||||||
|
# col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
|
||||||
|
col.separator()
|
||||||
|
col.prop(self.fixprops, 'lock_main_cam')
|
||||||
|
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
|
||||||
|
col.prop(self.fixprops, 'set_res_percentage')
|
||||||
|
col.prop(self.fixprops, 'set_fps', text=f'Reset FPS (to {self.fps})')
|
||||||
|
col.prop(self.fixprops, 'set_slider_n_sync')
|
||||||
|
col.prop(self.fixprops, 'check_front_axis')
|
||||||
|
col.prop(self.fixprops, 'check_placement')
|
||||||
|
col.prop(self.fixprops, 'set_gp_use_lights_off')
|
||||||
|
col.prop(self.fixprops, 'set_pivot_median_point')
|
||||||
|
col.prop(self.fixprops, 'disable_guide')
|
||||||
|
col.prop(self.fixprops, 'list_disabled_anim')
|
||||||
|
col.prop(self.fixprops, 'list_obj_vis_conflict')
|
||||||
|
col.prop(self.fixprops, 'list_gp_mod_vis_conflict')
|
||||||
|
col.prop(self.fixprops, 'list_broken_mod_targets')
|
||||||
|
col.prop(self.fixprops, 'autokey_add_n_replace')
|
||||||
|
col.prop(self.fixprops, 'remove_redundant_strokes')
|
||||||
|
#-# col.prop(self.fixprops, 'set_cursor_type')
|
||||||
|
|
||||||
|
# col = layout.column()
|
||||||
|
# col.use_property_split = True
|
||||||
|
col.prop(self.fixprops, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
||||||
|
col.prop(self.fixprops, "file_path_type")
|
||||||
|
col.prop(self.fixprops, "lock_object_mode")
|
||||||
|
# row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)')
|
||||||
|
|
||||||
|
# if self.pref_tabs == 'UPDATE':
|
||||||
|
# addon_updater_ops.update_settings_ui(self, context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### --- 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 ---
|
||||||
|
@ -408,63 +774,81 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
GPTB_prefs,
|
GPTB_set_env_settings,
|
||||||
GP_PG_ToolsSettings,
|
GPTB_prefs,
|
||||||
GPT_OT_auto_tint_gp_layers,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# register, unregister = bpy.utils.register_classes_factory(classes)
|
addon_modules = (
|
||||||
|
OP_helpers,
|
||||||
|
OP_pseudo_tint,
|
||||||
|
OP_keyframe_jump,
|
||||||
|
OP_file_checker,
|
||||||
|
OP_breakdowner,
|
||||||
|
OP_temp_cutter,
|
||||||
|
GP_colorize,
|
||||||
|
OP_playblast_bg,
|
||||||
|
OP_playblast,
|
||||||
|
OP_palettes,
|
||||||
|
OP_palettes_linker,
|
||||||
|
OP_brushes,
|
||||||
|
OP_cursor_snap_canvas,
|
||||||
|
OP_copy_paste,
|
||||||
|
# OP_flat_reproject # Disabled,
|
||||||
|
OP_realign,
|
||||||
|
OP_depth_move,
|
||||||
|
OP_key_duplicate_send,
|
||||||
|
OP_layer_namespace,
|
||||||
|
OP_layer_manager,
|
||||||
|
OP_material_picker,
|
||||||
|
OP_git_update,
|
||||||
|
OP_layer_picker,
|
||||||
|
OP_layer_nav,
|
||||||
|
OP_follow_curve,
|
||||||
|
OP_material_move_to_layer,
|
||||||
|
# OP_eraser_brush,
|
||||||
|
# TOOL_eraser_brush, # experimental eraser brush
|
||||||
|
handler_draw_cam,
|
||||||
|
UI_tools,
|
||||||
|
keymaps,
|
||||||
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
addon_updater_ops.register(bl_info)
|
# Register property group first
|
||||||
|
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_cursor_snap_canvas.register()
|
|
||||||
OP_render.register()
|
|
||||||
OP_copy_paste.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()
|
|
||||||
OP_copy_paste.unregister()
|
properties.unregister()
|
||||||
OP_render.unregister()
|
|
||||||
OP_cursor_snap_canvas.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.gptoolprops
|
del bpy.types.Scene.gptoolprops
|
||||||
|
|
||||||
|
|
||||||
|
|
1673
addon_updater.py
1673
addon_updater.py
File diff suppressed because it is too large
Load Diff
1503
addon_updater_ops.py
1503
addon_updater_ops.py
File diff suppressed because it is too large
Load Diff
75
functions.py
75
functions.py
|
@ -6,27 +6,10 @@ 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():
|
def to_bl_image(array, img):
|
||||||
#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):
|
|
||||||
# Write the result to Blender preview
|
# Write the result to Blender preview
|
||||||
width = len(array[0])
|
width = len(array[0])
|
||||||
height = len(array)
|
height = len(array)
|
||||||
|
@ -56,7 +39,7 @@ def to_bl_image(array,img):
|
||||||
image.pixels = output_pixels
|
image.pixels = output_pixels
|
||||||
|
|
||||||
|
|
||||||
def bm_angle_split(bm,angle) :
|
def bm_angle_split(bm, angle) :
|
||||||
bm.verts.ensure_lookup_table()
|
bm.verts.ensure_lookup_table()
|
||||||
loop = link_vert(bm.verts[0],[bm.verts[0]])
|
loop = link_vert(bm.verts[0],[bm.verts[0]])
|
||||||
splitted = []
|
splitted = []
|
||||||
|
@ -83,7 +66,7 @@ def bm_angle_split(bm,angle) :
|
||||||
|
|
||||||
return loops
|
return loops
|
||||||
|
|
||||||
def bm_uniform_density(bm,cam,max_spacing):
|
def bm_uniform_density(bm, cam, max_spacing):
|
||||||
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
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
ratio = scene.render.resolution_y/scene.render.resolution_x
|
ratio = scene.render.resolution_y/scene.render.resolution_x
|
||||||
|
@ -102,7 +85,7 @@ def bm_uniform_density(bm,cam,max_spacing):
|
||||||
return bm
|
return bm
|
||||||
|
|
||||||
|
|
||||||
def gp_stroke_angle_split (frame,strokes,angle):
|
def gp_stroke_angle_split (frame, strokes, angle):
|
||||||
strokes_info = gp_stroke_to_bmesh(strokes)
|
strokes_info = gp_stroke_to_bmesh(strokes)
|
||||||
|
|
||||||
new_strokes = []
|
new_strokes = []
|
||||||
|
@ -116,7 +99,8 @@ def gp_stroke_angle_split (frame,strokes,angle):
|
||||||
|
|
||||||
splitted_loops = bm_angle_split(bm,angle)
|
splitted_loops = bm_angle_split(bm,angle)
|
||||||
|
|
||||||
frame.strokes.remove(stroke_info['stroke'])
|
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||||
|
frame.drawing.strokes.remove(stroke_info['stroke'])
|
||||||
for loop in splitted_loops :
|
for loop in splitted_loops :
|
||||||
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
|
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
|
||||||
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
|
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
|
||||||
|
@ -125,7 +109,7 @@ def gp_stroke_angle_split (frame,strokes,angle):
|
||||||
return new_strokes
|
return new_strokes
|
||||||
|
|
||||||
|
|
||||||
def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
|
def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
|
||||||
strokes_info = gp_stroke_to_bmesh(strokes)
|
strokes_info = gp_stroke_to_bmesh(strokes)
|
||||||
|
|
||||||
new_strokes = []
|
new_strokes = []
|
||||||
|
@ -140,6 +124,7 @@ def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
|
||||||
|
|
||||||
bm_uniform_density(bm,cam,max_spacing)
|
bm_uniform_density(bm,cam,max_spacing)
|
||||||
|
|
||||||
|
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||||
frame.strokes.remove(stroke_info['stroke'])
|
frame.strokes.remove(stroke_info['stroke'])
|
||||||
bm.verts.ensure_lookup_table()
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
@ -152,7 +137,7 @@ def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
|
||||||
return new_strokes
|
return new_strokes
|
||||||
|
|
||||||
|
|
||||||
def along_stroke(stroke,attr,length,min,max) :
|
def along_stroke(stroke, attr, length, min, max) :
|
||||||
strokelen = len(stroke.points)
|
strokelen = len(stroke.points)
|
||||||
for index,point in enumerate(stroke.points) :
|
for index,point in enumerate(stroke.points) :
|
||||||
value = getattr(point,attr)
|
value = getattr(point,attr)
|
||||||
|
@ -164,9 +149,9 @@ def along_stroke(stroke,attr,length,min,max) :
|
||||||
remap = remapping((strokelen-index)/length,0,1,min,max)
|
remap = remapping((strokelen-index)/length,0,1,min,max)
|
||||||
setattr(point,attr,value*remap)
|
setattr(point,attr,value*remap)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -182,8 +167,7 @@ def randomise_points(mat,points,attr,strength) :
|
||||||
setattr(point,attr,value+random*strength)
|
setattr(point,attr,value+random*strength)
|
||||||
|
|
||||||
|
|
||||||
|
def zoom_to_object(cam, resolution, box, margin=0.01) :
|
||||||
def zoom_to_object(cam,resolution,box,margin=0.01) :
|
|
||||||
min_x= box[0]
|
min_x= box[0]
|
||||||
max_x= box[1]
|
max_x= box[1]
|
||||||
min_y= box[2]
|
min_y= box[2]
|
||||||
|
@ -208,7 +192,7 @@ def zoom_to_object(cam,resolution,box,margin=0.01) :
|
||||||
bpy.context.scene.objects.link(zoom_cam)
|
bpy.context.scene.objects.link(zoom_cam)
|
||||||
|
|
||||||
|
|
||||||
resolution = (int(resolution[0]*factor),int(resolution[1]*factor))
|
resolution = (int(resolution[0]*factor), int(resolution[1]*factor))
|
||||||
|
|
||||||
|
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
|
@ -233,29 +217,8 @@ def zoom_to_object(cam,resolution,box,margin=0.01) :
|
||||||
#print(matrix,resolution)
|
#print(matrix,resolution)
|
||||||
return modelview_matrix,projection_matrix,frame,resolution
|
return modelview_matrix,projection_matrix,frame,resolution
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_viewport_matrix(width,height,mat):
|
|
||||||
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
|
|
||||||
|
|
||||||
glViewport(0,0,width,height)
|
|
||||||
|
|
||||||
#glLoadIdentity()
|
|
||||||
|
|
||||||
glMatrixMode(GL_PROJECTION)
|
|
||||||
|
|
||||||
projection = [mat[j][i] for i in range(4) for j in range(4)]
|
|
||||||
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
|
|
||||||
|
|
||||||
#glMatrixMode( GL_MODELVIEW )
|
|
||||||
#glLoadIdentity()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# get object info
|
# get object info
|
||||||
def get_object_info(mesh_groups,order_list = []) :
|
def get_object_info(mesh_groups, order_list = []) :
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
cam = scene.camera
|
cam = scene.camera
|
||||||
#scale = scene.render.resolution_percentage / 100.0
|
#scale = scene.render.resolution_percentage / 100.0
|
||||||
|
@ -378,4 +341,10 @@ def get_object_info(mesh_groups,order_list = []) :
|
||||||
scene.render.resolution_y = res_y
|
scene.render.resolution_y = res_y
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import bpy
|
||||||
|
import gpu
|
||||||
|
# import blf
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||||
|
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
|
|
||||||
|
def extrapolate_points_by_length(a, b, length):
|
||||||
|
'''
|
||||||
|
Return a third point C from by continuing in AB direction
|
||||||
|
Length define BC distance. both vector2 and vector3
|
||||||
|
'''
|
||||||
|
# return b + ((b - a).normalized() * length)# one shot
|
||||||
|
ab = b - a
|
||||||
|
if not ab:
|
||||||
|
return None
|
||||||
|
return b + (ab.normalized() * length)
|
||||||
|
|
||||||
|
def view3d_camera_border_2d(context, cam):
|
||||||
|
# based on https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
|
||||||
|
# cam = context.scene.camera
|
||||||
|
frame = cam.data.view_frame(scene=context.scene)
|
||||||
|
# to world-space
|
||||||
|
frame = [cam.matrix_world @ v for v in frame]
|
||||||
|
# to pixelspace
|
||||||
|
region, rv3d = context.region, context.space_data.region_3d
|
||||||
|
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
|
||||||
|
return frame_px
|
||||||
|
|
||||||
|
def vertices_to_line_loop(v_list, closed=True) -> list:
|
||||||
|
'''Take a sequence of vertices
|
||||||
|
return a position lists of segments to create a line loop passing in all points
|
||||||
|
the result is usable with gpu_shader 'LINES'
|
||||||
|
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
|
||||||
|
'''
|
||||||
|
loop = []
|
||||||
|
for i in range(len(v_list) - 1):
|
||||||
|
loop += [v_list[i], v_list[i + 1]]
|
||||||
|
if closed:
|
||||||
|
# Add segment between last and first to close loop
|
||||||
|
loop += [v_list[-1], v_list[0]]
|
||||||
|
return loop
|
||||||
|
|
||||||
|
def draw_cam_frame_callback_2d():
|
||||||
|
context = bpy.context
|
||||||
|
if context.region_data.view_perspective != 'CAMERA':
|
||||||
|
return
|
||||||
|
if not context.scene.camera or context.scene.camera.name != 'draw_cam':
|
||||||
|
return
|
||||||
|
|
||||||
|
main_cam = context.scene.camera.parent
|
||||||
|
if not main_cam:
|
||||||
|
return
|
||||||
|
|
||||||
|
gpu.state.blend_set('ALPHA')
|
||||||
|
|
||||||
|
frame_point = view3d_camera_border_2d(
|
||||||
|
context, context.scene.camera.parent)
|
||||||
|
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
|
||||||
|
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||||
|
|
||||||
|
if context.scene.gptoolprops.drawcam_passepartout:
|
||||||
|
### PASSEPARTOUT
|
||||||
|
|
||||||
|
# frame positions
|
||||||
|
#
|
||||||
|
# lupext lup rup rupext
|
||||||
|
# D-----A
|
||||||
|
# | |
|
||||||
|
# C-----B
|
||||||
|
# ldnext ldn rdn lnext
|
||||||
|
|
||||||
|
a = frame_point[0]
|
||||||
|
b = frame_point[1]
|
||||||
|
c = frame_point[2]
|
||||||
|
d = frame_point[3]
|
||||||
|
|
||||||
|
ext = 10000
|
||||||
|
rup = extrapolate_points_by_length(b,a, ext)
|
||||||
|
rdn = extrapolate_points_by_length(a,b, ext)
|
||||||
|
rupext = rup + ((a-d).normalized() * ext)
|
||||||
|
rdnext = rdn + ((a-d).normalized() * ext)
|
||||||
|
lup = extrapolate_points_by_length(c,d, ext)
|
||||||
|
ldn = extrapolate_points_by_length(d,c, ext)
|
||||||
|
lupext = lup + ((c-b).normalized() * ext)
|
||||||
|
ldnext = ldn + ((c-b).normalized() * ext)
|
||||||
|
|
||||||
|
|
||||||
|
# ppp_color = (1.0, 1.0, 0.0, 1)
|
||||||
|
ppp_color = (0.0, 0.0, 0.0, context.scene.camera.data.passepartout_alpha)
|
||||||
|
|
||||||
|
rect = [rup, rdn, rupext, rdnext,
|
||||||
|
lup, ldn, lupext, ldnext,
|
||||||
|
a, b, c, d]
|
||||||
|
|
||||||
|
## convert to 2d
|
||||||
|
# region, rv3d = context.region, context.space_data.region_3d
|
||||||
|
# rect = [location_3d_to_region_2d(region, rv3d, v) for v in rect]
|
||||||
|
|
||||||
|
# lupext=6 lup=4 rup=0 rupext=2
|
||||||
|
# d=11, a=8
|
||||||
|
# c=10, b=9
|
||||||
|
# ldnext=7 ldn=5 rdn=1 dnpext=3
|
||||||
|
|
||||||
|
indices = [(0,1,2), (1,2,3),
|
||||||
|
(4,5,6), (5,6,7),
|
||||||
|
(8,11,4), (8,4,0),
|
||||||
|
(10,9,5), (9,5,1),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ### passpartout_points
|
||||||
|
|
||||||
|
passepartout = batch_for_shader(
|
||||||
|
shader_2d, 'TRIS', {"pos": rect}, indices=indices)
|
||||||
|
# shader_2d, 'LINE_LOOP', {"pos": rect})
|
||||||
|
|
||||||
|
shader_2d.bind()
|
||||||
|
shader_2d.uniform_float("color", ppp_color)
|
||||||
|
passepartout.draw(shader_2d)
|
||||||
|
|
||||||
|
|
||||||
|
### Camera framing trace over
|
||||||
|
|
||||||
|
gpu.state.line_width_set(1.0)
|
||||||
|
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||||
|
|
||||||
|
"""
|
||||||
|
## need to accurately detect viewport background color (difficult)
|
||||||
|
### COUNTER CURRENT FRAMING
|
||||||
|
if False:
|
||||||
|
camf = view3d_camera_border_2d(
|
||||||
|
context, context.scene.camera)
|
||||||
|
ca = (camf[0].x + 1, camf[0].y + 1)
|
||||||
|
cb = (camf[1].x + 1, camf[1].y - 1)
|
||||||
|
cc = (camf[2].x - 1, camf[2].y - 1)
|
||||||
|
cd = (camf[3].x - 1, camf[3].y + 1)
|
||||||
|
screen_framing = batch_for_shader(
|
||||||
|
shader_2d, 'LINE_LOOP', {"pos": [ca, cb, cc, cd]})
|
||||||
|
shader_2d.bind()
|
||||||
|
shader_2d.uniform_float("color", (0.25, 0.25, 0.25, 1.0))
|
||||||
|
# shader_2d.uniform_float("color", (0.9, 0.9, 0.9, 1.0))
|
||||||
|
screen_framing.draw(shader_2d)
|
||||||
|
"""
|
||||||
|
### FRAMING
|
||||||
|
|
||||||
|
# frame_color = (0.06, 0.4, 0.040, 0.5)
|
||||||
|
frame_color = (0.0, 0.0, 0.25, 1.0)
|
||||||
|
|
||||||
|
screen_framing = batch_for_shader(
|
||||||
|
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
|
||||||
|
|
||||||
|
shader_2d.bind()
|
||||||
|
shader_2d.uniform_float("color", frame_color)
|
||||||
|
screen_framing.draw(shader_2d)
|
||||||
|
|
||||||
|
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||||
|
gpu.state.blend_set('NONE')
|
||||||
|
|
||||||
|
|
||||||
|
draw_handle = None
|
||||||
|
|
||||||
|
def register():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
global draw_handle
|
||||||
|
draw_handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||||
|
draw_cam_frame_callback_2d, (), "WINDOW", "POST_PIXEL")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
if bpy.app.background:
|
||||||
|
return
|
||||||
|
|
||||||
|
global draw_handle
|
||||||
|
if draw_handle:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(draw_handle, 'WINDOW')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
|
@ -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)
|
||||||
|
|
|
@ -1,23 +1,165 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
from bpy.types import PropertyGroup
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
IntProperty,
|
IntProperty,
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
StringProperty,
|
StringProperty,
|
||||||
FloatProperty,
|
FloatProperty,
|
||||||
|
EnumProperty,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .OP_cursor_snap_canvas import cursor_follow_update
|
from .OP_cursor_snap_canvas import cursor_follow_update
|
||||||
|
from .OP_layer_manager import layer_name_build
|
||||||
|
|
||||||
def change_edit_lines_opacity(self, context):
|
def change_edit_lines_opacity(self, context):
|
||||||
# for o in context.scene.objects:
|
|
||||||
# if o.type != 'GPENCIL':
|
|
||||||
# continue
|
|
||||||
# o.data.edit_line_color[3]=self.edit_lines_opacity
|
|
||||||
for gp in bpy.data.grease_pencils:
|
for gp in bpy.data.grease_pencils:
|
||||||
if not gp.is_annotation:
|
if not gp.is_annotation:
|
||||||
gp.edit_line_color[3]=self.edit_lines_opacity
|
gp.edit_line_color[3]=self.edit_lines_opacity
|
||||||
|
|
||||||
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
|
|
||||||
|
def update_layer_name(self, context):
|
||||||
|
if not self.layer_name:
|
||||||
|
# never replace by nothing (since there should be prefix/suffix)
|
||||||
|
return
|
||||||
|
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||||
|
return
|
||||||
|
if not context.object.data.layers.active:
|
||||||
|
return
|
||||||
|
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
|
||||||
|
# context.object.data.layers.active.name = self.layer_name
|
||||||
|
|
||||||
|
|
||||||
|
class GP_PG_FixSettings(PropertyGroup):
|
||||||
|
|
||||||
|
|
||||||
|
lock_main_cam : BoolProperty(
|
||||||
|
name="Lock Main Cam",
|
||||||
|
description="Lock the main camera (works only if 'layout' is not in name)",
|
||||||
|
default=False, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_scene_res : BoolProperty(
|
||||||
|
name="Reset Scene Resolution",
|
||||||
|
description="Set the scene resolution to current prefs project settings",
|
||||||
|
default=False, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_res_percentage : BoolProperty(
|
||||||
|
name="Reset Resolution Percentage To 100%",
|
||||||
|
description="",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_fps : BoolProperty(
|
||||||
|
name="Reset Fps",
|
||||||
|
description="Set the framerate of the scene to current prefs project settings",
|
||||||
|
default=False, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_slider_n_sync : BoolProperty(
|
||||||
|
name="Dopesheets Show Slider And Sync Range",
|
||||||
|
description="Toggle on the use of show slider and sync range",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_gp_use_lights_off : BoolProperty(
|
||||||
|
name="Set Off Use lights On All Gpencil Objects",
|
||||||
|
description="Uncheck Use lights on all grease pencil objects\nAt object level, not layer level (Object properties > Visibility > GP uselight)",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
check_front_axis : BoolProperty(
|
||||||
|
name="Check If Draw Axis is Front (X-Z)",
|
||||||
|
description="Alert if the current grease pencil draw axis is not front (X-Z)",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
check_placement : BoolProperty(
|
||||||
|
name="Check Stroke Placement",
|
||||||
|
description="Alert if the current grease pencil stroke placement is not Origin",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
set_pivot_median_point : BoolProperty(
|
||||||
|
name="Set Pivot To Median Point",
|
||||||
|
description="Change the pivot point to the most used median point",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
disable_guide : BoolProperty(
|
||||||
|
name="Disable Drawing Guide",
|
||||||
|
description="Disable constrained guide in draw mode",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
list_disabled_anim : BoolProperty(
|
||||||
|
name="List Disabled Animation",
|
||||||
|
description="Alert if there are disabled animations",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
list_obj_vis_conflict : BoolProperty(
|
||||||
|
name="List Object Visibility Conflicts",
|
||||||
|
description="Alert if some objects have different hide viewport and hide render values",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
list_gp_mod_vis_conflict : BoolProperty(
|
||||||
|
name="List GP Object Modifiers Visibility Conflicts",
|
||||||
|
description="Alert if some GP modifier have different show viewport and show render values",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
list_broken_mod_targets : BoolProperty(
|
||||||
|
name="List GP Object modifiers broken target",
|
||||||
|
description="Alert if some GP modifier have a target layer not existing in layer stack",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
autokey_add_n_replace : BoolProperty(
|
||||||
|
name="Autokey Mode Add and Replace",
|
||||||
|
description="Change autokey mode back to 'Add & Replace'",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
remove_redundant_strokes : BoolProperty(
|
||||||
|
name="Remove Redundant Strokes",
|
||||||
|
description="Remove GP strokes duplication. When points are exactly identical within the same frame",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
# set_cursor_type : BoolProperty(
|
||||||
|
# name="Set Select Cursor Mode",
|
||||||
|
# description="Set the type of the selection cursor (according to addon prefs)",
|
||||||
|
# default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
## default active tool to use
|
||||||
|
select_active_tool : EnumProperty(
|
||||||
|
name="Set Default Selection Tool", description="Active tool to set when launching check fix scene",
|
||||||
|
default='builtin.select_lasso',
|
||||||
|
items=(
|
||||||
|
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
|
||||||
|
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
|
||||||
|
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
|
||||||
|
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
|
||||||
|
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
|
||||||
|
))
|
||||||
|
|
||||||
|
## check file path mapping type
|
||||||
|
file_path_type : EnumProperty(
|
||||||
|
name="Check File Path Mapping", description="Check if all path in scene are relative, absolute or disable the check",
|
||||||
|
default='RELATIVE',
|
||||||
|
items=(
|
||||||
|
('none', 'No Check', 'Do not check for path type', 0),
|
||||||
|
('RELATIVE', 'Relative', 'Check if all path are relative', 1),
|
||||||
|
('ABSOLUTE', 'Absolute', 'Check if all path are absolute', 2),
|
||||||
|
))
|
||||||
|
|
||||||
|
## check set lock object mode
|
||||||
|
lock_object_mode : EnumProperty(
|
||||||
|
name="Set Lock Object Mode", description="Set 'Lock object mode' parameter check'",
|
||||||
|
default='none',
|
||||||
|
items=(
|
||||||
|
('none', 'Do nothing', 'Do not set', 0),
|
||||||
|
('LOCK', 'Lock object mode', 'Toggle lock object mode On', 1),
|
||||||
|
('UNLOCK', 'Unlock object mode', 'Toggle lock object mode Off', 2),
|
||||||
|
))
|
||||||
|
|
||||||
|
class GP_PG_ToolsSettings(PropertyGroup):
|
||||||
|
eraser_radius : IntProperty(
|
||||||
|
name="Eraser Radius", description="Radius of eraser brush",
|
||||||
|
default=20, min=0, max=500, subtype='PIXEL')
|
||||||
|
|
||||||
|
drawcam_passepartout : BoolProperty(
|
||||||
|
name="Show cam passepartout",
|
||||||
|
description="Show a darkened overlay outside image area in Camera view",
|
||||||
|
default=True,
|
||||||
|
options={'HIDDEN'})
|
||||||
|
|
||||||
autotint_offset : IntProperty(
|
autotint_offset : IntProperty(
|
||||||
name="Tint hue offset", description="offset the tint by this value for better color",
|
name="Tint hue offset", description="offset the tint by this value for better color",
|
||||||
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1,
|
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1,
|
||||||
|
@ -36,14 +178,38 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
|
||||||
name='Cursor Follow', description="3D cursor follow active object animation when activated",
|
name='Cursor Follow', description="3D cursor follow active object animation when activated",
|
||||||
default=False, update=cursor_follow_update)
|
default=False, update=cursor_follow_update)
|
||||||
|
|
||||||
edit_lines_opacity : FloatProperty(
|
cursor_follow_target : bpy.props.PointerProperty(
|
||||||
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils", default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)#, get=None, set=None
|
name='Cursor Follow Target',
|
||||||
|
description="Optional target object to follow for cursor instead of active object",
|
||||||
|
type=bpy.types.Object, update=cursor_follow_update)
|
||||||
|
|
||||||
|
## gpv3 : no edit line color anymore
|
||||||
|
# edit_lines_opacity : FloatProperty(
|
||||||
|
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
|
||||||
|
# default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
|
||||||
|
|
||||||
## render
|
## render
|
||||||
name_for_current_render : StringProperty(
|
name_for_current_render : StringProperty(
|
||||||
name="Render_name", description="Name use for render current",
|
name="Render_name", description="Name use for render current",
|
||||||
default="")
|
default="")
|
||||||
|
|
||||||
|
keyframe_type : EnumProperty(
|
||||||
|
name="Keyframe Filter", description="Only jump to defined keyframe type",
|
||||||
|
default='ALL', options={'HIDDEN', 'SKIP_SAVE'},
|
||||||
|
items=(
|
||||||
|
('ALL', 'All', '', 0), # 'KEYFRAME'
|
||||||
|
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 1),
|
||||||
|
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 2),
|
||||||
|
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 3),
|
||||||
|
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 4),
|
||||||
|
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
|
||||||
|
))
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -65,3 +231,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)
|
|
@ -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)
|
Loading…
Reference in New Issue