Compare commits
139 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 |
517
CHANGELOG.md
517
CHANGELOG.md
|
@ -1,5 +1,412 @@
|
|||
# Changelog
|
||||
|
||||
4.0.3
|
||||
|
||||
changed: File checker doest not fix directly when clicked (also removed choice in preference):
|
||||
- list potential change and display an `Apply Fix`
|
||||
changed: Enhanced visibility conflict list:
|
||||
- also include viewlayer hide value
|
||||
- allow to set all hide value from the state of one of the three
|
||||
- fixed: material move operator
|
||||
|
||||
4.0.1
|
||||
|
||||
- fixed: layer nav operator on page up/down
|
||||
|
||||
4.0.0
|
||||
|
||||
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
|
||||
|
||||
3.3.0
|
||||
|
||||
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
|
||||
|
||||
3.2.0
|
||||
|
||||
- added: UI settings to show GP tool settings placement and orientation
|
||||
- fixed: Bug with reproject orientation settings
|
||||
- added: show current orientation in batch reproject popup UI (if current is selected)
|
||||
|
||||
3.1.0
|
||||
|
||||
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
|
||||
|
||||
3.0.2
|
||||
|
||||
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
|
||||
|
||||
3.0.1
|
||||
|
||||
- fixed: Crash after generating empty frames
|
||||
|
||||
3.0.0
|
||||
|
||||
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
|
||||
- fixed: openGL draw camera frame and passepartout
|
||||
|
||||
2.5.0
|
||||
|
||||
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
|
||||
|
||||
2.4.0
|
||||
|
||||
- changed: Batch reproject consider camera movement and is almost 8x faster
|
||||
- added: Batch reproject have "Current" mode (using current tool setting)
|
||||
|
||||
2.3.4
|
||||
|
||||
- fixed: bug when exporting json palettes containing empty material slots
|
||||
|
||||
2.3.3
|
||||
|
||||
- fixed: Bug with animation manager objects data
|
||||
|
||||
2.3.2
|
||||
|
||||
- fixed: Bug with animation manager when there is empty object in scene
|
||||
|
||||
2.3.1
|
||||
|
||||
- changed: Animation manager show hints when animation is enabled: fully, partially or not at all.
|
||||
|
||||
2.3.0
|
||||
|
||||
- added: Animation manager buttons are colored red when objects have disabled animation
|
||||
- fixed: Animation manager not enabling/disabling Action Groups
|
||||
- fixed: Animation manager `List Disabled Anims` list groups as well
|
||||
|
||||
2.2.3
|
||||
|
||||
- fixed: Type error on realign ops
|
||||
|
||||
2.2.2
|
||||
|
||||
- fixed: draw_cam data not changed when working with multiple camera in a shot
|
||||
|
||||
2.2.1
|
||||
|
||||
- added: class View3D to calculate area 3d related coordinates
|
||||
|
||||
2.2.0
|
||||
|
||||
- added: _Remove redundant stroke_ in File checker (Just list duplicate numbers in "check only" mode)
|
||||
|
||||
2.1.6
|
||||
|
||||
- fixed: Prevent some keymaps to register when blender is launched in background mode
|
||||
|
||||
2.1.5
|
||||
|
||||
- fixed: false positive with checkfile in modifier layer target
|
||||
|
||||
2.1.4
|
||||
|
||||
- fixed: layer change msgbus not working at first activation of the addon
|
||||
|
||||
2.1.3
|
||||
|
||||
- fixed: decoralate Prefix and Suffix UI_lists scroll
|
||||
- fixed: Problem when settings project namespaces
|
||||
- added: Button to reset project namespace (to have the right order)
|
||||
|
||||
2.1.2
|
||||
|
||||
- added: `gp.brush_set` operator to manually assign a brush by name to a shortcut
|
||||
- preferably add shortcut to `Grease Pencil > Grease Pencil Stroke Paint Mode`
|
||||
|
||||
2.1.1
|
||||
|
||||
- added: follow curve show offset property in UI
|
||||
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||
- changed: created follow curve use `fixed offset`
|
||||
|
||||
2.1.1
|
||||
|
||||
- added: follow curve show offset property in UI
|
||||
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||
- changed: created follow curve use `fixed offset`
|
||||
|
||||
2.1.0
|
||||
|
||||
- added: 3 actions buttons:
|
||||
- create curve with follow path and go into curve edit
|
||||
- go back to object
|
||||
- got to curve edit (if follow path constraint exists with a curve target)
|
||||
- if follow path exists, button to remove constraint
|
||||
|
||||
2.0.11
|
||||
|
||||
- fix: prefix set by project environment
|
||||
|
||||
2.0.10
|
||||
|
||||
- fix: poll error in console
|
||||
|
||||
2.0.9
|
||||
|
||||
- fix: prefix/suffix UIlist actions trigger UI redraw to see changes live
|
||||
- changed: PropertyGroups are now registered in their own file
|
||||
- code: cleanup
|
||||
|
||||
2.0.8
|
||||
|
||||
- changed: suffix as UIlist in prefs,
|
||||
- fix: prefix and suffix register and load
|
||||
|
||||
2.0.7
|
||||
|
||||
- fix: broken auto-fade with gp layer navigation when used with a customized shortcut.
|
||||
- changed: supported version number to 3.0.0
|
||||
|
||||
2.0.6
|
||||
|
||||
- changed: Use prefixes toggle is enabled by default
|
||||
- changed: prefixes are now set in preferences as a reorderable UI list with full name for description
|
||||
- This should not affect `PREFIXES` env variable (`'CO, LN'`)
|
||||
- Now can be also passed as prefix:name pair ex: `'CO : Color, LN : Line`
|
||||
|
||||
2.0.5
|
||||
|
||||
- changed: redo panel for GP layer picker has better name and display active layer name
|
||||
- changed: some operator id_name to expose "add shortcut" in context menu for some button
|
||||
- fix: error with draw_cam handler when no camera is active
|
||||
|
||||
2.0.3
|
||||
|
||||
- changed: `X-ray` to `In Front`, match original object property name
|
||||
|
||||
2.0.2
|
||||
|
||||
- added: possibility to select which point attribute is copied by GP layer copy to clipboard
|
||||
|
||||
2.0.1
|
||||
|
||||
- added: enable/disable camera animation separately in `animation manager`
|
||||
- for cam,disable properties like focal and shift (to be able to work on a fixed cam)
|
||||
- camera is not disable using `obj` anymore
|
||||
|
||||
2.0.0
|
||||
|
||||
- fix: error using playblast (api changes of Blender 3.0+)
|
||||
- added: possibility to change playblast destination in preferences
|
||||
- changed: playblast default is `playblast` folder instead of `images`
|
||||
- changed: completely disable experimental precise eraser
|
||||
|
||||
1.9.9
|
||||
|
||||
- fix: Bug setting GP layers actions on/off
|
||||
- fix: Add GP layers animation datas in `list disabled animation` report
|
||||
|
||||
1.9.8
|
||||
|
||||
- fix: animation manager `GP anim on/off` also toggle layers animation datas
|
||||
|
||||
1.9.7
|
||||
|
||||
- changed: `list modifiers visibility conflict` (used in `check file` or through search menu) now not limited to GP objects type.
|
||||
|
||||
1.9.6
|
||||
|
||||
- fix: icon removed in 3.0
|
||||
|
||||
1.9.5
|
||||
|
||||
- added: `Check file` also check if GP modifiers have broken layer target
|
||||
- changed: `Check file` disable drawing guide is now On by default
|
||||
|
||||
1.9.4
|
||||
|
||||
- feat: renaming using the name_manager take care of changing layer target values in GP modifiers
|
||||
|
||||
1.9.3
|
||||
|
||||
- feat: Add an update button at bottom of preferences if addon is a cloned repository
|
||||
|
||||
1.9.2
|
||||
|
||||
- feat: Palette linker has a name exclusion list in preferences
|
||||
- Avoid linking some material that are prefered local to file
|
||||
- Default item in list : `line`
|
||||
|
||||
1.9.1
|
||||
|
||||
- fix: add error handling on palette linker when blend_path isn't valid anymore
|
||||
- added: file checker entry, Disable all GP object `use lights` (True by default in addon pref `checklist`)
|
||||
- added: WIP of Batch reproject all on cursor.
|
||||
- No UI. In search menu `Flat Reproject Selected On cursor` (idname `gp.batch_flat_reproject`)
|
||||
|
||||
1.9.0
|
||||
|
||||
- feat: New shortcuts:
|
||||
- `F2` in Paint and Edit to rename active layer
|
||||
- `Insert` add a new layer (same as Krita)
|
||||
- `Shift + Insert` add a new layer and immediately pop-up a rename box
|
||||
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
|
||||
- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut
|
||||
|
||||
1.8.1
|
||||
|
||||
- fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore
|
||||
|
||||
1.8.0
|
||||
|
||||
- feat: palette linker (beta), with pop-up from material stack dropdown
|
||||
- feat: palette name fuzzy match
|
||||
- code: add an open addon preference ops
|
||||
|
||||
1.7.8
|
||||
|
||||
- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)
|
||||
|
||||
1.7.7
|
||||
|
||||
- feat: add copy path to `check link` ops with multiple path representation choices
|
||||
|
||||
1.7.6
|
||||
|
||||
- ui: expose (almost) all keymap added by the addon to allow user for customize/disable as needed
|
||||
- ui: changed some names
|
||||
|
||||
1.7.5
|
||||
|
||||
- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
|
||||
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
|
||||
- add undo step for `W`'s select layer from closest stroke
|
||||
|
||||
1.7.4
|
||||
|
||||
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
||||
- fix: copy-paste keymap error on background rendering
|
||||
|
||||
1.7.3
|
||||
|
||||
- added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`)
|
||||
|
||||
1.7.2
|
||||
|
||||
- added: `Object visibility conflict` in file check
|
||||
- print in console when object have render activated but not viewport (& vice-versa)
|
||||
- Standalone ops "List Object Visibility Conflicts" (`gp.list_object_visibility`)
|
||||
- added: `GP Modifier visibility conflict` in file check.
|
||||
- print in console when gp modifiers have render activated but not viewport (& vice-versa)
|
||||
- Standalone ops "List GP Modifiers Visibility Conflicts" (`gp.list_modifier_visibility`)
|
||||
- code: show_message_box utils can now receive operator in sublist (if 3 element)
|
||||
|
||||
1.7.1
|
||||
|
||||
- feat: Improved `Create Empty Frames` operator with mutliple filters to choose source layers
|
||||
|
||||
1.7.0
|
||||
|
||||
- removed: Obsolete operators and panels
|
||||
- Remove "line closer" panel as it's been a native tool for a while in 2.9x (stop register of `GP_guided_colorize > OP_line_closer`)
|
||||
- Remove "Render" subpanel, obsolete and not adapted to production
|
||||
- ui: clean and refactor
|
||||
- Gp clipboard panel and aimation Manager subpanel layout to aligned columns (gain space)
|
||||
- add `GP` on each panel name for wuick eye search
|
||||
- follow cursor now in animation manager subpanel (should be in an extra menu or removed in the end)
|
||||
|
||||
1.6.9
|
||||
|
||||
- change: material picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
|
||||
- this is made to let other operator use functionality on long press using `S`
|
||||
- feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all)
|
||||
- by default `S` is still fill only
|
||||
- but `Alt+S` is now stroke only instead of all
|
||||
|
||||
1.6.8
|
||||
|
||||
- fix: reproject GP repeating projection on same frames
|
||||
- fix: batch reproject all frame not poping dialog menu
|
||||
|
||||
1.6.7
|
||||
|
||||
- fix: error with operator `OP_key_duplicate_send` poll flooding console
|
||||
|
||||
1.6.6
|
||||
|
||||
- fix: inverted scene resolution from project environnement
|
||||
|
||||
1.6.5
|
||||
|
||||
- feat: check canvas alignement of the Gp object compare to chosen draw axis
|
||||
|
||||
1.6.4
|
||||
|
||||
- fix: disable multi-selection for layer naming manager
|
||||
- the dopesheet selection sometimes still consider layer as selected
|
||||
|
||||
1.6.3
|
||||
|
||||
<!-- - abort :: add checkbox to prevent from loading project environnement namespace -->
|
||||
- add buttons to load manually environnement namespace
|
||||
|
||||
1.6.2
|
||||
|
||||
- disable keymap register for breakdowner on background
|
||||
|
||||
1.6.1
|
||||
|
||||
- removed: Auto Updater that was failing since 2.93
|
||||
- prefs: add a checkbox to disable the "load base palette button" in UI.
|
||||
|
||||
1.6.0
|
||||
|
||||
- feat: Namespace upgrade
|
||||
- support pseudo group naming
|
||||
- add group and indent button
|
||||
- feat: Fill tool shortcut for material color picker (from closest visible stroke)
|
||||
- `S` : get material closest *fill* stroke
|
||||
- `Àlt+S` : get closest stroke (fill or stroke)
|
||||
|
||||
1.5.8
|
||||
|
||||
- feat: Namespace improvement:
|
||||
- new suffixes list that generate suffix buttons
|
||||
- dynamic layer name field that show active (msgbus)
|
||||
- possible to disable the panel with an option
|
||||
|
||||
1.5.7
|
||||
|
||||
- feat: check list, specify in addon pref if you prefer a dry run (check without set)
|
||||
- feat: check file can change the lock object mode state (do nothing by default)
|
||||
|
||||
1.5.6
|
||||
|
||||
- feat: layer drop-down menu include an operator for batch find/replace GP layers (idname: `gp.rename_gp_layers`)
|
||||
|
||||
1.5.5
|
||||
|
||||
- feat: check file: add check for filepath mapping (if all relative or all absolute)
|
||||
- change: check file: disable resolution set by default
|
||||
|
||||
1.5.4
|
||||
|
||||
- feat: Layer manager
|
||||
- select/set layer prefix
|
||||
- select/set layer color
|
||||
- code: refactor name builder function
|
||||
|
||||
1.5.3
|
||||
|
||||
- feat: layer aquick-prefix for layer using pref separator
|
||||
- list editable in addon pref
|
||||
- add interface button above layers
|
||||
- code: added environnement variable for prefix and separator:
|
||||
- `PREFIXES` : list of prefix (comma separated uppercase letters between 1 and 6 character, ex: 'AN,SP,L')
|
||||
- `SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
|
||||
- UI: add addon prefs namespace ui-box in project settings
|
||||
|
||||
|
||||
1.5.2
|
||||
|
||||
- add environnement variables to set addon preferences project settings at register through `os.getenv('KEY')`:
|
||||
- `RENDER_WIDTH` : resolution x
|
||||
- `RENDER_HEIGHT` : resolution y
|
||||
- `FPS` : project frame rate
|
||||
- `PALETTES` : path to the blends (or json) containing materials palettes
|
||||
- `BRUSHES` : path to the blend containing brushes to load
|
||||
|
||||
1.5.1
|
||||
|
||||
- fix: eraser brush
|
||||
|
@ -38,29 +445,29 @@
|
|||
|
||||
- feat: show main cam frame when in draw_cam
|
||||
|
||||
1.3.2:
|
||||
1.3.2
|
||||
|
||||
- change: disable manip cam name drawing
|
||||
- code: add initial support for main cam frame draw within camera view
|
||||
|
||||
1.3.1:
|
||||
1.3.1
|
||||
|
||||
- fix: native refresh error that rarely happen that doesn't completely refresh the scene on keyframe jump.
|
||||
- Use a double frame change to ensure refresh.
|
||||
|
||||
1.3.0:
|
||||
1.3.0
|
||||
|
||||
- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
|
||||
|
||||
1.2.2:
|
||||
1.2.2
|
||||
|
||||
- fix: realign anim return error
|
||||
|
||||
1.2.1:
|
||||
1.2.1
|
||||
|
||||
- fix: Breakdowner initial error check
|
||||
|
||||
1.2.0:
|
||||
1.2.0
|
||||
|
||||
- feat: New depth move operator that handle both perspective and orthographic cam
|
||||
- feat: Realign, added drawing plane checkbox to autoset to Front after realign
|
||||
|
@ -69,67 +476,67 @@
|
|||
- doc: Added changelog file (moved list from readme)
|
||||
- doc: relative link to changelog and FR_readme in main readme
|
||||
|
||||
1.1.0:
|
||||
1.1.0
|
||||
|
||||
- Important change : Remap relative is now disabled by default in addon preferences
|
||||
- feat: Add realign operator in sidebar with reproject as true by default
|
||||
- UI: Batch reproject all frames is now in menus. Same places as native reproject
|
||||
|
||||
1.0.9:
|
||||
1.0.9
|
||||
|
||||
- feat: Reproject all frames operator
|
||||
|
||||
1.0.8:
|
||||
1.0.8
|
||||
|
||||
- feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified
|
||||
|
||||
1.0.7:
|
||||
1.0.7
|
||||
|
||||
- feat: Keyframe jump filter by type. User can now choose if the shortcut should jump on a specific keyframe type (All by default)
|
||||
|
||||
1.0.5:
|
||||
1.0.5
|
||||
|
||||
- GP copy-paste : Pasted stroke are now selected (allow to use it to quickly rip/split strokes with cut/paste on the same layer)
|
||||
|
||||
1.0.4:
|
||||
1.0.4
|
||||
|
||||
- UI: Better cam ref exposition in Toolbox panel
|
||||
- Access to opacity
|
||||
- merge activation bool with source type icon
|
||||
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps)
|
||||
|
||||
1.0.3:
|
||||
1.0.3
|
||||
|
||||
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
|
||||
|
||||
1.0.2:
|
||||
1.0.2
|
||||
|
||||
- pref: Added option to disable always remap relative on save in addon-preference
|
||||
|
||||
1.0.1:
|
||||
1.0.1
|
||||
|
||||
- fix: copy paste problems
|
||||
- Get points uv_properties (used for brushed points)
|
||||
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
|
||||
|
||||
1.0.0:
|
||||
1.0.0
|
||||
|
||||
- Compatible with official grease pencil tools
|
||||
- removed box deform and rotate canvas that existed in other
|
||||
|
||||
0.9.3:
|
||||
0.9.3
|
||||
|
||||
- feat: keyframe jump keys are now auto-binded
|
||||
- UI: added keyframe jump customisation in addon pref
|
||||
- code: split keyframe jump in a separate file with his new key updater
|
||||
|
||||
0.9.2:
|
||||
0.9.2
|
||||
|
||||
- doc: Correct download link (important, bugged the addon install) + update
|
||||
- code: added tracker url
|
||||
- updater: remove updater temp file, reset minimum version, turn off verbose mode
|
||||
|
||||
0.9.1:
|
||||
0.9.1
|
||||
|
||||
- Public release
|
||||
- prefs: added fps as part of project settings
|
||||
|
@ -139,38 +546,38 @@
|
|||
- doc: Added fully-detailed french readme
|
||||
|
||||
|
||||
0.8.0:
|
||||
0.8.0
|
||||
|
||||
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
|
||||
- stripped associated properties from properties.py and passed as wm props.
|
||||
|
||||
0.7.2:
|
||||
0.7.2
|
||||
|
||||
- fix: Palette importer bug
|
||||
|
||||
0.7.0:
|
||||
0.7.0
|
||||
|
||||
- feat: auto create empty frame on color layer
|
||||
|
||||
0.6.3:
|
||||
0.6.3
|
||||
|
||||
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
|
||||
|
||||
0.6.2:
|
||||
0.6.2
|
||||
|
||||
- feat: colorisation, Option to change stop lines length
|
||||
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
|
||||
- Minor refactor for submodule register
|
||||
|
||||
0.6.1:
|
||||
0.6.1
|
||||
|
||||
- feat: render objects grouped, one anim render with all ticked object using manual output name
|
||||
|
||||
0.6.0:
|
||||
0.6.0
|
||||
|
||||
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
|
||||
|
||||
0.5.9:
|
||||
0.5.9
|
||||
|
||||
- feat: render exporter
|
||||
- Render a selection of GP object isolated from the rest
|
||||
|
@ -180,21 +587,21 @@
|
|||
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
|
||||
- check file: set scene resolution to settings in prefs (default 2048x1080)
|
||||
|
||||
0.5.8:
|
||||
0.5.8
|
||||
|
||||
- feat: GP material append on active object from single blend file
|
||||
|
||||
0.5.7:
|
||||
0.5.7
|
||||
|
||||
- Added warning message for cursor snapping
|
||||
|
||||
0.5.5 - 0.5.6:
|
||||
0.5.6
|
||||
|
||||
- check file: added check for placement an projection mode for Gpencil.
|
||||
- add a slider to change edit_lines_opacity globally for all GP data at once
|
||||
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
|
||||
|
||||
0.5.4:
|
||||
0.5.4
|
||||
|
||||
- feat: anim manager in his own GP_toolbox submenu:
|
||||
- button to list disabled anim (allow to quickly check state of the scene)
|
||||
|
@ -202,19 +609,19 @@
|
|||
- shift clic to target selection only
|
||||
- check file: added disabled fcurved counter alert with detail in console
|
||||
|
||||
0.5.3:
|
||||
0.5.3
|
||||
|
||||
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
|
||||
- check file option: change select active tool (choice added in addon preferences)
|
||||
|
||||
0.5.2:
|
||||
0.5.2
|
||||
|
||||
- Revert back obj_cam operator for following object (native lock view follow only translation)
|
||||
- Changed method for canvas rotation to more robust rotate axis.
|
||||
- Add operators on link checker to open containing folder/file of link
|
||||
- Refactor: file checkers in their own file
|
||||
|
||||
0.5.1:
|
||||
0.5.1
|
||||
|
||||
- fix: error when empty material slot on GP object.
|
||||
- fix: cursor snap on GP canvas when GP is parented
|
||||
|
@ -229,11 +636,11 @@
|
|||
- set show slider and sync range
|
||||
- set fps to 24
|
||||
|
||||
0.4.6:
|
||||
0.4.6
|
||||
|
||||
- feat: basic Palette manager with base material check and warning
|
||||
|
||||
0.4.5:
|
||||
0.4.5
|
||||
|
||||
- open blender config folder from addon preference
|
||||
- fix: obj cam parent on selected object
|
||||
|
@ -242,29 +649,29 @@
|
|||
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
|
||||
|
||||
|
||||
0.4.4:
|
||||
0.4.4
|
||||
|
||||
- feat: added cursor follow handlers and UI toggle
|
||||
|
||||
0.4.3:
|
||||
0.4.3
|
||||
|
||||
- change playblast out to 'images' and add playblast as name prefix
|
||||
|
||||
0.4.2:
|
||||
0.4.2
|
||||
|
||||
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
|
||||
- fix: canvas rotate works with parented camera !
|
||||
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
|
||||
|
||||
0.4.1:
|
||||
0.4.1
|
||||
|
||||
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
|
||||
|
||||
0.4.0:
|
||||
0.4.0
|
||||
|
||||
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
|
||||
|
||||
0.3.8:
|
||||
0.3.8
|
||||
|
||||
- UI: expose onion skin in interface
|
||||
- UI: expose autolock in interface
|
||||
|
@ -272,47 +679,47 @@
|
|||
- code: refactor, pushed most of class register in their owner file
|
||||
- tool: tool to rename current or all grease pencil datablock with different name than container object
|
||||
|
||||
0.3.7:
|
||||
0.3.7
|
||||
|
||||
- UI: new interface with tabs for addon preferences
|
||||
- UI: possible to disable color panel from preference (might be deleted if unusable)
|
||||
- docs: change readme changelog format and correct doc
|
||||
|
||||
0.3.6:
|
||||
0.3.6
|
||||
|
||||
- UI: Stoplines : add a button for quickly set stoplines visibility.
|
||||
|
||||
0.3.5:
|
||||
0.3.5
|
||||
|
||||
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
|
||||
- Fix: Enter key added to valid object-breakdown modal.
|
||||
|
||||
0.3.3:
|
||||
0.3.3
|
||||
|
||||
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
|
||||
|
||||
0.3.3:
|
||||
0.3.3
|
||||
|
||||
- version 1 beta of gmic colorize
|
||||
- variant of `screen.gp_keyframe_jump` through keymap seetings
|
||||
|
||||
0.3.0:
|
||||
0.3.0
|
||||
|
||||
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
|
||||
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
|
||||
|
||||
0.2.3:
|
||||
0.2.3
|
||||
|
||||
- add operator to `screen.gp_keyframe_jump`
|
||||
- add shortcut to rotate canvas
|
||||
- fix duplicate class
|
||||
|
||||
0.2.2:
|
||||
0.2.2
|
||||
|
||||
- separated props resolution_percentage parameter
|
||||
- playblast options for launching folder and opening folder
|
||||
|
||||
0.2.1:
|
||||
0.2.1
|
||||
|
||||
- playblast feature
|
||||
- Button to go zoom 100% or fit screen
|
||||
|
@ -320,22 +727,22 @@
|
|||
- Fix reference panel : works with video and display in a box layout.
|
||||
- close pseudo-color panel by default (plan to move it to Gpencil tab)
|
||||
|
||||
0.2.0:
|
||||
0.2.0
|
||||
|
||||
- UI: Toggle camera background images from Toolbox panel
|
||||
- UI: quick access to passepartout
|
||||
- Feature: option to use namespace for pseudo color
|
||||
|
||||
0.1.5:
|
||||
0.1.5
|
||||
|
||||
- added CGC-auto-updater
|
||||
|
||||
0.1.3:
|
||||
0.1.3
|
||||
|
||||
- flip cam x
|
||||
- inital stage of overlay toggle (need pref/multiple pref)
|
||||
|
||||
0.1.2:
|
||||
0.1.2
|
||||
|
||||
- subpanel of GP data (instead of direct append)
|
||||
- initial commit with GP pseudo color
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from . import OP_line_closer
|
||||
# from . import OP_line_closer
|
||||
from . import OP_create_empty_frames
|
||||
|
||||
|
||||
|
@ -45,11 +45,11 @@ def register():
|
|||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
OP_create_empty_frames.register()
|
||||
OP_line_closer.register()
|
||||
# OP_line_closer.register()
|
||||
bpy.types.Scene.gpcolor_props = bpy.props.PointerProperty(type = GPCOLOR_PG_settings)
|
||||
|
||||
def unregister():
|
||||
OP_line_closer.unregister()
|
||||
# OP_line_closer.unregister()
|
||||
OP_create_empty_frames.unregister()
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
|
|
@ -1,33 +1,199 @@
|
|||
## Create empty keyframe where keyframe exists in layers above.
|
||||
import bpy
|
||||
from bpy.props import (FloatProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
IntProperty)
|
||||
from .. import utils
|
||||
from ..utils import is_hidden
|
||||
|
||||
## copied from OP_key_duplicate_send
|
||||
def get_layer_list(self, context):
|
||||
'''return (identifier, name, description) of enum content'''
|
||||
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
|
||||
def get_group_list(self, context):
|
||||
return [(g.name, g.name, '') for g in context.object.data.layer_groups]
|
||||
|
||||
|
||||
class GP_OT_create_empty_frames(bpy.types.Operator):
|
||||
bl_idname = "gp.create_empty_frames"
|
||||
bl_label = "Create empty frames"
|
||||
bl_description = "Create new empty frames on active layer where there is a frame in layer above\n(usefull in color layers to match line frames)"
|
||||
bl_label = "Create Empty Frames"
|
||||
bl_description = "Create new empty frames on active layer where there is a frame in targeted layers\
|
||||
\n(usefull in color layers to match line frames)"
|
||||
bl_options = {'REGISTER','UNDO'}
|
||||
|
||||
layers_enum : EnumProperty(
|
||||
name="Empty Keys from Layer",
|
||||
description="Reference keys from layer",
|
||||
items=get_layer_list
|
||||
)
|
||||
|
||||
groups_enum : EnumProperty(
|
||||
name="Empty Keys from Group",
|
||||
description="Duplicate keys from group",
|
||||
items=get_group_list
|
||||
)
|
||||
|
||||
targeted_layers : EnumProperty(
|
||||
name="Sources", # Empty keys from targets
|
||||
description="Duplicate keys as empty on current layer from selected targets",
|
||||
default="ALL_ABOVE",
|
||||
items=(
|
||||
('ALL_ABOVE', 'All Layers Above', 'Empty frames from all layers above'),
|
||||
('ALL_BELOW', 'All Layers Below', 'Empty frames from all layers below'),
|
||||
('NUMBER', 'Number Above Or Below', 'Positive number above layers\nNegative number below layers'),
|
||||
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
|
||||
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
|
||||
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
|
||||
('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'),
|
||||
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
|
||||
)
|
||||
)
|
||||
|
||||
range : EnumProperty(
|
||||
name="Range",
|
||||
description="Restraint empty copy from a defined range",
|
||||
default="FULL",
|
||||
items=(
|
||||
('FULL', 'Full range', 'Empty frames from all layers above'),
|
||||
('BEFORE', 'Before Time Cursor', 'Empty frames from all layers below'),
|
||||
('AFTER', 'After Time Cursor', 'Only After time cursor'),
|
||||
('SCENE', 'On scene range', 'Restric to Scene/Preview range'),
|
||||
)
|
||||
)
|
||||
|
||||
number : IntProperty(name='Number',
|
||||
default=1,
|
||||
description='Number of layer to create empty key from\nabove (positive) or layer below (negative)',
|
||||
options={'SKIP_SAVE'})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Possible preset with shortcut
|
||||
# if event.alt:
|
||||
# self.targeted_layers = 'ALL_VISIBLE'
|
||||
gp = context.grease_pencil
|
||||
layer_from_group = None
|
||||
if gp.layer_groups.active:
|
||||
layer_from_group = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
|
||||
## Can just do if not utils.get_closest_active_layer(context.grease_pencil):
|
||||
if not gp.layers.active and not layer_from_group:
|
||||
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
|
||||
return {'CANCELLED'}
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# layout.label(text='Create Empty Frames From Other Layers')
|
||||
# target
|
||||
layout.prop(self, 'targeted_layers')
|
||||
if self.targeted_layers == 'CHOSEN':
|
||||
if self.layers_enum:
|
||||
layout.prop(self, 'layers_enum')
|
||||
else:
|
||||
layout.label(text='No other layers to match keyframe!', icon='ERROR')
|
||||
|
||||
if self.targeted_layers == 'CHOSEN_GROUP':
|
||||
if self.groups_enum:
|
||||
layout.prop(self, 'groups_enum')
|
||||
else:
|
||||
layout.label(text='No other groups to match keyframe!', icon='ERROR')
|
||||
|
||||
elif self.targeted_layers == 'NUMBER':
|
||||
row = layout.row()
|
||||
row.prop(self, 'number')
|
||||
row.active = self.number != 0
|
||||
if self.number == 0:
|
||||
layout.label(text="Can't have 0 as value")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, 'range')
|
||||
if self.range == 'SCENE':
|
||||
if context.scene.use_preview_range:
|
||||
layout.label(text='Using preview range', icon='INFO')
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
gpl.active_index
|
||||
gp = obj.data
|
||||
gpl = gp.layers
|
||||
|
||||
## Only possible on 'fill' layer ??
|
||||
# if not 'fill' in gpl.active.info.lower():
|
||||
# self.report({'ERROR'}, f"There must be 'fill' text in layer name")
|
||||
# return {'CANCELLED'}
|
||||
if gp.layer_groups.active:
|
||||
reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
|
||||
else:
|
||||
reference_layer = gpl.active
|
||||
|
||||
active_index = next((i for i, l in enumerate(gpl) if l == reference_layer), None)
|
||||
|
||||
print(self.targeted_layers)
|
||||
if self.targeted_layers == 'ALL_ABOVE':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i > active_index]
|
||||
|
||||
elif self.targeted_layers == 'ALL_BELOW':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i < active_index]
|
||||
|
||||
elif self.targeted_layers == 'ABOVE':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1]
|
||||
|
||||
elif self.targeted_layers == 'BELOW':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
|
||||
|
||||
elif self.targeted_layers == 'ALL_VISIBLE':
|
||||
tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
|
||||
|
||||
elif self.targeted_layers == 'CHOSEN':
|
||||
if not self.layers_enum:
|
||||
self.report({'ERROR'}, f"No chosen layers, aborted")
|
||||
return {'CANCELLED'}
|
||||
tgt_layers = [l for l in gpl if l.name == self.layers_enum]
|
||||
|
||||
elif self.targeted_layers == 'CHOSEN_GROUP':
|
||||
if not self.groups_enum:
|
||||
self.report({'ERROR'}, f"No chosen groups, aborted")
|
||||
return {'CANCELLED'}
|
||||
group = gp.layer_groups.get(self.groups_enum)
|
||||
tgt_layers = [l for l in gpl if l.parent_group == group]
|
||||
|
||||
elif self.targeted_layers == 'NUMBER':
|
||||
if self.number == 0:
|
||||
self.report({'ERROR'}, f"Can't have 0 as value")
|
||||
return {'CANCELLED'}
|
||||
|
||||
l_range = active_index + self.number
|
||||
print('l_range: ', l_range)
|
||||
if self.number > 0: # positive
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range]
|
||||
else:
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range]
|
||||
|
||||
if not tgt_layers:
|
||||
self.report({'ERROR'}, f"No layers found with chosen Targets")
|
||||
return {'CANCELLED'}
|
||||
|
||||
frame_id_list = []
|
||||
for i, l in enumerate(gpl):
|
||||
# don't list layer below
|
||||
if i <= gpl.active_index:
|
||||
continue
|
||||
# print(l.info, "index:", i)
|
||||
for l in tgt_layers:
|
||||
for f in l.frames:
|
||||
|
||||
## frame filter
|
||||
if self.range != 'FULL': # FULl = No filter
|
||||
if self.range == 'BEFORE':
|
||||
if not f.frame_number <= context.scene.frame_current:
|
||||
continue
|
||||
elif self.range == 'AFTER':
|
||||
if not f.frame_number >= context.scene.frame_current:
|
||||
continue
|
||||
elif self.range == 'SCENE':
|
||||
if context.scene.use_preview_range:
|
||||
if not context.scene.frame_preview_start <= f.frame_number <= context.scene.frame_preview_end:
|
||||
continue
|
||||
else:
|
||||
if not context.scene.frame_start <= f.frame_number <= context.scene.frame_end:
|
||||
continue
|
||||
|
||||
frame_id_list.append(f.frame_number)
|
||||
|
||||
frame_id_list = list(set(frame_id_list))
|
||||
|
@ -40,17 +206,17 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
if num in current_frames:
|
||||
continue
|
||||
#Create empty frame
|
||||
gpl.active.frames.new(num, active=False)
|
||||
gpl.active.frames.new(num)
|
||||
fct += 1
|
||||
|
||||
gpl.update()
|
||||
if fct:
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
|
||||
else:
|
||||
self.report({'WARNING'}, f"No frames to create !")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GP_OT_create_empty_frames)
|
||||
|
||||
|
|
|
@ -5,11 +5,14 @@ from ..utils import (location_to_region,
|
|||
vector_length,
|
||||
draw_gp_stroke,
|
||||
extrapolate_points_by_length,
|
||||
simple_draw_gp_stroke)
|
||||
simple_draw_gp_stroke,
|
||||
is_hidden,
|
||||
is_locked)
|
||||
|
||||
import bpy
|
||||
from math import degrees
|
||||
from mathutils import Vector
|
||||
|
||||
# from os.path import join, basename, exists, dirname, abspath, splitext
|
||||
|
||||
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
|
||||
|
@ -53,11 +56,14 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter = defaultdict(list)
|
||||
plist = []
|
||||
matrix = ob.matrix_world
|
||||
for s in f.strokes:#add first and last
|
||||
for s in f.drawing.strokes: #add first and last
|
||||
smat = ob.material_slots[s.material_index].material
|
||||
if not smat:continue#no material on line
|
||||
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
|
||||
if len(s.points) < 2:continue#avoid 0 or 1 points
|
||||
if not smat:
|
||||
continue #no material on line
|
||||
if smat.grease_pencil.show_fill:
|
||||
continue # skip fill lines -> #smat.grease_pencil.show_stroke
|
||||
if len(s.points) < 2:
|
||||
continue #avoid 0 or 1 points
|
||||
plist.append(s.points[0])
|
||||
plist.append(s.points[-1])
|
||||
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
|
||||
|
@ -70,7 +76,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
for op in plist:#other points
|
||||
if p == op:# print('same point')
|
||||
continue
|
||||
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
|
||||
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position))
|
||||
# print('gap2d: ', gap2d)
|
||||
if gap2d > tol:
|
||||
continue
|
||||
|
@ -102,16 +108,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter[p].append(op)
|
||||
|
||||
|
||||
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
|
||||
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
|
||||
ctl += 1
|
||||
|
||||
print(f'{ctl} line created')
|
||||
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
|
||||
|
||||
def create_closing_line(tolerance=0.2):
|
||||
for ob in bpy.context.selected_objects:
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
continue
|
||||
|
||||
mat_id = get_closeline_mat(ob)# get a the closing material
|
||||
|
@ -128,7 +134,7 @@ def create_closing_line(tolerance=0.2):
|
|||
## filter on selected
|
||||
if not l.select:continue# comment this line for all
|
||||
# for f in l.frames:#not all for now
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
## create gap stroke
|
||||
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
|
||||
|
||||
|
@ -143,9 +149,9 @@ def is_deviating_by(s, deviation=0.75):
|
|||
pb = s.points[-2]
|
||||
pc = s.points[-3]
|
||||
|
||||
a = location_to_region(pa.co)
|
||||
b = location_to_region(pb.co)
|
||||
c = location_to_region(pc.co)
|
||||
a = location_to_region(pa.position)
|
||||
b = location_to_region(pb.position)
|
||||
c = location_to_region(pc.position)
|
||||
|
||||
#cb-> compare angle with ba->
|
||||
angle = (b-c).angle(a-b)
|
||||
|
@ -158,16 +164,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
|
|||
'''extend line boundary by given length'''
|
||||
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
|
||||
## 2D mode
|
||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
|
||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
|
||||
# b = location_to_region(b_loc)
|
||||
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
|
||||
# c_loc = region_to_location(c, b_loc)
|
||||
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
|
||||
|
||||
## 3D
|
||||
a = s.points[id_pair[0]].co# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].co# ob.matrix_world @
|
||||
a = s.points[id_pair[0]].position# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].position# ob.matrix_world @
|
||||
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
|
||||
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
|
||||
|
||||
|
@ -188,15 +194,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
|
|||
|
||||
## Change length of current length to designated
|
||||
# Vector point A to point B (direction), push point B in this direction
|
||||
a = s.points[-2].co
|
||||
a = s.points[-2].position
|
||||
bp = s.points[-1]#end-point
|
||||
b = bp.co
|
||||
b = bp.position
|
||||
ab = b - a
|
||||
if not ab:
|
||||
continue
|
||||
# new pos of B is A + new length in the AB direction
|
||||
newb = a + (ab.normalized() * length)
|
||||
bp.co = newb
|
||||
bp.position = newb
|
||||
ct += 1
|
||||
|
||||
return ct
|
||||
|
@ -210,14 +216,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
|
|||
return
|
||||
|
||||
# TODO need custom filters or go in GP refine strokes...
|
||||
# frame = ob.data.layers.active.active_frame
|
||||
# frame = ob.data.layers.active.current_frame()
|
||||
|
||||
if not frame: return
|
||||
ct = 0
|
||||
#TODO need to delete previous closing lines on frame before launching
|
||||
|
||||
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
|
||||
for s in list(frame.strokes):
|
||||
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in !
|
||||
for s in list(frame.drawing.strokes):
|
||||
if s.material_index == mat_id:#is a closeline
|
||||
continue
|
||||
if len(s.points) < 2:#not enough point to evaluate
|
||||
|
@ -241,7 +247,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
# mode : bpy.props.StringProperty(
|
||||
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
|
||||
|
@ -273,18 +279,18 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
|||
if self.layer_tgt == 'ACTIVE':
|
||||
lays = [ob.data.layers.active]
|
||||
elif self.layer_tgt == 'SELECTED':
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
continue
|
||||
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
|
||||
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No strokes extended... see console"
|
||||
|
@ -306,7 +312,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
layer_tgt : bpy.props.EnumProperty(
|
||||
name="Extend layers", description="Choose which layer to target",
|
||||
|
@ -334,18 +340,18 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
|||
if self.layer_tgt == 'ACTIVE':
|
||||
lays = [ob.data.layers.active]
|
||||
elif self.layer_tgt == 'SELECTED':
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
continue
|
||||
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
|
||||
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No extension modified... see console"
|
||||
|
@ -367,15 +373,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ct = 0
|
||||
ob = context.object
|
||||
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
for l in lays:
|
||||
if not l.active_frame:continue
|
||||
for s in l.active_frame.strokes:
|
||||
if not l.current_frame():continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
|
||||
ct+=1
|
||||
|
||||
|
@ -397,7 +403,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.object is not None)# and context.object.type == 'GPENCIL'
|
||||
return (context.object is not None)# and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
## draw stuff inside the header (place before main label)
|
||||
# def draw_header(self, context):
|
||||
|
@ -414,7 +420,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
|||
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
|
||||
|
||||
#diplay closeline visibility
|
||||
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
|
||||
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
|
||||
row=layout.row()
|
||||
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
|
||||
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
|
||||
|
|
|
@ -302,8 +302,10 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
|||
|
||||
### --- KEYMAP ---
|
||||
|
||||
breakdowner_addon_keymaps = []
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
if bpy.app.background:
|
||||
return
|
||||
# pref = get_addon_prefs()
|
||||
# if not pref.breakdowner_use_shortcut:
|
||||
# return
|
||||
|
@ -320,24 +322,23 @@ def register_keymaps():
|
|||
if ops_id not in km.keymap_items:
|
||||
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY
|
||||
kmi = km.keymap_items.new(ops_id, type="E", value="PRESS", shift=True)
|
||||
breakdowner_addon_keymaps.append((km, kmi))
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in breakdowner_addon_keymaps:
|
||||
if bpy.app.background:
|
||||
return
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
|
||||
breakdowner_addon_keymaps.clear()
|
||||
# del breakdowner_addon_keymaps[:]
|
||||
addon_keymaps.clear()
|
||||
|
||||
### --- REGISTER ---
|
||||
|
||||
def register():
|
||||
# if not bpy.app.background:
|
||||
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
|
||||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
# if not bpy.app.background:
|
||||
unregister_keymaps()
|
||||
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
|
|||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GPENCIL'
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
|
@ -48,30 +48,53 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_brush_set(bpy.types.Operator):
|
||||
bl_idname = "gp.brush_set"
|
||||
bl_label = "Set Brush"
|
||||
bl_description = "Set Gpencil brush"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
brush_name : bpy.props.StringProperty(name='Brush', description='Name of the brush to use')
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
brush = bpy.data.brushes.get(self.brush_name)
|
||||
if not brush:
|
||||
self.report({'ERROR'}, f'Brush "{self.brush_name}" not found')
|
||||
return {"CANCELLED"}
|
||||
context.scene.tool_settings.gpencil_paint.brush = brush
|
||||
return {"FINISHED"}
|
||||
|
||||
### -- MENU ENTRY --
|
||||
|
||||
def load_brush_ui(self, context):
|
||||
if context.mode == 'PAINT_GPENCIL':
|
||||
self.layout.operator('gp.load_brushes', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
|
||||
|
||||
def load_brush_top_bar_ui(self, context):
|
||||
if context.mode == 'PAINT_GPENCIL':
|
||||
self.layout.operator('gp.load_brushes')
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
|
||||
|
||||
classes = (
|
||||
GPTB_OT_load_brushes,
|
||||
GPTB_OT_brush_set,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(load_brush_ui)
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui)
|
||||
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
|
||||
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(load_brush_ui)
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
||||
|
|
398
OP_copy_paste.py
398
OP_copy_paste.py
|
@ -1,38 +1,15 @@
|
|||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
## based on GPclipboard 1.3.2 (just stripped addon prefs)
|
||||
|
||||
bl_info = {
|
||||
"name": "GP clipboard",
|
||||
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
|
||||
"author": "Samuel Bernou",
|
||||
"version": (1, 3, 2),
|
||||
"blender": (2, 83, 0),
|
||||
"location": "View3D > Toolbar > Gpencil > GP clipboard",
|
||||
"warning": "",
|
||||
"doc_url": "https://github.com/Pullusb/GP_clipboard",
|
||||
"category": "Object" }
|
||||
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
|
||||
## View3D > Toolbar > Gpencil > GP clipboard
|
||||
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
import json
|
||||
from time import time
|
||||
from operator import itemgetter
|
||||
from itertools import groupby
|
||||
# from pprint import pprint
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
def convertAttr(Attr):
|
||||
'''Convert given value to a Json serializable format'''
|
||||
|
@ -48,97 +25,106 @@ def convertAttr(Attr):
|
|||
def getMatrix(layer) :
|
||||
matrix = mathutils.Matrix.Identity(4)
|
||||
|
||||
if layer.is_parented:
|
||||
if layer.parent_type == 'BONE':
|
||||
object = layer.parent
|
||||
bone = object.pose.bones[layer.parent_bone]
|
||||
matrix = bone.matrix @ object.matrix_world
|
||||
matrix = matrix.copy() @ layer.matrix_inverse
|
||||
if parent := layer.parent:
|
||||
if parent.type == 'ARMATURE' and layer.parent_bone:
|
||||
bone = parent.pose.bones[layer.parent_bone]
|
||||
matrix = bone.matrix @ parent.matrix_world
|
||||
matrix = matrix.copy() @ layer.matrix_parent_inverse
|
||||
else:
|
||||
matrix = layer.parent.matrix_world @ layer.matrix_inverse
|
||||
matrix = parent.matrix_world @ layer.matrix_parent_inverse
|
||||
|
||||
return matrix.copy()
|
||||
|
||||
default_pt_uv_fill = Vector((0.5, 0.5))
|
||||
# default_pt_uv_fill = Vector((0.5, 0.5))
|
||||
|
||||
def dump_gp_point(p, l, obj):
|
||||
def dump_gp_point(p, l, obj,
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
|
||||
'''add properties of a given points to a dic and return it'''
|
||||
pdic = {}
|
||||
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
|
||||
point_dict = {}
|
||||
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
|
||||
#for att in point_attr_list:
|
||||
# pdic[att] = convertAttr(getattr(p, att))
|
||||
# point_dict[att] = convertAttr(getattr(p, att))
|
||||
if l.parent:
|
||||
mat = getMatrix(l)
|
||||
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
|
||||
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
|
||||
else:
|
||||
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
|
||||
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
|
||||
# pdic['select'] = convertAttr(getattr(p,'select'))# need selection ?
|
||||
pdic['strength'] = convertAttr(getattr(p,'strength'))
|
||||
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
|
||||
|
||||
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
|
||||
if radius and p.radius != 1.0:
|
||||
point_dict['radius'] = convertAttr(getattr(p,'radius'))
|
||||
|
||||
if opacity and p.opacity != 1.0:
|
||||
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
|
||||
|
||||
## get vertex color (long...)
|
||||
if p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
||||
pdic['vertex_color'] = convertAttr(p.vertex_color)
|
||||
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
||||
point_dict['vertex_color'] = convertAttr(p.vertex_color)
|
||||
|
||||
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
|
||||
if p.uv_fill != default_pt_uv_fill:
|
||||
pdic['uv_fill'] = convertAttr(p.uv_fill)
|
||||
if p.uv_factor != 0.0:
|
||||
pdic['uv_factor'] = convertAttr(p.uv_factor)
|
||||
if p.uv_rotation != 0.0:
|
||||
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
|
||||
if rotation and p.rotation != 0.0:
|
||||
point_dict['rotation'] = convertAttr(p.rotation)
|
||||
|
||||
return pdic
|
||||
## No time infos
|
||||
# if delta_time and p.delta_time != 0.0:
|
||||
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
|
||||
|
||||
return point_dict
|
||||
|
||||
def dump_gp_stroke_range(s, sid, l, obj):
|
||||
def dump_gp_stroke_range(s, sid, l, obj,
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||
'''Get a grease pencil stroke and return a dic with attribute
|
||||
(points attribute being a dic of dics to store points and their attributes)
|
||||
'''
|
||||
|
||||
sdic = {}
|
||||
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
|
||||
for att in stroke_attr_list:
|
||||
sdic[att] = getattr(s, att)
|
||||
stroke_dict = {}
|
||||
# stroke_attr_list = ('line_width',)
|
||||
# for att in stroke_attr_list:
|
||||
# stroke_dict[att] = getattr(s, att)
|
||||
|
||||
## Dump following these value only if they are non default
|
||||
if s.material_index != 0:
|
||||
sdic['material_index'] = s.material_index
|
||||
stroke_dict['material_index'] = s.material_index
|
||||
|
||||
if getattr(s, 'draw_cyclic', None):# pre-2.92
|
||||
sdic['draw_cyclic'] = s.draw_cyclic
|
||||
if s.cyclic:
|
||||
stroke_dict['cyclic'] = s.cyclic
|
||||
|
||||
if getattr(s, 'use_cyclic', None):# from 2.92
|
||||
sdic['use_cyclic'] = s.use_cyclic
|
||||
if s.softness != 0.0:
|
||||
stroke_dict['softness'] = s.softness
|
||||
|
||||
if s.uv_scale != 1.0:
|
||||
sdic['uv_scale'] = s.uv_scale
|
||||
if s.aspect_ratio != 1.0:
|
||||
stroke_dict['aspect_ratio'] = s.aspect_ratio
|
||||
|
||||
if s.uv_rotation != 0.0:
|
||||
sdic['uv_rotation'] = s.uv_rotation
|
||||
if s.start_cap != 0:
|
||||
stroke_dict['start_cap'] = s.start_cap
|
||||
|
||||
if s.hardness != 1.0:
|
||||
sdic['hardness'] = s.hardness
|
||||
if s.end_cap != 0:
|
||||
stroke_dict['end_cap'] = s.end_cap
|
||||
|
||||
if s.uv_translation != Vector((0.0, 0.0)):
|
||||
sdic['uv_translation'] = convertAttr(s.uv_translation)
|
||||
if fill_color and s.fill_color[:] != (0,0,0,0):
|
||||
stroke_dict['fill_color'] = convertAttr(s.fill_color)
|
||||
|
||||
if s.vertex_color_fill[:] != (0,0,0,0):
|
||||
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
|
||||
if fill_opacity and s.fill_opacity != 0.0:
|
||||
stroke_dict['fill_opacity'] = s.fill_opacity
|
||||
|
||||
## No time infos
|
||||
# if s.time_start != 0.0:
|
||||
# stroke_dict['time_start'] = s.time_start
|
||||
|
||||
points = []
|
||||
if sid is None: # no ids, just full points...
|
||||
for p in s.points:
|
||||
points.append(dump_gp_point(p,l,obj))
|
||||
points.append(dump_gp_point(p, l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||
else:
|
||||
for pid in sid:
|
||||
points.append(dump_gp_point(s.points[pid],l,obj))
|
||||
sdic['points'] = points
|
||||
return sdic
|
||||
points.append(dump_gp_point(s.points[pid], l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||
|
||||
stroke_dict['points'] = points
|
||||
return stroke_dict
|
||||
|
||||
|
||||
|
||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow filter)
|
||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
||||
'''
|
||||
copy all visibles selected strokes on active frame
|
||||
layers can be None, a single layer object or list of layer object as filter
|
||||
|
@ -154,7 +140,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
# color = gp.palettes.active.colors.active.name
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]#[]
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -162,32 +148,36 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
stroke_list = [] # one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if f: # active frame can be None
|
||||
if not copy:
|
||||
staylist = [] # init part of strokes that must survive on this layer
|
||||
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
rm_list = [] # init strokes that must be removed from this layer
|
||||
for s_index, stroke in enumerate(f.drawing.strokes):
|
||||
if stroke.select:
|
||||
# separate in multiple stroke if parts of the strokes a selected.
|
||||
sel = [i for i, p in enumerate(s.points) if p.select]
|
||||
sel = [i for i, p in enumerate(stroke.points) if p.select]
|
||||
substrokes = [] # list of list containing isolated selection
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
|
||||
|
||||
# continuity stroke have same substract result between point index and enumerator
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
|
||||
group = list(map(itemgetter(1), g))
|
||||
substrokes.append(group)
|
||||
|
||||
for ss in substrokes:
|
||||
if len(ss) > 1: # avoid copy isolated points
|
||||
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
||||
stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj))
|
||||
|
||||
# Cutting operation
|
||||
if not copy:
|
||||
maxindex = len(s.points)-1
|
||||
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier
|
||||
f.strokes.remove(s)
|
||||
maxindex = len(stroke.points)-1
|
||||
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
|
||||
# f.drawing.strokes.remove(stroke) # gpv2
|
||||
rm_list.append(s_index)
|
||||
else:
|
||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
||||
neg = [i for i, p in enumerate(stroke.points) if not p.select]
|
||||
|
||||
staying = []
|
||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||
|
@ -201,37 +191,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow fil
|
|||
|
||||
for ns in staying:
|
||||
if len(ns) > 1:
|
||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
||||
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
|
||||
# make a negative list containing all last index
|
||||
|
||||
|
||||
'''#full stroke version
|
||||
# if s.colorname == color: #line for future filters
|
||||
stroke_list.append(dump_gp_stroke(s,l))
|
||||
#delete stroke on the fly
|
||||
if not copy:
|
||||
f.strokes.remove(s)
|
||||
'''
|
||||
if rm_list:
|
||||
f.drawing.remove_strokes(indices=rm_list)
|
||||
|
||||
if not copy:
|
||||
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
|
||||
|
||||
# delete all selected strokes...
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
f.strokes.remove(s)
|
||||
if selected_ids:
|
||||
f.drawing.remove_strokes(indices=selected_ids)
|
||||
|
||||
# ...recreate these uncutted ones
|
||||
#pprint(staylist)
|
||||
if staylist:
|
||||
add_multiple_strokes(staylist, l)
|
||||
#for ns in staylist:#weirdly recreate the stroke twice !
|
||||
# add_stroke(ns, f, l)
|
||||
|
||||
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
||||
if not copy and not keep_empty:#
|
||||
if not len(f.strokes):
|
||||
# If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
||||
if not copy and not keep_empty:
|
||||
if not len(f.drawing.strokes):
|
||||
l.frames.remove(f)
|
||||
|
||||
|
||||
|
||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
#print(stroke_list)
|
||||
return stroke_list
|
||||
|
@ -253,7 +236,7 @@ def copy_all_strokes(layers=None):
|
|||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -261,12 +244,12 @@ def copy_all_strokes(layers=None):
|
|||
stroke_list = []# one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## full stroke version
|
||||
# if s.select:
|
||||
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
|
||||
|
@ -276,11 +259,11 @@ def copy_all_strokes(layers=None):
|
|||
return stroke_list
|
||||
"""
|
||||
|
||||
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
||||
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||
'''
|
||||
copy all stroke, not affected by selection on active frame
|
||||
layers can be None, a single layer object or list of layer object as filter
|
||||
if keep_empty is False the frame is deleted when all strokes are cutted
|
||||
'''
|
||||
t0 = time()
|
||||
scene = bpy.context.scene
|
||||
|
@ -293,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
|||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -301,74 +284,69 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
|||
stroke_list = []
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## full stroke version
|
||||
# if s.select:
|
||||
# send index of all points to get the whole stroke with "range"
|
||||
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj) )
|
||||
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
|
||||
|
||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
#print(stroke_list)
|
||||
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
return stroke_list
|
||||
|
||||
def add_stroke(s, frame, layer, obj, select=False):
|
||||
'''add stroke on a given frame, (layer is for parentage setting)'''
|
||||
# print(3*'-',s)
|
||||
ns = frame.strokes.new()
|
||||
pts_to_add = len(s['points'])
|
||||
frame.drawing.add_strokes([pts_to_add])
|
||||
|
||||
ns = frame.drawing.strokes[-1]
|
||||
|
||||
## set strokes atrributes
|
||||
for att, val in s.items():
|
||||
if att not in ('points'):
|
||||
setattr(ns, att, val)
|
||||
pts_to_add = len(s['points'])
|
||||
# print(pts_to_add, 'points')#dbg
|
||||
|
||||
ns.points.add(pts_to_add)
|
||||
|
||||
ob_mat_inv = obj.matrix_world.inverted()
|
||||
|
||||
## patch pressure 1
|
||||
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
|
||||
if layer.parent:
|
||||
layer_matrix = getMatrix(layer).inverted()
|
||||
transform_matrix = ob_mat_inv @ layer_matrix
|
||||
else:
|
||||
transform_matrix = ob_mat_inv
|
||||
|
||||
if layer.is_parented:
|
||||
mat = getMatrix(layer).inverted()
|
||||
## Set points attributes
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'co':
|
||||
if k == 'position':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
|
||||
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
if select:
|
||||
ns.points[i].select = True
|
||||
|
||||
else:
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'co':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
if select:
|
||||
ns.points[i].select = True
|
||||
## Opacity initialized at 0.0 (should be 1.0)
|
||||
if not 'opacity' in pt:
|
||||
ns.points[i].opacity = 1.0
|
||||
|
||||
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
|
||||
ns.points.update()
|
||||
## Radius initialized at 0.0 (should probably be 0.01)
|
||||
if not 'radius' in pt:
|
||||
ns.points[i].radius = 0.01
|
||||
|
||||
## patch pressure 2
|
||||
# ns.points.foreach_set('pressure', pressure_flat_list)
|
||||
|
||||
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
|
||||
'''
|
||||
add a list of strokes to active frame of given layer
|
||||
if no layer specified, active layer is used
|
||||
if use_current_frame is True, a new frame will be created only if needed
|
||||
if select is True, newly added strokes are selected
|
||||
if select is True, newly added strokes are set selected
|
||||
if stroke list is empty create an empty frame at current frame
|
||||
'''
|
||||
scene = bpy.context.scene
|
||||
obj = bpy.context.object
|
||||
|
@ -381,8 +359,8 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
|
||||
fnum = scene.frame_current
|
||||
target_frame = False
|
||||
act = layer.active_frame
|
||||
for s in stroke_list:
|
||||
act = layer.current_frame()
|
||||
## set frame if needed
|
||||
if act:
|
||||
if use_current_frame or act.frame_number == fnum:
|
||||
#work on current frame if exists
|
||||
|
@ -394,12 +372,10 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
#or active exists but not aligned scene.current with use_current_frame disabled
|
||||
target_frame = layer.frames.new(fnum)
|
||||
|
||||
for s in stroke_list:
|
||||
add_stroke(s, target_frame, layer, obj, select=select)
|
||||
'''
|
||||
for s in stroke_data:
|
||||
add_stroke(s, target_frame)
|
||||
'''
|
||||
print(len(stroke_list), 'strokes pasted')
|
||||
|
||||
# print(len(stroke_list), 'strokes pasted')
|
||||
|
||||
|
||||
### OPERATORS
|
||||
|
@ -407,24 +383,24 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
class GPCLIP_OT_copy_strokes(bpy.types.Operator):
|
||||
bl_idname = "gp.copy_strokes"
|
||||
bl_label = "GP Copy strokes"
|
||||
bl_description = "Copy strokes to str in paperclip"
|
||||
bl_description = "Copy strokes to text in paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
#ct = check_pressure()
|
||||
#ct = check_radius()
|
||||
strokelist = copycut_strokes(copy=True, keep_empty=True)
|
||||
if not strokelist:
|
||||
self.report({'ERROR'},'rien a copier')
|
||||
self.report({'ERROR'}, 'Nothing to copy')
|
||||
return {"CANCELLED"}
|
||||
bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
|
||||
#if ct:
|
||||
|
@ -437,20 +413,20 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
|
|||
class GPCLIP_OT_cut_strokes(bpy.types.Operator):
|
||||
bl_idname = "gp.cut_strokes"
|
||||
bl_label = "GP Cut strokes"
|
||||
bl_description = "Cut strokes to str in paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Cut strokes to text in paperclip"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True)#ct = check_pressure()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
|
||||
if not strokelist:
|
||||
self.report({'ERROR'},'Nothing to cut')
|
||||
return {"CANCELLED"}
|
||||
|
@ -467,10 +443,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected to paste on')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
|
@ -493,14 +469,47 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
|||
|
||||
class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||
bl_idname = "gp.copy_multi_strokes"
|
||||
bl_label = "GP Copy multi strokes"
|
||||
bl_description = "Copy multiple layers>frames>strokes (unlocked and unhided ones) to str in paperclip"
|
||||
bl_label = "GP Copy Multi Strokes"
|
||||
bl_description = "Copy multiple layers>frames>strokes from selected layers to str in paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
radius : bpy.props.BoolProperty(name='radius', default=True,
|
||||
description='Dump point radius attribute (already skipped if at default value)')
|
||||
opacity : bpy.props.BoolProperty(name='opacity', default=True,
|
||||
description='Dump point opacity attribute (already skipped if at default value)')
|
||||
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
|
||||
description='Dump point vertex_color attribute (already skipped if at default value)')
|
||||
fill_color : bpy.props.BoolProperty(name='fill color', default=True,
|
||||
description='Dump point fill_color attribute (already skipped if at default value)')
|
||||
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
|
||||
description='Dump point fill_opacity attribute (already skipped if at default value)')
|
||||
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
|
||||
description='Dump point uv_factor attribute (already skipped if at default value)')
|
||||
rotation : bpy.props.BoolProperty(name='rotation', default=True,
|
||||
description='Dump point rotation attribute (already skipped if at default value)')
|
||||
|
||||
def invoke(self, context, event):
|
||||
# self.file_dump = event.ctrl
|
||||
return context.window_manager.invoke_props_dialog(self) # , width=400
|
||||
# return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout=self.layout
|
||||
layout.use_property_split = True
|
||||
col = layout.column()
|
||||
col.label(text='Keep following point attributes:')
|
||||
col.prop(self, 'radius')
|
||||
col.prop(self, 'opacity')
|
||||
col.prop(self, 'vertex_color')
|
||||
col.prop(self, 'fill_color')
|
||||
col.prop(self, 'fill_opacity')
|
||||
col.prop(self, 'rotation')
|
||||
return
|
||||
|
||||
def execute(self, context):
|
||||
bake_moves = True
|
||||
|
@ -510,10 +519,10 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
t0 = time()
|
||||
#ct = check_pressure()
|
||||
#ct = check_radius()
|
||||
layerdic = {}
|
||||
|
||||
layerpool = [l for l in gpl if not l.hide and l.select]# and not l.lock
|
||||
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
|
||||
if not layerpool:
|
||||
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
|
||||
return {"CANCELLED"}
|
||||
|
@ -525,17 +534,20 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
|
||||
frame_dic = {}
|
||||
for f in l.frames:
|
||||
if skip_empty_frame and not len(f.strokes):
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
continue
|
||||
context.scene.frame_set(f.frame_number) # use matrix of this frame
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
||||
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||
|
||||
frame_dic[f.frame_number] = strokelist
|
||||
|
||||
layerdic[l.info] = frame_dic
|
||||
layerdic[l.name] = frame_dic
|
||||
|
||||
else: # bake position: copy frame where object as moved even if frame is unchanged
|
||||
for l in layerpool:
|
||||
print('dump layer:', l.name)
|
||||
if not l.frames:
|
||||
continue# skip empty layers
|
||||
|
||||
|
@ -559,14 +571,17 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
break
|
||||
|
||||
## skip empty frame if specified
|
||||
if skip_empty_frame and not len(f.strokes):
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
continue
|
||||
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
||||
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||
|
||||
frame_dic[i] = strokelist
|
||||
|
||||
prevmat = curmat
|
||||
layerdic[l.info] = frame_dic
|
||||
layerdic[l.name] = frame_dic
|
||||
|
||||
## All to clipboard manager
|
||||
bpy.context.window_manager.clipboard = json.dumps(layerdic)
|
||||
|
@ -579,14 +594,14 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
|
||||
class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
|
||||
bl_idname = "gp.paste_multi_strokes"
|
||||
bl_label = "GP paste multi strokes"
|
||||
bl_description = "Paste multiple layers>frames>strokes from paperclip"
|
||||
bl_label = "GP Paste Multi Strokes"
|
||||
bl_description = "Paste multiple layers>frames>strokes from paperclip on active layer"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
org_frame = context.scene.frame_current
|
||||
|
@ -615,7 +630,7 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
|
|||
layer = gpl.new(layname)
|
||||
for fnum, fstrokes in allframes.items():
|
||||
context.scene.frame_set(int(fnum)) # use matrix of this frame for copying (maybe just evaluate depsgraph for object
|
||||
add_multiple_strokes(fstrokes, use_current_frame=False)#create a new frame at each encoutered
|
||||
add_multiple_strokes(fstrokes, use_current_frame=False) # create a new frame at each encoutered occurence
|
||||
|
||||
print('total_time', time() - t0)
|
||||
|
||||
|
@ -637,13 +652,16 @@ class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
|
|||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row(align=True)
|
||||
row.operator('gp.copy_strokes', text='Copy strokes', icon='COPYDOWN')
|
||||
row.operator('gp.cut_strokes', text='Cut strokes', icon='PASTEFLIPUP')
|
||||
layout.operator('gp.paste_strokes', text='Paste strokes', icon='PASTEDOWN')
|
||||
layout.separator()
|
||||
layout.operator('gp.copy_multi_strokes', text='Copy layers', icon='COPYDOWN')
|
||||
layout.operator('gp.paste_multi_strokes', text='Paste layers', icon='PASTEDOWN')
|
||||
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.operator('gp.copy_strokes', text='Copy Strokes', icon='COPYDOWN')
|
||||
row.operator('gp.cut_strokes', text='Cut Strokes', icon='PASTEFLIPUP')
|
||||
col.operator('gp.paste_strokes', text='Paste Strokes', icon='PASTEDOWN')
|
||||
# layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.operator('gp.copy_multi_strokes', text='Copy Layers', icon='COPYDOWN')
|
||||
col.operator('gp.paste_multi_strokes', text='Paste Layers', icon='PASTEDOWN')
|
||||
|
||||
###---TEST zone
|
||||
|
||||
|
@ -712,6 +730,9 @@ GPCLIP_PT_clipboard_ui,
|
|||
)
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
|
@ -719,6 +740,9 @@ def register():
|
|||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
unregister_keymaps()
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
|
||||
|
||||
## override all sursor snap shortcut with this in keymap
|
||||
|
@ -13,7 +15,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GPENCIL'
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
#print('-!SNAP!-')
|
||||
|
@ -23,7 +25,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
def execute(self, context):
|
||||
if not context.object or context.object.type != 'GPENCIL':
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
self.report({'INFO'}, 'Not GP, Cursor surface project')
|
||||
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
|
||||
return {"FINISHED"}
|
||||
|
@ -48,7 +50,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
if warning:
|
||||
self.report({'WARNING'}, ', '.join(warning))
|
||||
|
||||
plane_co, plane_no = get_gp_draw_plane(context)
|
||||
plane_co, plane_no = get_gp_draw_plane()
|
||||
|
||||
if not plane_co:#default to object location
|
||||
plane_co = context.object.matrix_world.to_translation()#context.object.location
|
||||
|
@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
|
|||
k.idname = new_idname
|
||||
|
||||
|
||||
# prev_matrix = mathutils.Matrix()
|
||||
prev_matrix = None
|
||||
|
||||
# @call_once(bpy.app.handlers.frame_change_post)
|
||||
|
||||
## used in properties file to register in boolprop update
|
||||
def cursor_follow_update(self, context):
|
||||
'''append or remove cursor_follow handler according a boolean'''
|
||||
ob = bpy.context.object
|
||||
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||
## override with target object is specified
|
||||
ob = bpy.context.scene.gptoolprops.cursor_follow_target
|
||||
global prev_matrix
|
||||
# imported in properties to register in boolprop update
|
||||
if self.cursor_follow:#True
|
||||
if ob:
|
||||
# out of below condition to be called when setting target as well
|
||||
prev_matrix = ob.matrix_world.copy()
|
||||
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||
if context.object:
|
||||
prev_matrix = context.object.matrix_world
|
||||
|
||||
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||
|
||||
else:#False
|
||||
|
@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
|
|||
|
||||
def cursor_follow(scene):
|
||||
'''Handler to make the cursor follow active object matrix changes on frame change'''
|
||||
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
|
||||
if not bpy.context.object:
|
||||
ob = bpy.context.object
|
||||
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||
## override with target object is specified
|
||||
ob = bpy.context.scene.gptoolprops.cursor_follow_target
|
||||
if not ob:
|
||||
return
|
||||
global prev_matrix
|
||||
ob = bpy.context.object
|
||||
current_matrix = ob.matrix_world
|
||||
if not prev_matrix:
|
||||
prev_matrix = current_matrix.copy()
|
||||
|
@ -147,14 +155,43 @@ def cursor_follow(scene):
|
|||
## translation only
|
||||
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
|
||||
|
||||
# print('offset:', (current_matrix - prev_matrix).to_translation())
|
||||
|
||||
## full
|
||||
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
|
||||
|
||||
# store for next use
|
||||
prev_matrix = current_matrix.copy()
|
||||
|
||||
prev_active_obj = None
|
||||
|
||||
## Add check for object selection change
|
||||
def selection_changed():
|
||||
"""Callback function for selection changes"""
|
||||
if not bpy.context.scene.gptoolprops.cursor_follow:
|
||||
return
|
||||
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||
# we are following a target, nothing to update on selection change
|
||||
return
|
||||
global prev_matrix, prev_active_obj
|
||||
if prev_active_obj != bpy.context.object:
|
||||
## Set stored matrix to active object
|
||||
prev_matrix = bpy.context.object.matrix_world.copy()
|
||||
prev_active_obj = bpy.context.object
|
||||
|
||||
## Note: Same owner as layer manager (will be removed as well)
|
||||
def subscribe_object_change():
|
||||
subscribe_to = (bpy.types.LayerObjects, 'active')
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
# owner of msgbus subcribe (for clearing later)
|
||||
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
|
||||
args=(),
|
||||
notify=selection_changed,
|
||||
options={'PERSISTENT'},
|
||||
)
|
||||
|
||||
@persistent
|
||||
def subscribe_object_change_handler(dummy):
|
||||
subscribe_object_change()
|
||||
|
||||
classes = (
|
||||
GPTB_OT_cusor_snap,
|
||||
|
@ -163,14 +200,18 @@ GPTB_OT_cusor_snap,
|
|||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
|
||||
|
||||
# bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||
## Follow cursor matrix update on object change
|
||||
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
|
||||
# ## Directly set msgbus to work at first addon activation # select_change
|
||||
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
|
||||
|
||||
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
|
||||
|
||||
|
||||
def unregister():
|
||||
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
||||
|
||||
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
|
||||
|
||||
|
@ -180,3 +221,5 @@ def unregister():
|
|||
# force remove handler if it's there at unregister
|
||||
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
|
@ -1,6 +1,5 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
import bgl
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import gpu
|
||||
|
@ -12,6 +11,7 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
|
|||
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
|
||||
from time import time
|
||||
from math import pi, cos, sin
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
|
||||
def get_gp_mat(gp, name, set_active=False):
|
||||
|
@ -190,7 +190,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def draw_callback_px(self):
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
gpu.state.blend_set('ALPHA')
|
||||
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
|
||||
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
|
||||
|
||||
|
@ -201,7 +201,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bg_color = area.spaces.active.shading.background_color
|
||||
#print(bg_color)
|
||||
|
||||
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (1, 1, 1, 1))
|
||||
for mouse, radius in self.mouse_path:
|
||||
|
@ -210,7 +210,7 @@ class GPTB_OT_eraser(Operator):
|
|||
batch.draw(shader)
|
||||
|
||||
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
|
||||
|
@ -232,7 +232,7 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
hld_stroke.points.add(count=1)
|
||||
p = hld_stroke.points[-1]
|
||||
p.co = mat_inv @ mouse_3d
|
||||
p.position = mat_inv @ mouse_3d
|
||||
p.pressure = search_radius * 2000
|
||||
|
||||
#context.scene.cursor.location = mouse_3d
|
||||
|
@ -253,14 +253,14 @@ class GPTB_OT_eraser(Operator):
|
|||
#print(self.cuts_data)
|
||||
|
||||
# for f in self.gp_frames:
|
||||
# for s in [s for s in f.strokes if s.material_index==self.hld_index]:
|
||||
# f.strokes.remove(s)
|
||||
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
|
||||
# f.drawing.strokes.remove(s)
|
||||
|
||||
#gp.data.materials.pop(index=self.hld_index)
|
||||
#bpy.data.materials.remove(self.hld_mat)
|
||||
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
|
||||
|
@ -282,7 +282,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
strokes = [s for f in self.gp_frames for s in f.strokes]
|
||||
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
|
||||
#print('select_circle', time()-t1)
|
||||
|
||||
t2 = time()
|
||||
|
@ -310,18 +310,18 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
|
||||
|
||||
new_p1 = stroke.points[p1_index+1]
|
||||
new_p1.co = mat_inv@intersects[0]
|
||||
new_p1.position = mat_inv@intersects[0]
|
||||
new_points += [(stroke, p1_index+1)]
|
||||
|
||||
#print('number_cuts', number_cuts)
|
||||
|
||||
if number_cuts == 2:
|
||||
new_p2 = stroke.points[p1_index+2]
|
||||
new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||
#new_points += [new_p2]
|
||||
|
||||
new_p3 = stroke.points[p1_index+3]
|
||||
new_p3.co = mat_inv@intersects[1]
|
||||
new_p3.position = mat_inv@intersects[1]
|
||||
new_points += [(stroke, p1_index+3)]
|
||||
|
||||
#print('subdivide', time() - t3)
|
||||
|
@ -330,7 +330,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
'''
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
|
||||
|
||||
bpy.ops.gpencil.select_less()
|
||||
|
@ -342,7 +342,7 @@ class GPTB_OT_eraser(Operator):
|
|||
'''
|
||||
|
||||
t4 = time()
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
|
||||
if selected_strokes:
|
||||
bpy.ops.gpencil.delete(type='POINTS')
|
||||
|
@ -359,9 +359,9 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
#bpy.ops.object.mode_set(mode='OBJECT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
|
||||
bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||
#selected_strokes = [s for s in self.gp_frame.strokes if s.select]
|
||||
#bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
|
||||
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
|
||||
def modal(self, context, event):
|
||||
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
|
@ -441,23 +441,23 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
t0 = time()
|
||||
gp_mats = gp.data.materials
|
||||
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
|
||||
self.gp_frames = [l.active_frame for l in gp_layers]
|
||||
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
|
||||
self.gp_frames = [l.current_frame() for l in gp_layers]
|
||||
'''
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
|
||||
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
|
||||
print('get_gp_points', time()-t0)
|
||||
|
||||
t0 = time()
|
||||
#points_data = [(s, f, m, p, get_screen_co(p.co, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||
points_data = [(s, f, m, p, org + ((matrix @ p.co)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||
points_data = [(s, f, m, p, org + ((matrix @ p.position)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||
print('points_to_2d', time()-t0)
|
||||
|
||||
#print(points_data)
|
||||
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
|
||||
|
||||
#for s, f, m, p, co in self.points_data:
|
||||
# p.co = co
|
||||
# p.position = co
|
||||
|
||||
|
||||
t0 = time()
|
||||
|
@ -482,7 +482,7 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
self.hld_strokes = []
|
||||
for f in self.gp_frames:
|
||||
hld_stroke = f.strokes.new()
|
||||
hld_stroke = f.drawing.strokes.new()
|
||||
hld_stroke.start_cap_mode = 'ROUND'
|
||||
hld_stroke.end_cap_mode = 'ROUND'
|
||||
hld_stroke.material_index = self.hld_index
|
||||
|
|
|
@ -1,20 +1,47 @@
|
|||
import bpy
|
||||
import os
|
||||
from pathlib import Path
|
||||
from .utils import show_message_box, get_addon_prefs
|
||||
import numpy as np
|
||||
|
||||
from . import utils
|
||||
|
||||
from bpy.props import (BoolProperty,
|
||||
PointerProperty,
|
||||
CollectionProperty,
|
||||
StringProperty)
|
||||
|
||||
def remove_stroke_exact_duplications(apply=True):
|
||||
'''Remove accidental stroke duplication (points exactly in the same place)
|
||||
:apply: Remove the duplication instead of just listing dupes
|
||||
return number of duplication found/deleted
|
||||
'''
|
||||
# TODO: add additional check of material (even if unlikely to happen)
|
||||
ct = 0
|
||||
gp_datas = [gp for gp in bpy.data.grease_pencils]
|
||||
for gp in gp_datas:
|
||||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
stroke_list = []
|
||||
for s in reversed(f.drawing.strokes):
|
||||
|
||||
point_list = [p.position for p in s.points]
|
||||
|
||||
if point_list in stroke_list:
|
||||
ct += 1
|
||||
if apply:
|
||||
# Remove redundancy
|
||||
f.drawing.strokes.remove(s)
|
||||
else:
|
||||
stroke_list.append(point_list)
|
||||
return ct
|
||||
class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
bl_idname = "gp.file_checker"
|
||||
bl_label = "File check"
|
||||
bl_label = "Check File"
|
||||
bl_description = "Check / correct some aspect of the file, properties and such and report"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.region_data.view_perspective == 'CAMERA'
|
||||
|
||||
## list of action :
|
||||
# Lock main cam:
|
||||
## list of actions :
|
||||
# Lock main cam
|
||||
# set scene res
|
||||
# set scene percentage at 100:
|
||||
# set show slider and sync range
|
||||
|
@ -23,13 +50,37 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
# GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
||||
# GP stroke placement/projection check
|
||||
# Disabled animation
|
||||
# Objects visibility conflict
|
||||
# Objects modifiers visibility conflict
|
||||
# GP modifiers broken target check
|
||||
# Set onion skin filter to 'All type'
|
||||
# Set filepath type
|
||||
# Set Lock object mode state
|
||||
# Disable use light on all object
|
||||
# Remove redundant strokes in frames
|
||||
|
||||
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
|
||||
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
|
||||
options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# need some self-control (I had to...)
|
||||
self.ctrl = event.ctrl
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
prefs = utils.get_addon_prefs()
|
||||
fix = prefs.fixprops
|
||||
problems = []
|
||||
|
||||
## Old method : Apply fixes based on pref (inverted by ctrl key)
|
||||
# # If Ctrl is pressed, invert behavior (invert boolean)
|
||||
# apply ^= self.ctrl
|
||||
|
||||
apply = self.apply_fixes
|
||||
if self.ctrl:
|
||||
apply = True
|
||||
|
||||
## Lock main cam:
|
||||
if fix.lock_main_cam:
|
||||
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
|
||||
|
@ -44,6 +95,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
triple = (True,True,True)
|
||||
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
|
||||
problems.append('Lock main camera')
|
||||
if apply:
|
||||
cam.lock_location = cam.lock_rotation = triple
|
||||
|
||||
## set scene res at pref res according to addon pref
|
||||
|
@ -52,18 +104,21 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
# TODO set (rx, ry) to camera resolution if specified in camera name
|
||||
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
|
||||
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
|
||||
if apply:
|
||||
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
|
||||
|
||||
## set scene percentage at 100:
|
||||
if fix.set_res_percentage:
|
||||
if context.scene.render.resolution_percentage != 100:
|
||||
problems.append('Resolution output to 100%')
|
||||
if apply:
|
||||
context.scene.render.resolution_percentage = 100
|
||||
|
||||
## set fps according to preferences settings
|
||||
if fix.set_fps:
|
||||
if context.scene.render.fps != prefs.fps:
|
||||
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
|
||||
if apply:
|
||||
context.scene.render.fps = prefs.fps
|
||||
|
||||
## set show slider and sync range
|
||||
|
@ -74,18 +129,17 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
if area.type == 'DOPESHEET_EDITOR':
|
||||
if hasattr(area.spaces[0], 'show_sliders'):
|
||||
setattr(area.spaces[0], 'show_sliders', True)
|
||||
|
||||
if hasattr(area.spaces[0], 'show_locked_time'):
|
||||
setattr(area.spaces[0], 'show_locked_time', True)
|
||||
|
||||
## set cursor type (according to prefs ?)
|
||||
if fix.set_cursor_type:
|
||||
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
|
||||
tool = prefs.select_active_tool
|
||||
## set cursor type
|
||||
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
|
||||
tool = fix.select_active_tool
|
||||
if tool != 'none':
|
||||
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
|
||||
bpy.ops.wm.tool_set_by_id(name=tool)# Tweaktoolcode
|
||||
problems.append(f'tool changed to {tool.split(".")[1]}')
|
||||
if apply:
|
||||
bpy.ops.wm.tool_set_by_id(name=tool) # Tweaktoolcode
|
||||
|
||||
# ## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
||||
# if not context.scene.tool_settings.use_gpencil_draw_additive:
|
||||
|
@ -102,6 +156,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
|
||||
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
|
||||
|
||||
## GP Use light disable
|
||||
if fix.set_gp_use_lights_off:
|
||||
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
|
||||
if gp_with_lights:
|
||||
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
|
||||
if apply:
|
||||
for o in gp_with_lights:
|
||||
o.use_grease_pencil_lights = False
|
||||
|
||||
## Disabled animation
|
||||
if fix.list_disabled_anim:
|
||||
|
@ -114,24 +176,102 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
fcu_ct += 1
|
||||
print(f"muted: {act.name} > {fcu.data_path}")
|
||||
if fcu_ct:
|
||||
problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
|
||||
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
|
||||
|
||||
## Object visibility conflict
|
||||
if fix.list_obj_vis_conflict:
|
||||
viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
if not (o.hide_get() == o.hide_viewport == o.hide_render):
|
||||
hv = 'No' if o.hide_get() else 'Yes'
|
||||
vp = 'No' if o.hide_viewport else 'Yes'
|
||||
rd = 'No' if o.hide_render else 'Yes'
|
||||
viz_ct += 1
|
||||
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
|
||||
if viz_ct:
|
||||
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
||||
|
||||
## GP modifiers visibility conflict
|
||||
if fix.list_gp_mod_vis_conflict:
|
||||
mod_viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
vp = 'Yes' if m.show_viewport else 'No'
|
||||
rd = 'Yes' if m.show_render else 'No'
|
||||
mod_viz_ct += 1
|
||||
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
|
||||
|
||||
if mod_viz_ct:
|
||||
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
|
||||
|
||||
## check if GP modifier have broken layer targets
|
||||
if fix.list_broken_mod_targets:
|
||||
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
|
||||
lay_name_list = [l.name for l in o.data.layers]
|
||||
for m in o.modifiers:
|
||||
if not hasattr(m, 'layer_filter'):
|
||||
continue
|
||||
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
|
||||
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
|
||||
print(mess)
|
||||
problems.append(mess)
|
||||
|
||||
## Use median point
|
||||
if fix.set_pivot_median_point:
|
||||
if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
|
||||
problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
|
||||
if apply:
|
||||
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
|
||||
|
||||
if fix.disable_guide:
|
||||
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
|
||||
problems.append(f"Disabled Draw Guide")
|
||||
if apply:
|
||||
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
|
||||
|
||||
if fix.autokey_add_n_replace:
|
||||
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
|
||||
problems.append(f"Autokey mode reset to 'Add & Replace'")
|
||||
if apply:
|
||||
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
|
||||
|
||||
if fix.file_path_type != 'none':
|
||||
pathes = []
|
||||
for p in bpy.utils.blend_paths():
|
||||
if fix.file_path_type == 'RELATIVE':
|
||||
if not p.startswith('//'):
|
||||
pathes.append(p)
|
||||
elif fix.file_path_type == 'ABSOLUTE':
|
||||
if p.startswith('//'):
|
||||
pathes.append(p)
|
||||
if pathes:
|
||||
mess = f'{len(pathes)}/{len(bpy.utils.blend_paths())} paths not {fix.file_path_type.lower()} (see console)'
|
||||
problems.append(mess)
|
||||
print(mess)
|
||||
print('\n'.join(pathes))
|
||||
print('-')
|
||||
|
||||
if fix.lock_object_mode != 'none':
|
||||
lockmode = bpy.context.scene.tool_settings.lock_object_mode
|
||||
if fix.lock_object_mode == 'LOCK':
|
||||
if not lockmode:
|
||||
problems.append(f"Lock object mode toggled On")
|
||||
if apply:
|
||||
bpy.context.scene.tool_settings.lock_object_mode = True
|
||||
|
||||
elif fix.lock_object_mode == 'UNLOCK':
|
||||
if lockmode:
|
||||
problems.append(f"Lock object mode toggled Off")
|
||||
if apply:
|
||||
bpy.context.scene.tool_settings.lock_object_mode = False
|
||||
|
||||
if fix.remove_redundant_strokes:
|
||||
ct = remove_stroke_exact_duplications(apply=apply)
|
||||
if ct > 0:
|
||||
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
|
||||
problems.append(mess)
|
||||
|
||||
# ## Set onion skin filter to 'All type'
|
||||
# fix_kf_type = 0
|
||||
# for gp in bpy.data.grease_pencils:#from data
|
||||
|
@ -143,7 +283,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
||||
|
||||
# for ob in context.scene.objects:#from object
|
||||
# if ob.type == 'GPENCIL':
|
||||
# if ob.type == 'GREASEPENCIL':
|
||||
# ob.data.onion_keyframe_type = 'ALL'
|
||||
|
||||
#### --- print fix/problems report
|
||||
|
@ -154,47 +294,92 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
print(p)
|
||||
else:
|
||||
print(p[0])
|
||||
|
||||
if not self.apply_fixes:
|
||||
## button to call the operator again with apply_fixes set to True
|
||||
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
|
||||
|
||||
# Show in viewport
|
||||
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO')
|
||||
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
|
||||
utils.show_message_box(problems, _title = title, _icon = 'INFO')
|
||||
else:
|
||||
self.report({'INFO'}, 'All good')
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
""" OLD links checker with show_message_box
|
||||
class GPTB_OT_links_checker(bpy.types.Operator):
|
||||
bl_idname = "gp.links_checker"
|
||||
bl_label = "Links check"
|
||||
bl_description = "Check states of file direct links"
|
||||
class GPTB_OT_copy_string_to_clipboard(bpy.types.Operator):
|
||||
bl_idname = "gp.copy_string_to_clipboard"
|
||||
bl_label = "Copy String"
|
||||
bl_description = "Copy passed string to clipboard"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
all_lnks = []
|
||||
has_broken_link = False
|
||||
## check for broken links
|
||||
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
||||
lfp = Path(lib)
|
||||
realib = Path(current)
|
||||
if not lfp.exists():
|
||||
has_broken_link = True
|
||||
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
|
||||
else:
|
||||
if realib.as_posix().startswith('//'):
|
||||
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
|
||||
else:
|
||||
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
|
||||
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
|
||||
|
||||
all_lnks.sort(key=lambda x: x[1], reverse=True)
|
||||
if all_lnks:
|
||||
print('===File check===')
|
||||
for p in all_lnks:
|
||||
if isinstance(p, str):
|
||||
print(p)
|
||||
else:
|
||||
print(p[0])
|
||||
# Show in viewport
|
||||
show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
|
||||
return {"FINISHED"} """
|
||||
def execute(self, context):
|
||||
if not self.string:
|
||||
# self.report({'ERROR'}, 'Nothing to copy')
|
||||
return {"CANCELLED"}
|
||||
bpy.context.window_manager.clipboard = self.string
|
||||
self.report({'INFO'}, f'Copied: {self.string}')
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_copy_multipath_clipboard(bpy.types.Operator):
|
||||
bl_idname = "gp.copy_multipath_clipboard"
|
||||
bl_label = "Choose Path to Copy"
|
||||
bl_description = "Copy Chosen Path"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not self.string:
|
||||
return {"CANCELLED"}
|
||||
self.pathes = []
|
||||
|
||||
try:
|
||||
absolute = os.path.abspath(bpy.path.abspath(self.string))
|
||||
abs_parent = os.path.dirname(os.path.abspath(bpy.path.abspath(self.string)))
|
||||
path_abs = str(Path(bpy.path.abspath(self.string)).resolve())
|
||||
|
||||
except:
|
||||
# case of invalid / non-accessable path
|
||||
bpy.context.window_manager.clipboard = self.string
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
self.pathes.append(('Raw Path', self.string))
|
||||
self.pathes.append(('Parent', os.path.dirname(self.string)))
|
||||
|
||||
if absolute != self.string:
|
||||
self.pathes.append(('Absolute', absolute))
|
||||
|
||||
if absolute != self.string:
|
||||
self.pathes.append(('Absolute Parent', abs_parent))
|
||||
|
||||
if absolute != path_abs:
|
||||
self.pathes.append(('Resolved',path_abs))
|
||||
|
||||
self.pathes.append(('File name', os.path.basename(self.string)))
|
||||
|
||||
maxlen = max(len(l[1]) for l in self.pathes)
|
||||
popup_width = 800
|
||||
if maxlen < 50:
|
||||
popup_width = 500
|
||||
elif maxlen > 100:
|
||||
popup_width = 1000
|
||||
return context.window_manager.invoke_props_dialog(self, width=popup_width)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
for l in self.pathes:
|
||||
split=col.split(factor=0.2, align=True)
|
||||
split.operator('gp.copy_string_to_clipboard', text=l[0], icon='COPYDOWN').string = l[1]
|
||||
split.label(text=l[1])
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_links_checker(bpy.types.Operator):
|
||||
|
@ -223,14 +408,28 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
|||
|
||||
|
||||
layout.separator()
|
||||
# layout = layout.column() # thinner linespace
|
||||
for l in self.all_lnks:
|
||||
if l[1] == 'LIBRARY_DATA_BROKEN':
|
||||
if l[1] == 'CANCEL':
|
||||
layout.label(text=l[0], icon=l[1])
|
||||
else:
|
||||
split=layout.split(factor=0.75)
|
||||
continue
|
||||
|
||||
if l[1] == 'LIBRARY_DATA_BROKEN':
|
||||
split=layout.split(factor=0.85)
|
||||
split.label(text=l[0], icon=l[1])
|
||||
split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
|
||||
split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()#os.path.abspath(bpy.path.abspath(dirname(l[0])))
|
||||
# layout.label(text=l[0], icon=l[1])
|
||||
else:
|
||||
split=layout.split(factor=0.70, align=True)
|
||||
split.label(text=l[0], icon=l[1])
|
||||
## resolve() return somethin different than os.path.abspath.
|
||||
# split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
|
||||
# split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()
|
||||
|
||||
split.operator('wm.path_open', text='Open Folder', icon='FILE_FOLDER').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).parent.as_posix()
|
||||
split.operator('wm.path_open', text='Open File', icon='FILE_TICK').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).as_posix()
|
||||
|
||||
split.operator('gp.copy_multipath_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0]
|
||||
# split.operator('gp.copy_string_to_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0] # copy blend path directly
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.all_lnks = []
|
||||
|
@ -239,19 +438,32 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
|||
abs_ct = 0
|
||||
rel_ct = 0
|
||||
## check for broken links
|
||||
viewed = []
|
||||
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
||||
lfp = Path(lib)
|
||||
realib = Path(current)
|
||||
# avoid relisting same path mutliple times
|
||||
if current in viewed:
|
||||
continue
|
||||
# TODO find a proper way to show the number of user of this path...
|
||||
viewed.append(current)
|
||||
|
||||
realib = Path(current) # path as-is
|
||||
lfp = Path(lib) # absolute path
|
||||
|
||||
try: # Try because some path may fail parsing
|
||||
if not lfp.exists():
|
||||
self.broke_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )
|
||||
else:
|
||||
if realib.as_posix().startswith('//'):
|
||||
rel_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )
|
||||
else:
|
||||
abs_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )
|
||||
except:
|
||||
self.broke_ct += 1
|
||||
self.all_lnks.append( (f"{current}" , 'CANCEL') ) # error accessing file
|
||||
|
||||
|
||||
if not self.all_lnks:
|
||||
self.report({'INFO'}, 'No external links in files')
|
||||
|
@ -273,50 +485,221 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
|||
print(p[0])
|
||||
# Show in viewport
|
||||
|
||||
maxlen = max(len(x) for x in viewed)
|
||||
|
||||
# if broke_ct == 0:
|
||||
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
|
||||
# return {"FINISHED"}
|
||||
try:
|
||||
self.proj = context.preferences.addons['pipe_sync'].preferences['local_folder']
|
||||
except:
|
||||
self.proj = None
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
popup_width = 800
|
||||
if maxlen < 50:
|
||||
popup_width = 500
|
||||
elif maxlen > 100:
|
||||
popup_width = 1000
|
||||
|
||||
'''### OLD
|
||||
class GPTB_OT_check_scene(bpy.types.Operator):
|
||||
bl_idname = "gp.scene_check"
|
||||
bl_label = "Check GP scene"
|
||||
bl_description = "Check and fix scene settings"
|
||||
self.proj = os.environ.get('PROJECT_ROOT')
|
||||
return context.window_manager.invoke_props_dialog(self, width=popup_width)
|
||||
|
||||
|
||||
|
||||
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
|
||||
bl_idname = "gp.list_viewport_render_visibility"
|
||||
bl_label = "List Viewport And Render Visibility Conflicts"
|
||||
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
def invoke(self, context, event):
|
||||
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def draw(self, context):
|
||||
# TODO: Add visibility check with viewlayer visibility as well
|
||||
layout = self.layout
|
||||
for o in self.ob_list:
|
||||
row = layout.row()
|
||||
row.label(text=o.name)
|
||||
row.prop(o, 'hide_viewport', text='', emboss=False) # invert_checkbox=True
|
||||
row.prop(o, 'hide_render', text='', emboss=False) # invert_checkbox=True
|
||||
|
||||
def execute(self, context):
|
||||
## check scene resolution / 100% / framerate
|
||||
context.scene.render.resolution_percentage = 100
|
||||
context.scene.render.resolution_x = 3072# define addon properties to make generic ?
|
||||
context.scene.render.resolution_y = 1620# define addon properties to make generic ?
|
||||
context.scene.render.fps = 24# define addon properties to make generic ?
|
||||
return {'FINISHED'}
|
||||
|
||||
## check GP datas name
|
||||
gp_os = [o for o in context.scene.objects if o.type == 'GPENCIL' if o.data.users == 1]#no multiple users
|
||||
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
|
||||
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
|
||||
bl_idname = "gp.sync_visibility_from_viewlayer"
|
||||
bl_label = "Sync Visibility From Viewlayer"
|
||||
bl_description = "Set viewport and render visibility to match viewlayer visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
for gpo in gp_os:
|
||||
if gpo.data.name.startswith('Stroke'):# dont touch already renamed group
|
||||
if gpo.data.name != gpo.name:
|
||||
print('renaming GP data:', gpo.data.name, '-->', gpo.name)
|
||||
gpo.data.name = gpo.name
|
||||
def execute(self, context):
|
||||
for obj in context.scene.objects:
|
||||
is_hidden = obj.hide_get() # Get viewlayer visibility
|
||||
obj.hide_viewport = is_hidden
|
||||
obj.hide_render = is_hidden
|
||||
return {'FINISHED'}
|
||||
|
||||
## disable autolock
|
||||
context.scene.tool_settings.lock_object_mode = False
|
||||
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
|
||||
bl_idname = "gp.sync_visibility_from_viewport"
|
||||
bl_label = "Sync Visibility From Viewport"
|
||||
bl_description = "Set viewlayer and render visibility to match viewport visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
return {"FINISHED"}
|
||||
'''
|
||||
def execute(self, context):
|
||||
for obj in context.scene.objects:
|
||||
is_hidden = obj.hide_viewport
|
||||
obj.hide_set(is_hidden)
|
||||
obj.hide_render = is_hidden
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
|
||||
bl_idname = "gp.sync_visibility_from_render"
|
||||
bl_label = "Sync Visibility From Render"
|
||||
bl_description = "Set viewlayer and viewport visibility to match render visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.scene.objects:
|
||||
is_hidden = obj.hide_render
|
||||
obj.hide_set(is_hidden)
|
||||
obj.hide_viewport = is_hidden
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
|
||||
bl_idname = "gp.sync_visibible_to_render"
|
||||
bl_label = "Sync Overall Viewport Visibility To Render"
|
||||
bl_description = "Set render visibility from"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.scene.objects:
|
||||
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
|
||||
obj.hide_render = not obj.visible_get()
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
|
||||
"""Property group to handle object visibility"""
|
||||
is_hidden: BoolProperty(
|
||||
name="Hide in Viewport",
|
||||
description="Toggle object visibility in viewport",
|
||||
get=lambda self: self.get("is_hidden", False),
|
||||
set=lambda self, value: self.set_visibility(value)
|
||||
)
|
||||
|
||||
object_name: StringProperty(name="Object Name")
|
||||
|
||||
def set_visibility(self, value):
|
||||
"""Set the visibility using hide_set()"""
|
||||
obj = bpy.context.view_layer.objects.get(self.object_name)
|
||||
if obj:
|
||||
obj.hide_set(value)
|
||||
self["is_hidden"] = value
|
||||
|
||||
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
|
||||
bl_idname = "gp.list_object_visibility_conflicts"
|
||||
bl_label = "List Object Visibility Conflicts"
|
||||
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Clear and rebuild both collections
|
||||
self.visibility_items.clear()
|
||||
|
||||
# Store objects with conflicts
|
||||
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
|
||||
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
|
||||
|
||||
# Create visibility items in same order
|
||||
for obj in objects_with_conflicts:
|
||||
item = self.visibility_items.add()
|
||||
item.object_name = obj.name
|
||||
item["is_hidden"] = obj.hide_get()
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
# Add sync buttons at the top
|
||||
row = layout.row(align=False)
|
||||
row.label(text="Sync All Visibility From:")
|
||||
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
|
||||
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
|
||||
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
|
||||
layout.separator()
|
||||
|
||||
col = layout.column()
|
||||
# We can safely iterate over visibility_items since objects are stored in same order
|
||||
for vis_item in self.visibility_items:
|
||||
obj = context.view_layer.objects.get(vis_item.object_name)
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
row = col.row(align=False)
|
||||
row.label(text=obj.name)
|
||||
|
||||
## Viewlayer visibility "as prop" to allow slide toggle
|
||||
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
|
||||
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
|
||||
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
|
||||
|
||||
# Direct object properties
|
||||
row.prop(obj, 'hide_viewport', text='', emboss=False)
|
||||
row.prop(obj, 'hide_render', text='', emboss=False)
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
|
||||
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
||||
bl_idname = "gp.list_modifier_visibility"
|
||||
bl_label = "List Objects Modifiers Visibility Conflicts"
|
||||
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ob_list = []
|
||||
for o in context.scene.objects:
|
||||
if not len(o.modifiers):
|
||||
continue
|
||||
mods = []
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
if not mods:
|
||||
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
|
||||
mods.append(m)
|
||||
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not self.ob_list:
|
||||
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
|
||||
return
|
||||
|
||||
for o in self.ob_list:
|
||||
layout.label(text=o[0].name, icon=o[2])
|
||||
for m in o[1]:
|
||||
row = layout.row()
|
||||
row.label(text='')
|
||||
row.label(text=m.name, icon='MODIFIER_ON')
|
||||
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
|
||||
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
classes = (
|
||||
# GPTB_OT_check_scene,
|
||||
GPTB_OT_list_viewport_render_visibility, # Only viewport and render
|
||||
GPTB_OT_sync_visibility_from_viewlayer,
|
||||
GPTB_OT_sync_visibility_from_viewport,
|
||||
GPTB_OT_sync_visibility_from_render,
|
||||
GPTB_OT_sync_visibible_to_render,
|
||||
GPTB_PG_object_visibility,
|
||||
GPTB_OT_list_object_visibility_conflicts,
|
||||
GPTB_OT_list_modifier_visibility,
|
||||
GPTB_OT_copy_string_to_clipboard,
|
||||
GPTB_OT_copy_multipath_clipboard,
|
||||
GPTB_OT_file_checker,
|
||||
GPTB_OT_links_checker,
|
||||
)
|
||||
|
|
|
@ -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)
|
422
OP_helpers.py
422
OP_helpers.py
|
@ -1,12 +1,19 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
import math
|
||||
|
||||
from time import ctime
|
||||
from mathutils import Vector #, Matrix
|
||||
from pathlib import Path
|
||||
from math import radians
|
||||
from .utils import get_gp_objects, set_collection, show_message_box
|
||||
from bpy.types import Operator
|
||||
|
||||
class GPTB_OT_copy_text(bpy.types.Operator):
|
||||
from .view3d_utils import View3D
|
||||
from . import utils
|
||||
|
||||
class GPTB_OT_copy_text(Operator):
|
||||
bl_idname = "wm.copytext"
|
||||
bl_label = "Copy to clipboard"
|
||||
bl_label = "Copy To Clipboard"
|
||||
bl_description = "Insert passed text to clipboard"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
|
@ -18,23 +25,53 @@ class GPTB_OT_copy_text(bpy.types.Operator):
|
|||
self.report({'INFO'}, mess)
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_flipx_view(bpy.types.Operator):
|
||||
bl_idname = "gp.mirror_flipx"
|
||||
bl_label = "cam mirror flipx"
|
||||
class GPTB_OT_flipx_view(Operator):
|
||||
bl_idname = "view3d.camera_mirror_flipx"
|
||||
bl_label = "Cam Mirror Flipx"
|
||||
bl_description = "Invert X scale on camera to flip image horizontally"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.region_data.view_perspective == 'CAMERA'
|
||||
return context.area.type == 'VIEW_3D' and \
|
||||
context.region_data.view_perspective == 'CAMERA'
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.camera.scale.x *= -1
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
|
||||
class GPTB_OT_view_camera_frame_fit(Operator):
|
||||
bl_idname = "view3d.view_camera_frame_fit"
|
||||
bl_label = "View Fit"
|
||||
bl_description = "Fit the camera in view (view 1:1)"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area.type == 'VIEW_3D' and \
|
||||
context.region_data.view_perspective == 'CAMERA'
|
||||
|
||||
def zoom_from_fac(self, zoomfac):
|
||||
from math import sqrt
|
||||
return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0
|
||||
|
||||
def execute(self, context):
|
||||
# Calculate zoom level to fit in view considering upper and side panel (Not done by native view 1:1)
|
||||
# context.space_data.region_3d.view_camera_zoom = 0 # (value range: -30, - 600)
|
||||
view3d = View3D()
|
||||
view3d.fit_camera_view()
|
||||
|
||||
## re-center
|
||||
# context.space_data.region_3d.view_camera_offset = (0,0)
|
||||
|
||||
# With a margin
|
||||
# Calculate pan to fit view in viewport
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_rename_data_from_obj(Operator):
|
||||
bl_idname = "gp.rename_data_from_obj"
|
||||
bl_label = "Rename GP from object"
|
||||
bl_label = "Rename GP From Object"
|
||||
bl_description = "Rename the GP datablock with the same name as the object"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
@ -42,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
if not self.rename_all:
|
||||
|
@ -56,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
|
|||
else:
|
||||
oblist = []
|
||||
for o in context.scene.objects:
|
||||
if o.type == 'GPENCIL':
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.name == o.data.name:
|
||||
continue
|
||||
oblist.append(f'{o.data.name} -> {o.name}')
|
||||
|
@ -113,9 +150,9 @@ def get_gp_alignement_vector(context):
|
|||
elif orient == 'CURSOR':
|
||||
return Vector((0,0,1))#.rotate(context.scene.cursor.matrix)
|
||||
|
||||
class GPTB_OT_draw_cam(bpy.types.Operator):
|
||||
class GPTB_OT_draw_cam(Operator):
|
||||
bl_idname = "gp.draw_cam_switch"
|
||||
bl_label = "Draw cam switch"
|
||||
bl_label = "Draw Cam Switch"
|
||||
bl_description = "switch between main camera and draw (manipulate) camera"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
@ -170,7 +207,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
|||
|
||||
# dcam_col = bpy.data.collections.get(camcol_name)
|
||||
# if not dcam_col:
|
||||
set_collection(drawcam, camcol_name)
|
||||
utils.set_collection(drawcam, camcol_name)
|
||||
|
||||
# Swap to it, unhide if necessary and hide previous
|
||||
context.scene.camera = maincam
|
||||
|
@ -188,7 +225,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
|||
if not drawcam:
|
||||
created=True
|
||||
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
|
||||
set_collection(drawcam, 'manip_cams')
|
||||
utils.set_collection(drawcam, 'manip_cams')
|
||||
|
||||
if dcam_name == 'draw_cam':
|
||||
drawcam.parent = maincam
|
||||
|
@ -197,10 +234,13 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
|||
drawcam.lock_location = (True,True,True)
|
||||
# drawcam.hide_viewport = True
|
||||
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout
|
||||
drawcam.data = maincam.data # get data from parent
|
||||
|
||||
# Hide the other passepartout to let only the custom OpenGL one
|
||||
maincam.data.show_passepartout = False
|
||||
|
||||
|
||||
else:
|
||||
# object cam
|
||||
if created:
|
||||
drawcam['maincam_name'] = context.scene.camera.name
|
||||
drawcam.parent = act
|
||||
|
@ -210,7 +250,7 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
|||
drawcam.parent = act
|
||||
vec = Vector((0,1,0))
|
||||
|
||||
if act.type == 'GPENCIL':
|
||||
if act.type == 'GREASEPENCIL':
|
||||
#change vector according to alignement
|
||||
vec = get_gp_alignement_vector(context)
|
||||
|
||||
|
@ -238,15 +278,16 @@ class GPTB_OT_draw_cam(bpy.types.Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_set_view_as_cam(bpy.types.Operator):
|
||||
class GPTB_OT_set_view_as_cam(Operator):
|
||||
bl_idname = "gp.set_view_as_cam"
|
||||
bl_label = "Cam at view"
|
||||
bl_label = "Cam At View"
|
||||
bl_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.region_data.view_perspective != 'CAMERA'# need to be out of camera
|
||||
return context.area.type == 'VIEW_3D' and \
|
||||
context.region_data.view_perspective != 'CAMERA'# need to be out of camera
|
||||
# return context.scene.camera and not context.scene.camera.name.startswith('Cam')
|
||||
|
||||
def execute(self, context):
|
||||
|
@ -279,9 +320,9 @@ class GPTB_OT_set_view_as_cam(bpy.types.Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_reset_cam_rot(bpy.types.Operator):
|
||||
class GPTB_OT_reset_cam_rot(Operator):
|
||||
bl_idname = "gp.reset_cam_rot"
|
||||
bl_label = "Reset rotation"
|
||||
bl_label = "Reset Rotation"
|
||||
bl_description = "Reset rotation of the draw manipulation camera"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
@ -290,52 +331,115 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator):
|
|||
return context.scene.camera and not context.scene.camera.name.startswith('Cam')
|
||||
# return context.region_data.view_perspective == 'CAMERA'# check if in camera
|
||||
|
||||
def execute(self, context):
|
||||
# dcam_name = 'draw_cam'
|
||||
# camcol_name = 'manip_cams'
|
||||
drawcam = context.scene.camera
|
||||
if drawcam.parent.type == 'CAMERA':
|
||||
## align to parent camera
|
||||
drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset
|
||||
# drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset
|
||||
elif drawcam.parent:
|
||||
## there is a parent, so align the Y of the camera to object's Z
|
||||
# drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong
|
||||
pass
|
||||
def get_center_view(self, context, cam):
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
frame = cam.data.view_frame()
|
||||
mat = cam.matrix_world
|
||||
frame = [mat @ v for v in frame]
|
||||
frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
|
||||
center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
|
||||
center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
|
||||
return mathutils.Vector((center_x, center_y))
|
||||
|
||||
def get_ui_ratio(self, context):
|
||||
'''correct ui overlap from header/toolbars'''
|
||||
regs = context.area.regions
|
||||
if context.preferences.system.use_region_overlap:
|
||||
w = context.area.width
|
||||
# minus tool header
|
||||
h = context.area.height - regs[0].height
|
||||
else:
|
||||
# minus tool leftbar + sidebar right
|
||||
w = context.area.width - regs[2].width - regs[3].width
|
||||
# minus tool header + header
|
||||
h = context.area.height - regs[0].height - regs[1].height
|
||||
|
||||
self.ratio = h / w
|
||||
self.ratio_inv = w / h
|
||||
|
||||
def execute(self, context):
|
||||
cam = context.scene.camera
|
||||
if not cam.parent or cam.parent.type != 'CAMERA':
|
||||
self.report({'ERROR'}, "No parents to refer to for rotation reset")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# store original rotation mode
|
||||
org_rotation_mode = cam.rotation_mode
|
||||
|
||||
# set to euler to works with quaternions, restored at finish
|
||||
cam.rotation_mode = 'XYZ'
|
||||
# store camera matrix world
|
||||
org_cam_matrix = cam.matrix_world.copy()
|
||||
|
||||
org_cam_z = cam.rotation_euler.z
|
||||
|
||||
## initialize current view_offset in camera
|
||||
view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset)
|
||||
|
||||
# Do the reset to parent transforms
|
||||
cam.matrix_world = cam.parent.matrix_world # wrong, get the parent rotation offset
|
||||
|
||||
# Get diff angle
|
||||
angle = cam.rotation_euler.z - org_cam_z
|
||||
# create rotation matrix with negative angle (we want to counter the move)
|
||||
neg = -angle
|
||||
rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]])
|
||||
|
||||
# restore original rotation mode
|
||||
cam.rotation_mode = org_rotation_mode
|
||||
|
||||
self.get_ui_ratio(context)
|
||||
# apply rotation matrix
|
||||
new_cam_offset = view_cam_offset.copy()
|
||||
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio)) # apply screen ratio
|
||||
new_cam_offset.rotate(rot_mat2d)
|
||||
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv)) # restore screen ratio
|
||||
|
||||
context.space_data.region_3d.view_camera_offset = new_cam_offset
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
|
||||
class GPTB_OT_toggle_mute_animation(Operator):
|
||||
bl_idname = "gp.toggle_mute_animation"
|
||||
bl_label = "Toggle animation mute"
|
||||
bl_label = "Toggle Animation Mute"
|
||||
bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
||||
mute : bpy.props.BoolProperty(default=False)
|
||||
skip_gp : bpy.props.BoolProperty(default=False)
|
||||
skip_obj : bpy.props.BoolProperty(default=False)
|
||||
mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selection = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def set_action_mute(self, act):
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
print(i, fcu.data_path, fcu.array_index)
|
||||
# fcu.group don't have mute attribute in api.
|
||||
fcu.mute = self.mute
|
||||
for g in act.groups:
|
||||
g.mute = self.mute
|
||||
|
||||
def execute(self, context):
|
||||
if self.selection:
|
||||
pool = context.selected_objects
|
||||
else:
|
||||
pool = context.scene.objects
|
||||
|
||||
for o in pool:
|
||||
if self.skip_gp and o.type == 'GPENCIL':
|
||||
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
if self.skip_obj and o.type != 'GPENCIL':
|
||||
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
continue
|
||||
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
||||
continue
|
||||
|
||||
# mute attribute animation for GP and cameras
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
print(f'\n---{o.name} data:')
|
||||
self.set_action_mute(gp_act)
|
||||
|
||||
if not o.animation_data:
|
||||
continue
|
||||
|
@ -343,15 +447,45 @@ class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
|
|||
if not act:
|
||||
continue
|
||||
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
print(i, fcu.data_path, fcu.array_index)
|
||||
fcu.mute = self.mute
|
||||
print(f'\n---{o.name}:')
|
||||
self.set_action_mute(act)
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_list_disabled_anims(bpy.types.Operator):
|
||||
class GPTB_OT_toggle_hide_gp_modifier(Operator):
|
||||
bl_idname = "gp.toggle_hide_gp_modifier"
|
||||
bl_label = "Toggle Modifier Hide"
|
||||
bl_description = "Show/Hide viewport on GP objects modifier\
|
||||
\nOnly touch modifier that are showed in render\
|
||||
\nShift + click to affect selection only"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
show : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selection = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
if self.selection:
|
||||
pool = context.selected_objects
|
||||
else:
|
||||
pool = context.scene.objects
|
||||
for o in pool:
|
||||
if o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
for m in o.modifiers:
|
||||
# skip modifier that are not visible in render
|
||||
if not m.show_render:
|
||||
continue
|
||||
m.show_viewport = self.show
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_list_disabled_anims(Operator):
|
||||
bl_idname = "gp.list_disabled_anims"
|
||||
bl_label = "List disabled anims"
|
||||
bl_label = "List Disabled Anims"
|
||||
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
@ -372,25 +506,41 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
|
|||
pool = context.scene.objects
|
||||
|
||||
for o in pool:
|
||||
# if self.skip_gp and o.type == 'GPENCIL':
|
||||
# if self.skip_gp and o.type == 'GREASEPENCIL':
|
||||
# continue
|
||||
# if self.skip_obj and o.type != 'GPENCIL':
|
||||
# if self.skip_obj and o.type != 'GREASEPENCIL':
|
||||
# continue
|
||||
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
for i, fcu in enumerate(gp_act.fcurves):
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
oblist.append(o)
|
||||
li.append(f'{o.name}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
|
||||
if not o.animation_data:
|
||||
continue
|
||||
act = o.animation_data.action
|
||||
if not act:
|
||||
continue
|
||||
|
||||
for g in act.groups:
|
||||
if g.mute:
|
||||
li.append(f'{o.name} - group: {g.name}')
|
||||
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
# print(i, fcu.data_path, fcu.array_index)
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
oblist.append(o)
|
||||
li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}')
|
||||
else:
|
||||
li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}')
|
||||
li.append(f'{o.name}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
if li:
|
||||
show_message_box(li)
|
||||
utils.show_message_box(li)
|
||||
else:
|
||||
self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}")
|
||||
return {'FINISHED'}
|
||||
|
@ -398,7 +548,7 @@ class GPTB_OT_list_disabled_anims(bpy.types.Operator):
|
|||
|
||||
## TODO presets are still not used... need to make a custom preset save/remove/quickload manager to be efficient (UIlist ?)
|
||||
|
||||
class GPTB_OT_overlay_presets(bpy.types.Operator):
|
||||
class GPTB_OT_overlay_presets(Operator):
|
||||
bl_idname = "gp.overlay_presets"
|
||||
bl_label = "Overlay presets"
|
||||
bl_description = "Overlay save/load presets for showing only whats needed"
|
||||
|
@ -453,7 +603,7 @@ class GPTB_OT_overlay_presets(bpy.types.Operator):
|
|||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_clear_active_frame(bpy.types.Operator):
|
||||
class GPTB_OT_clear_active_frame(Operator):
|
||||
bl_idname = "gp.clear_active_frame"
|
||||
bl_label = "Clear Active Frame"
|
||||
bl_description = "Delete all strokes in active frames"
|
||||
|
@ -461,7 +611,7 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
@ -469,32 +619,186 @@ class GPTB_OT_clear_active_frame(bpy.types.Operator):
|
|||
if not l:
|
||||
self.report({'ERROR'}, 'No layers')
|
||||
return {'CANCELLED'}
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
if not f:
|
||||
self.report({'ERROR'}, 'No active frame')
|
||||
return {'CANCELLED'}
|
||||
|
||||
ct = len(f.strokes)
|
||||
ct = len(f.drawing.strokes)
|
||||
if not ct:
|
||||
self.report({'ERROR'}, 'Active frame already empty')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for s in reversed(f.strokes):
|
||||
f.strokes.remove(s)
|
||||
for s in reversed(f.drawing.strokes):
|
||||
f.drawing.strokes.remove(s)
|
||||
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_check_canvas_alignement(Operator):
|
||||
bl_idname = "gp.check_canvas_alignement"
|
||||
bl_label = "Check Canvas Alignement"
|
||||
bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# if lock_axis is 'VIEW' then the draw axis is always aligned
|
||||
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
||||
|
||||
def execute(self, context):
|
||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
|
||||
self.report({'INFO'}, 'Drawing plane use "View" (always aligned)')
|
||||
return {'FINISHED'}
|
||||
|
||||
_angle, ret, message = utils.check_angle_from_view(obj=context.object, context=context)
|
||||
if not ret or not message:
|
||||
self.report({'ERROR'}, 'Could not get view angle infos')
|
||||
return {'CANCELLED'}
|
||||
|
||||
title = 'Aligned \o/' if ret == 'INFO' else "Not aligned !"
|
||||
|
||||
if context.region_data.view_perspective != 'CAMERA':
|
||||
title = title + ' ( not in camera view)'
|
||||
|
||||
utils.show_message_box(_message=message, _title=title, _icon=ret)
|
||||
# self.report({ret}, message)
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_step_select_frames(Operator):
|
||||
bl_idname = "gptb.step_select_frames"
|
||||
bl_label = "Step Select Frame"
|
||||
bl_description = "Select frames by a step frame value"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
start : bpy.props.IntProperty(name='Start Frame',
|
||||
description='Start frame of the step animation',
|
||||
default=100)
|
||||
|
||||
step : bpy.props.IntProperty(name='Step',
|
||||
description='Step of the frame, value of 2 select one frame on two',
|
||||
default=2,
|
||||
min=2)
|
||||
|
||||
strict : bpy.props.BoolProperty(name='Strict',
|
||||
description='Strictly select step frame from start to scene end range\
|
||||
\nElse reset step when a gap exsits already',
|
||||
default=False)
|
||||
|
||||
# TODO: add option to start at cursor (True default)
|
||||
|
||||
def invoke(self, context, execute):
|
||||
## list frame to keep
|
||||
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||
# return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, 'step')
|
||||
|
||||
col.prop(self, 'strict')
|
||||
if self.strict:
|
||||
col.prop(self, 'start')
|
||||
|
||||
## helper (need more work)
|
||||
# col.separator()
|
||||
# range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}"
|
||||
# col.label(text=f'Will keep {range_string}')
|
||||
if not self.strict:
|
||||
col.label(text=f'Each gap will be considered a new step start', icon='INFO')
|
||||
|
||||
def execute(self, context):
|
||||
numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)]
|
||||
self.to_select = numbers
|
||||
|
||||
## Negative switch : list frames to remove
|
||||
self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers]
|
||||
|
||||
gp = context.object.data
|
||||
|
||||
## Get frame summary (reset start after each existing gaps)
|
||||
key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames]))
|
||||
key_summary.sort()
|
||||
print('key summary: ', key_summary)
|
||||
|
||||
start = key_summary[0]
|
||||
if self.strict:
|
||||
to_select = self.to_select
|
||||
else:
|
||||
to_select = []
|
||||
prev = None
|
||||
for k in key_summary:
|
||||
print(k, prev)
|
||||
if prev is not None and k != prev + 1:
|
||||
## this is a gap ! new start
|
||||
prev = start = k
|
||||
# print('new start', start)
|
||||
continue
|
||||
|
||||
new_range = [i for i in range(start, key_summary[-1] + 1, self.step)]
|
||||
# print('new_range: ', new_range)
|
||||
if k not in new_range:
|
||||
to_select.append(k)
|
||||
|
||||
prev = k
|
||||
|
||||
## deselect all
|
||||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
f.select = False
|
||||
|
||||
print('To select:', to_select)
|
||||
gct = 0
|
||||
for i in to_select:
|
||||
ct = 0
|
||||
for l in gp.layers:
|
||||
frame = next((f for f in l.frames if f.frame_number == i), None)
|
||||
if not frame:
|
||||
continue
|
||||
|
||||
## Select instead of remove
|
||||
frame.select = True
|
||||
## Optionnally remove frames ?
|
||||
# l.frames.remove(frame)
|
||||
|
||||
ct += 1
|
||||
|
||||
# print(f'{i}: Selected {ct} frame(s)')
|
||||
gct += ct
|
||||
|
||||
self.report({'INFO'}, f'Selected {gct} frames')
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_open_addon_prefs(Operator):
|
||||
bl_idname = "gptb.open_addon_prefs"
|
||||
bl_label = "Open Addon Prefs"
|
||||
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
utils.open_addon_prefs()
|
||||
return {'FINISHED'}
|
||||
|
||||
classes = (
|
||||
GPTB_OT_copy_text,
|
||||
GPTB_OT_flipx_view,
|
||||
GPTB_OT_view_camera_frame_fit,
|
||||
GPTB_OT_rename_data_from_obj,
|
||||
GPTB_OT_draw_cam,
|
||||
GPTB_OT_set_view_as_cam,
|
||||
GPTB_OT_reset_cam_rot,
|
||||
GPTB_OT_toggle_mute_animation,
|
||||
GPTB_OT_toggle_hide_gp_modifier,
|
||||
GPTB_OT_list_disabled_anims,
|
||||
GPTB_OT_clear_active_frame,
|
||||
GPTB_OT_check_canvas_alignement,
|
||||
GPTB_OT_step_select_frames,
|
||||
GPTB_OT_open_addon_prefs,
|
||||
)
|
||||
|
||||
def register():
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_layer_list(self, context):
|
||||
'''return (identifier, name, description) of enum content'''
|
||||
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
if not context:
|
||||
return [('None', 'None','None')]
|
||||
if not context.object:
|
||||
return [('None', 'None','None')]
|
||||
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
# try:
|
||||
# except:
|
||||
# return [("", "", "")]
|
||||
|
@ -13,22 +18,31 @@ def get_layer_list(self, context):
|
|||
|
||||
class GPTB_OT_duplicate_send_to_layer(Operator) :
|
||||
bl_idname = "gp.duplicate_send_to_layer"
|
||||
bl_label = 'Duplicate and send to layer'
|
||||
bl_label = 'Duplicate Send To Layer'
|
||||
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
|
||||
# important to have the updated enum here as bl_property
|
||||
bl_property = "layers_enum"
|
||||
|
||||
|
||||
layers_enum : bpy.props.EnumProperty(
|
||||
name="Duplicate to layers",
|
||||
description="Duplicate selected keys in active layer and send them to choosen layer",
|
||||
items=get_layer_list
|
||||
description="Duplicate selected keys in active layer and send them to chosen layer",
|
||||
items=get_layer_list,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.delete_source:
|
||||
return f"Move selected keys in active layer to chosen layer"
|
||||
else:
|
||||
return f"Copy selected keys in active layer and send to chosen layer"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL' and context.space_data.ui_mode == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'\
|
||||
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
|
||||
|
||||
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
|
||||
|
||||
|
@ -50,31 +64,27 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
|
|||
|
||||
replaced = len(to_replace)
|
||||
|
||||
## remove overlapping frames
|
||||
## Remove overlapping frames
|
||||
for f in reversed(to_replace):
|
||||
target_layer.frames.remove(f)
|
||||
target_layer.frames.remove(f.frame_number)
|
||||
|
||||
## copy original frames
|
||||
## Copy original frames
|
||||
for f in selected_frames:
|
||||
target_layer.frames.copy(f)
|
||||
utils.copy_frame_at(f, target_layer, f.frame_number)
|
||||
# target_layer.frames.copy(f) # GPv2
|
||||
sent = len(selected_frames)
|
||||
|
||||
## delete original frames as an option
|
||||
## Delete original frames as an option
|
||||
if self.delete_source:
|
||||
for f in reversed(selected_frames):
|
||||
act_layer.frames.remove(f)
|
||||
|
||||
act_layer.frames.remove(f.frame_number)
|
||||
mess = f'{sent} keys moved'
|
||||
else:
|
||||
mess = f'{sent} keys copied'
|
||||
|
||||
if replaced:
|
||||
mess += f' ({replaced} replaced)'
|
||||
|
||||
# context.view_layer.update()
|
||||
# bpy.ops.gpencil.editmode_toggle()
|
||||
|
||||
mod = context.mode
|
||||
bpy.ops.gpencil.editmode_toggle()
|
||||
bpy.ops.object.mode_set(mode=mod)
|
||||
|
||||
self.report({'INFO'}, mess)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
@ -103,15 +113,13 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
|
|||
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
# pref = get_addon_prefs()
|
||||
# if not pref.kfj_use_shortcut:
|
||||
# return
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
# km = addon.keymaps.new(name = "Screen", space_type = "EMPTY")
|
||||
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", alt=False, ctrl=True, shift=True, any=False)
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
|
||||
addon_keymaps.append((km,kmi))
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", alt=False, ctrl=True, shift=True, any=False)
|
||||
|
||||
# km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter)
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True)
|
||||
kmi.properties.delete_source = True
|
||||
addon_keymaps.append((km,kmi))
|
||||
|
||||
|
@ -122,18 +130,35 @@ def unregister_keymaps():
|
|||
addon_keymaps.clear()
|
||||
|
||||
|
||||
def menu_duplicate_and_send_to_layer(self, context):
|
||||
if context.space_data.ui_mode == 'GPENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN'
|
||||
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
|
||||
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
|
||||
|
||||
classes = (
|
||||
GPTB_OT_duplicate_send_to_layer,
|
||||
)
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
register_keymaps()
|
||||
bpy.types.DOPESHEET_MT_key.append(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
|
||||
unregister_keymaps()
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -1,5 +1,5 @@
|
|||
import bpy
|
||||
from .utils import get_addon_prefs
|
||||
from .utils import get_addon_prefs, is_locked, is_hidden
|
||||
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
|
||||
|
||||
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||
|
@ -10,7 +10,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
next : BoolProperty(
|
||||
name="Next GP keyframe", description="Go to next active GP keyframe",
|
||||
|
@ -36,6 +36,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
|
||||
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
|
||||
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
|
||||
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
|
||||
))
|
||||
|
||||
def execute(self, context):
|
||||
|
@ -44,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
return {"CANCELLED"}
|
||||
|
||||
if self.target == 'ACTIVE':
|
||||
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
|
||||
gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
|
||||
if not context.object.data.layers.active in gpl:
|
||||
gpl.append(context.object.data.layers.active)
|
||||
|
||||
elif self.target == 'VISIBLE':
|
||||
gpl = [l for l in context.object.data.layers if not l.hide]
|
||||
gpl = [l for l in context.object.data.layers if not is_hidden(l)]
|
||||
|
||||
elif self.target == 'ACCESSIBLE':
|
||||
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
|
||||
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
|
||||
if self.keyframe_type != 'NONE':
|
||||
# use shortcut choice override
|
||||
|
@ -171,13 +172,14 @@ def register_keymaps():
|
|||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name = "Screen", space_type = "EMPTY")
|
||||
|
||||
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_next_keycode, value="PRESS", alt=pref.kfj_next_alt, ctrl=pref.kfj_next_ctrl, shift=pref.kfj_next_shift, any=False)
|
||||
kmi.properties.next = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_prev_keycode, value="PRESS", alt=pref.kfj_prev_alt, ctrl=pref.kfj_prev_ctrl, shift=pref.kfj_prev_shift, any=False)
|
||||
kmi.properties.next = False
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
kmi = km.keymap_items.new('screen.gp_keyframe_jump', type=pref.kfj_next_keycode, value="PRESS", alt=pref.kfj_next_alt, ctrl=pref.kfj_next_ctrl, shift=pref.kfj_next_shift, any=False)
|
||||
kmi.properties.next = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
# print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg
|
||||
for km, kmi in addon_keymaps:
|
||||
|
|
|
@ -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)
|
|
@ -5,7 +5,6 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper
|
|||
from pathlib import Path
|
||||
from .utils import convert_attr, get_addon_prefs
|
||||
|
||||
|
||||
### --- Json serialized material load/save
|
||||
|
||||
def load_palette(context, filepath):
|
||||
|
@ -43,7 +42,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# Start Clean (delete unuesed sh*t)
|
||||
|
@ -83,7 +82,7 @@ class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.json'
|
||||
|
||||
|
@ -111,7 +110,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||
|
||||
|
@ -130,6 +129,8 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
|||
dic = {}
|
||||
allmat=[]
|
||||
for mat in ob.data.materials:
|
||||
if not mat:
|
||||
continue
|
||||
if not mat.is_grease_pencil:
|
||||
continue
|
||||
if mat in allmat:
|
||||
|
@ -165,12 +166,10 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
|||
|
||||
def load_blend_palette(context, filepath):
|
||||
'''Load materials on current active object from current chosen blend'''
|
||||
#from pathlib import Path
|
||||
#palette_fp = C.preferences.addons['GP_toolbox'].preferences['palette_path']
|
||||
#fp = Path(palette_fp) / 'christina.blend'
|
||||
|
||||
print(f'-- import palette from : {filepath} --')
|
||||
for ob in context.selected_objects:
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
print(f'{ob.name} not a GP object')
|
||||
continue
|
||||
|
||||
|
@ -225,7 +224,7 @@ class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
|
@ -253,7 +252,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
|
@ -261,7 +260,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
|||
self.report({'ERROR'}, 'No materials to transfer')
|
||||
return {"CANCELLED"}
|
||||
|
||||
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
|
||||
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
|
||||
|
||||
if not selection:
|
||||
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
|
||||
|
@ -314,7 +313,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ob = context.object
|
||||
|
@ -355,7 +354,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
import re
|
||||
diff_ct = 0
|
||||
todel = []
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not hasattr(ob, 'material_slots'):
|
||||
return
|
||||
|
@ -411,7 +410,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
# iterate in all strokes and replace with new_mat_id
|
||||
for l in ob.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
if s.material_index == i:
|
||||
s.material_index = new_mat_id
|
||||
|
||||
|
@ -428,7 +427,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
# if self.skip_binded_empty_slots:
|
||||
# for l in ob.data.layers:
|
||||
# for f in l.frames:
|
||||
# for s in f.strokes:
|
||||
# for s in f.drawing.strokes:
|
||||
# if s.material_index == i:
|
||||
# is_binded = True
|
||||
# break
|
||||
|
@ -460,7 +459,6 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = (
|
||||
GPTB_OT_load_palette,
|
||||
GPTB_OT_save_palette,
|
||||
|
@ -468,6 +466,7 @@ GPTB_OT_load_default_palette,
|
|||
GPTB_OT_load_blend_palette,
|
||||
GPTB_OT_copy_active_to_selected_palette,
|
||||
GPTB_OT_clean_material_stack,
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
"""
|
||||
|
||||
'''
|
||||
def render_with_restore():
|
||||
class RenderFileRestorer:
|
||||
rd = bpy.context.scene.render
|
||||
|
@ -80,11 +82,51 @@ def render_with_restore():
|
|||
|
||||
|
||||
return RenderFileRestorer()
|
||||
'''
|
||||
|
||||
|
||||
class render_with_restore:
|
||||
def __init__(self):
|
||||
rd = bpy.context.scene.render
|
||||
im = rd.image_settings
|
||||
ff = rd.ffmpeg
|
||||
# ffmpeg (ff) need to be before image_settings(im) in list
|
||||
# otherwise __exit__ may try to restore settings of image mode in video mode !
|
||||
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
|
||||
self.zones = [rd, ff, im]
|
||||
|
||||
self.val_dic = {}
|
||||
self.cam = bpy.context.scene.camera
|
||||
|
||||
def __enter__(self):
|
||||
## store attribute of data_path in self.zones list.
|
||||
for data_path in self.zones:
|
||||
self.val_dic[data_path] = {}
|
||||
for attr in dir(data_path):#iterate in attribute of given datapath
|
||||
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
|
||||
self.val_dic[data_path][attr] = getattr(data_path, attr)
|
||||
|
||||
if self.cam and self.cam.name == 'draw_cam':
|
||||
if self.cam.parent:
|
||||
bpy.context.scene.camera = self.cam.parent
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
## restore attribute from self.zones list
|
||||
for data_path, prop_dic in self.val_dic.items():
|
||||
for attr, val in prop_dic.items():
|
||||
try:
|
||||
setattr(data_path, attr, val)
|
||||
except Exception as e:
|
||||
print(f"/!\ Impossible to re-assign: {attr} = {val}")
|
||||
print(e)
|
||||
|
||||
if self.cam:
|
||||
bpy.context.scene.camera = self.cam
|
||||
|
||||
def playblast(viewport = False, stamping = True):
|
||||
scn = bpy.context.scene
|
||||
res_factor = scn.gptoolprops.resolution_percentage
|
||||
playblast_path = get_addon_prefs().playblast_path
|
||||
rd = scn.render
|
||||
ff = rd.ffmpeg
|
||||
with render_with_restore():
|
||||
|
@ -116,13 +158,18 @@ def playblast(viewport = False, stamping = True):
|
|||
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
|
||||
blend = Path(bpy.data.filepath)
|
||||
date_format = "%Y-%m-%d_%H-%M-%S"
|
||||
fp = join(blend.parent, "images", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
|
||||
|
||||
## old direct place
|
||||
# fp = join(blend.parent, "playblast", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
|
||||
|
||||
fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'playblast_{blend.stem}_{strftime(date_format)}.mp4'
|
||||
fp = str(fp)
|
||||
|
||||
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
|
||||
rd.filepath = fp
|
||||
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
|
||||
#stamp options
|
||||
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
|
||||
rd.stamp_font_size = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
|
||||
|
||||
# bpy.ops.render.render_wrap(use_view=viewport)
|
||||
### render
|
||||
|
|
|
@ -22,13 +22,18 @@ exclude = (
|
|||
)
|
||||
|
||||
def delete_file(filepath):
|
||||
fp = Path(filepath)
|
||||
if fp.exists() and fp.is_file():
|
||||
try:
|
||||
if os.path.isfile(filepath) :
|
||||
print('removing', filepath)
|
||||
os.remove(filepath)
|
||||
print('removing', fp)
|
||||
fp.unlink(missing_ok=False)
|
||||
# os.remove(fp)
|
||||
return True
|
||||
except PermissionError:
|
||||
print(f'impossible to remove {filepath}')
|
||||
print(f'impossible to remove (permission error): {fp}')
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print(f'Impossible to remove (file not found error): {fp}')
|
||||
return False
|
||||
|
||||
# render function
|
||||
|
@ -58,7 +63,7 @@ def render_function(cmd, total_frame, scene) :
|
|||
frame_count += 1
|
||||
try :
|
||||
# print('frame_count: ', frame_count, 'total_frame: ', total_frame)
|
||||
bpy.context.window_manager.pblast_completion = frame_count / total_frame * 100
|
||||
bpy.context.window_manager.pblast_completion = int(frame_count / total_frame * 100)
|
||||
except AttributeError :
|
||||
#debug
|
||||
if debug : print("AttributeError avoided")
|
||||
|
@ -231,9 +236,7 @@ class BGBLAST_OT_playblast_modal_check(bpy.types.Operator):
|
|||
|
||||
self.report({'INFO'}, "Render Finished")
|
||||
|
||||
|
||||
### classic sauce
|
||||
|
||||
"""
|
||||
def render_with_restore():
|
||||
class RenderFileRestorer:
|
||||
rd = bpy.context.scene.render
|
||||
|
@ -266,6 +269,38 @@ def render_with_restore():
|
|||
print(e)
|
||||
|
||||
return RenderFileRestorer()
|
||||
"""
|
||||
|
||||
class render_with_restore:
|
||||
def __init__(self):
|
||||
rd = bpy.context.scene.render
|
||||
im = rd.image_settings
|
||||
ff = rd.ffmpeg
|
||||
# ffmpeg (ff) need to be before image_settings(im) in list
|
||||
# otherwise __exit__ may try to restore settings of image mode in video mode !
|
||||
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
|
||||
|
||||
self.zones = [rd, ff, im]
|
||||
|
||||
self.val_dic = {}
|
||||
|
||||
def __enter__(self):
|
||||
## store attribute of data_path in self.zones list.
|
||||
for data_path in self.zones:
|
||||
self.val_dic[data_path] = {}
|
||||
for attr in dir(data_path):#iterate in attribute of given datapath
|
||||
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
|
||||
self.val_dic[data_path][attr] = getattr(data_path, attr)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
## restore attribute from self.zones list
|
||||
for data_path, prop_dic in self.val_dic.items():
|
||||
for attr, val in prop_dic.items():
|
||||
try:
|
||||
setattr(data_path, attr, val)
|
||||
except Exception as e:
|
||||
print(f"/!\ Impossible to re-assign: {attr} = {val}")
|
||||
print(e)
|
||||
|
||||
|
||||
def playblast(context, viewport = False, stamping = True):
|
||||
|
@ -274,6 +309,7 @@ def playblast(context, viewport = False, stamping = True):
|
|||
rd = scn.render
|
||||
ff = rd.ffmpeg
|
||||
|
||||
playblast_path = get_addon_prefs().playblast_path
|
||||
prefix = 'tempblast_'
|
||||
|
||||
# delete old playblast and blend files
|
||||
|
@ -316,16 +352,20 @@ def playblast(context, viewport = False, stamping = True):
|
|||
# rd.is_movie_format# check if its movie mode
|
||||
|
||||
## set filepath
|
||||
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
|
||||
blend = Path(bpy.data.filepath)
|
||||
# mode incremental or just use fulldate (cannot create conflict and filter OK but long name)
|
||||
date_format = "%Y-%m-%d_%H-%M-%S"
|
||||
fp = join(blend.parent, "playblast", f'{prefix}{blend.stem}_{strftime(date_format)}.mp4')
|
||||
## old
|
||||
blend = Path(bpy.data.filepath)
|
||||
# fp = blend.parent / "playblast" / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4'
|
||||
|
||||
## with path variable
|
||||
fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4'
|
||||
fp = str(fp)
|
||||
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
|
||||
rd.filepath = fp
|
||||
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
|
||||
#stamp options
|
||||
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
|
||||
rd.stamp_font_size = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
|
||||
|
||||
|
||||
# get total number of frames
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from .utils import get_gp_objects, get_gp_datas, get_addon_prefs
|
||||
import bpy
|
||||
from .utils import get_gp_datas, get_addon_prefs, translate_range
|
||||
|
||||
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
|
||||
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
|
||||
|
||||
def get_hue_by_name(name, offset=0):
|
||||
'''
|
||||
|
@ -76,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
# namespace_order
|
||||
namespaces=[]
|
||||
for l in gpl:
|
||||
ns= l.info.lower().split(separator, 1)[0]
|
||||
ns= l.name.lower().split(separator, 1)[0]
|
||||
if ns not in namespaces:
|
||||
namespaces.append(ns)
|
||||
|
||||
|
@ -90,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
### step from 0.1 to 0.9
|
||||
|
||||
for i, l in enumerate(gpl):
|
||||
if l.info.lower() not in ('background',):
|
||||
if l.name.lower() not in ('background',):
|
||||
print()
|
||||
print('>', l.info)
|
||||
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
|
||||
print('>', l.name)
|
||||
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
|
||||
print("namespace", ns)#Dbg
|
||||
|
||||
if context.scene.gptoolprops.autotint_namespace:
|
||||
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
|
||||
h = get_hue_by_name(ns, hue_offset)#l.name == individuels
|
||||
|
||||
else:
|
||||
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
|
||||
|
@ -127,3 +125,10 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
def invoke(self, context, event):
|
||||
self.autotint_offset = context.scene.gptoolprops.autotint_offset
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GPT_OT_auto_tint_gp_layers)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(GPT_OT_auto_tint_gp_layers)
|
179
OP_realign.py
179
OP_realign.py
|
@ -1,10 +1,13 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
import numpy as np
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
from math import pi
|
||||
import numpy as np
|
||||
from time import time
|
||||
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
from . import utils
|
||||
from .utils import is_hidden, is_locked
|
||||
|
||||
def get_scale_matrix(scale):
|
||||
# recreate a neutral mat scale
|
||||
|
@ -16,46 +19,74 @@ def get_scale_matrix(scale):
|
|||
|
||||
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject - ops method
|
||||
:all_stroke: affect hided, locked layers
|
||||
:all_stroke: affect hidden, locked layers
|
||||
'''
|
||||
|
||||
if restore_frame:
|
||||
oframe = bpy.context.scene.frame_current
|
||||
omode = bpy.context.mode
|
||||
|
||||
# FIXME : if all_stroke is False, might be better to still store>set>restore "lock_frame"
|
||||
if all_strokes:
|
||||
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
|
||||
for l in obj.data.layers:
|
||||
l.hide = False
|
||||
l.lock = False
|
||||
l.lock_frame = False
|
||||
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
|
||||
frame_list = list(set(frame_list))
|
||||
frame_list.sort()
|
||||
|
||||
for l in obj.data.layers:
|
||||
for f in l.frames:
|
||||
if not len(f.strokes):
|
||||
scn = bpy.context.scene
|
||||
for i in frame_list:
|
||||
scn.frame_set(i) # refresh scene
|
||||
# scn.frame_current = i # no refresh
|
||||
|
||||
origin = scn.camera.matrix_world.to_translation()
|
||||
matrix_inv = obj.matrix_world.inverted()
|
||||
# origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
|
||||
# matrix = np.array(obj.matrix_world, dtype='float64')
|
||||
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
|
||||
#mat = src.matrix_world
|
||||
for layer in obj.data.layers:
|
||||
if not all_strokes:
|
||||
if not layer.select:
|
||||
continue
|
||||
if is_hidden(layer) or is_locked(layer):
|
||||
continue
|
||||
bpy.context.scene.frame_set(f.frame_number)
|
||||
|
||||
# switch to edit to reproject through ops
|
||||
bpy.ops.gpencil.select_all(action='SELECT')
|
||||
bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW
|
||||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
frame = next((f for f in layer.frames if f.frame_number == i), None)
|
||||
if frame is None:
|
||||
print(layer.name, 'Not found')
|
||||
# FIXME: some strokes are ignored
|
||||
# print(frame'skip {layer.name}, no frame at {i}')
|
||||
continue
|
||||
|
||||
# restore
|
||||
if all_strokes:
|
||||
for layer, hide, lock, lock_frame in layers_state:
|
||||
layer.hide = hide
|
||||
layer.lock = lock
|
||||
layer.lock_frame = lock_frame
|
||||
for s in frame.drawing.strokes:
|
||||
# print(layer.name, s.material_index)
|
||||
|
||||
bpy.ops.object.mode_set(mode=omode)
|
||||
## Batch matrix apply (Here is slower than list comprehension).
|
||||
# nb_points = len(s.points)
|
||||
# coords = np.empty(nb_points * 3, dtype='float64')
|
||||
# s.points.foreach_get('co', coords)
|
||||
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
|
||||
|
||||
## list comprehension method
|
||||
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
|
||||
|
||||
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
|
||||
|
||||
# Basic method (Slower than foreach_set and compatible with GPv3)
|
||||
## TODO: use low level api with curve offsets...
|
||||
for pt_index, point in enumerate(s.points):
|
||||
point.position = matrix_inv @ new_world_co_3d[pt_index]
|
||||
|
||||
## GPv2: ravel and use foreach_set
|
||||
## Ravel new coordinate on the fly
|
||||
## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
|
||||
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
|
||||
# s.points.foreach_set('co', new_local_coords)
|
||||
|
||||
if restore_frame:
|
||||
bpy.context.scene.frame_current = oframe
|
||||
|
||||
## Update the layer and redraw all viewports
|
||||
obj.data.layers.update()
|
||||
utils.refresh_areas()
|
||||
|
||||
def align_global(reproject=True, ref=None, all_strokes=True):
|
||||
|
||||
|
@ -87,24 +118,24 @@ def align_global(reproject=True, ref=None, all_strokes=True):
|
|||
# world_coords = []
|
||||
for l in o.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
|
||||
# print('coords: ', coords)
|
||||
# print([co for v in coords for co in v])
|
||||
s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
# s.points.update() # seem to works # but adding/deleting a point is "safer"
|
||||
## force update
|
||||
s.points.add(1)
|
||||
s.points.pop()
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
|
||||
# for p in s.points:
|
||||
## GPv2
|
||||
# s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
# # s.points.update() # seem to works # but adding/deleting a point is "safer"
|
||||
# ## force update
|
||||
# s.points.add(1)
|
||||
# s.points.pop()
|
||||
|
||||
for p in s.points:
|
||||
## GOOD :
|
||||
# world_co = mat @ p.co
|
||||
# p.co = new_mat.inverted() @ world_co
|
||||
# world_co = mat @ p.position
|
||||
# p.position = new_mat.inverted() @ world_co
|
||||
|
||||
## GOOD :
|
||||
# p.co = p.co @ mat.inverted() @ new_mat
|
||||
p.position = p.position @ mat.inverted() @ new_mat
|
||||
|
||||
if o.parent:
|
||||
o.matrix_world = new_mat
|
||||
|
@ -127,7 +158,7 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
|||
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
||||
|
||||
## double list keys
|
||||
rot_keys = [k.co.x for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||
|
||||
## normal iter
|
||||
# for fcu in o.animation_data.action.fcurves:
|
||||
|
@ -164,9 +195,9 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
|||
scale_mat = get_scale_matrix(o_scale)
|
||||
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
# print('coords: ', coords)
|
||||
# print([co for v in coords for co in v])
|
||||
s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
|
@ -215,7 +246,7 @@ class GPTB_OT_realign(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
reproject : bpy.props.BoolProperty(
|
||||
name='Reproject', default=True,
|
||||
|
@ -231,8 +262,8 @@ class GPTB_OT_realign(bpy.types.Operator):
|
|||
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.object.data.use_multiedit:
|
||||
self.report({'ERROR'}, 'Does not work in Multi-edit')
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multiframe mode')
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.alert = ''
|
||||
|
@ -313,24 +344,25 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
all_strokes : bpy.props.BoolProperty(
|
||||
name='All Strokes', default=True,
|
||||
description='Hided and locked layer will also be reprojected')
|
||||
|
||||
type : bpy.props.EnumProperty(name='Type',
|
||||
items=(('FRONT', "Front", ""),
|
||||
items=(('CURRENT', "Current", ""),
|
||||
('FRONT', "Front", ""),
|
||||
('SIDE', "Side", ""),
|
||||
('TOP', "Top", ""),
|
||||
('VIEW', "View", ""),
|
||||
('SURFACE', "Surface", ""),
|
||||
('CURSOR', "Cursor", ""),
|
||||
# ('SURFACE', "Surface", ""),
|
||||
),
|
||||
default='FRONT')
|
||||
default='CURRENT')
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.object.data.use_multiedit:
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multi-edit')
|
||||
return {"CANCELLED"}
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
@ -338,15 +370,36 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not context.region_data.view_perspective == 'CAMERA':
|
||||
layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||
layout.label(text='Reprojection is made from camera', icon='ERROR')
|
||||
layout.prop(self, "all_strokes")
|
||||
layout.prop(self, "type")
|
||||
layout.prop(self, "type", text='Project Axis')
|
||||
|
||||
## Hint show axis
|
||||
if self.type == 'CURRENT':
|
||||
## Show as prop
|
||||
# row = layout.row()
|
||||
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
|
||||
# row.enabled = False
|
||||
|
||||
orient = {
|
||||
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
|
||||
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
|
||||
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
|
||||
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
|
||||
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
|
||||
}
|
||||
box = layout.box()
|
||||
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
|
||||
box.label(text=orient[axis][0], icon=orient[axis][1])
|
||||
|
||||
def execute(self, context):
|
||||
t0 = time()
|
||||
orient = self.type
|
||||
if self.type == 'CURRENT':
|
||||
orient = None
|
||||
|
||||
batch_reproject(context.object, proj_type=self.type, all_strokes=self.all_strokes, restore_frame=True)
|
||||
batch_reproject(context.object, proj_type=orient, all_strokes=self.all_strokes, restore_frame=True)
|
||||
|
||||
self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
|
||||
|
||||
|
@ -355,12 +408,14 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
### -- MENU ENTRY --
|
||||
|
||||
def reproject_clean_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL':
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
|
||||
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
def reproject_context_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='SMALL_TRI_RIGHT_VEC') # KEYTYPE_JITTER_VEC
|
||||
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
classes = (
|
||||
GPTB_OT_realign,
|
||||
|
@ -371,12 +426,12 @@ def register():
|
|||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(reproject_context_menu)
|
||||
bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu)
|
||||
|
||||
def unregister():
|
||||
bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(reproject_context_menu)
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
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)
|
||||
# register_keymaps()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
# unregister_keymaps()
|
||||
|
|
77
README.md
77
README.md
|
@ -2,11 +2,18 @@
|
|||
|
||||
Blender addon - Various tool to help with grease pencil in animation productions.
|
||||
|
||||
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
|
||||
### /!\ Main branch is currently broken, in migration to gpv3
|
||||
|
||||
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||
|
||||
**[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
|
||||
|
||||
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||
|
||||
**[Readme Doc in French (Documentation en Français et plus détaillée)](README_FR.md)**
|
||||
**[Readme Doc in French](README_FR.md)**
|
||||
|
||||
It is recommended to enable _Grease Pencil Tools_ native Blender addon. Brings essentials features:
|
||||
Canvas rotation, Box deform, Timeline scrubbing in viewport, Quick layer navigator.
|
||||
|
||||
---
|
||||
|
||||
|
@ -29,9 +36,38 @@ Set path to the palette folder (there is a json palette IO but you an also put a
|
|||
Note about palette : For now the importer is not working with linked palette as it's not easy for animator (there are properties of the material you cannot access and the link grey-out fade the real color in UIlist preview)
|
||||
|
||||
|
||||
### Environnement Variables
|
||||
|
||||
> Mainly for devellopers to set project environnement
|
||||
|
||||
Since 1.5.2, following _environnement variable_ can set the project properties in toolbox preferences at register launch:
|
||||
|
||||
`RENDER_WIDTH` : resolution x
|
||||
`RENDER_HEIGHT` : resolution y
|
||||
`FPS` : project frame rate
|
||||
`PALETTES` : path to the blends (or json) containing materials palettes
|
||||
`BRUSHES` : path to the blend containing brushes to load
|
||||
`PREFIXES` : list of prefix (comma separated uppercase letters (2), an optional tooltip can be set after `:`, ex: 'LN:Line, CO:color, SH:Shadow') <!-- between 1 and 6 character -->
|
||||
`SUFFIXES` : list of suffixes (comma separated uppercase letters of 2 character, ex: 'OL,UL')
|
||||
`SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
|
||||
|
||||
### Expose native functionnality
|
||||
|
||||
The panel expose some attributes that are too "far" in the UI:
|
||||
|
||||
- Zoom 1:1 - Camera view take 100% zoom according to current scene resolution (ops `view3d.zoom_camera_1_to_1`)
|
||||
- Zoom fit - Adjust view so camera frame takes full viewport spac (ops `view3d.view_center_camera`)
|
||||
<!-- - autolock layer - tick layers'autolock -->
|
||||
- In Front - the `In Front` property of the object to get an X-ray view
|
||||
- passepartout camera - enable/disable + set opacity
|
||||
- button and sliders to enable / disable / set opacity of single background camera images
|
||||
|
||||
**Edit line opacity** - Animators usually like to hide completely edit lines to have a better view of the drawing in edit/sculpt mode, lowering opacity also allows a better reading on what's selected.
|
||||
This options is stored per layer per object but this apply on everything at once.
|
||||
|
||||
### Passive action
|
||||
|
||||
Add an "on save" Handler that trigger relative remap of all path (can be disabled in addon prefs).
|
||||
An "on save" Handler that trigger relative remap of all path can be enabled in addon prefs (disabled by default).
|
||||
|
||||
### function
|
||||
|
||||
|
@ -71,7 +107,7 @@ Store strokes in os'clipboard (easier cross blend copy)
|
|||
cutting is use a more user friendly (leave boundary points of left strokes untouched).
|
||||
Also Possible to copy whole selected layers.
|
||||
|
||||
- Auto update : you have an updater in the addon preference tabs (use the [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater))
|
||||
<!-- - Auto update : you have an updater in the addon preference tabs (use the [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater)) -->
|
||||
|
||||
|
||||
**Palette management**
|
||||
|
@ -84,6 +120,39 @@ In material submenu you have mutliple new entry:
|
|||
|
||||
- Load Color palette : same as the load above exept it loads directly from a blend file (all the material that the blend contains)
|
||||
|
||||
- Clean materials
|
||||
|
||||
**Shortcuts**
|
||||
|
||||
Viewport:
|
||||
|
||||
- Layer Picker from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
||||
|
||||
- Material Picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
|
||||
|
||||
- `F2` in Paint and Edit to rename active layer
|
||||
|
||||
- `Insert` add a new layer (same as Krita)
|
||||
|
||||
- `Shift + Insert` add a new layer and immediately pop-up a rename box
|
||||
|
||||
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
|
||||
|
||||
- `Shift + E` breakdown animation in object Mode
|
||||
|
||||
- `Ctrl + Shift + X/C/V` - Worldspace cut/copy/paste selected strokes/points:
|
||||
|
||||
Dopesheet:
|
||||
|
||||
- `Ctrl + Shift + X` Cut and send to layer
|
||||
|
||||
- `Ctrl + Shift + D` Duplicate and send to layer
|
||||
|
||||
Sculpt:
|
||||
|
||||
- point/stroke filter shortcut on `1`, `2`, `3` as toggles (similar to edit mode native shortcuts)
|
||||
|
||||
|
||||
### Where ?
|
||||
|
||||
Panel in sidebar : 3D view > sidebar 'N' > Gpencil
|
||||
|
|
98
README_FR.md
98
README_FR.md
|
@ -2,16 +2,33 @@
|
|||
|
||||
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
|
||||
|
||||
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
|
||||
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||
|
||||
**[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
|
||||
|
||||
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||
|
||||
> Une fois en place un système de mise a jour facilité est accessible dans les préférence (La vérification automatique de nouvelle mise a jour peut y être activé)
|
||||
**[English Readme Doc](README.md)**
|
||||
|
||||
Il est recommandé de désactiver l'addon natif "Grease pencil tools" car ces outils sont déjà intégré dans la toolbox et risque de créer des conflit.
|
||||
Il est recommandé d'activer l'addon natif _Grease pencil tools_ en parallèle qui ajoute des outils essentiels:
|
||||
Rotation du Canvas, Boîte de déformation, Scrub dans la timeline, Navigation de calque facilité...
|
||||
|
||||
## Fonctionnalités et détails
|
||||
|
||||
### Variables d'environnement
|
||||
|
||||
> Cette partie s'adresse surtout aux dévellopeurs qui préparent les environement de projets.
|
||||
|
||||
Depuis la version 1.5.2, les variables d'environnement suivantes peuvent surcharger les préférences projet dans blender (au moment du register)
|
||||
|
||||
`RENDER_WIDTH` : résolution x
|
||||
`RENDER_HEIGHT` : résolution y
|
||||
`FPS` : Vitesse du projet
|
||||
`PALETTES` : Chemin vers le dossier des blends (ou json) contenant des palettes de matériaux
|
||||
`BRUSHES` : Chemin vers le dossier des blends contenant les brushes a charger
|
||||
`PREFIXES` : Liste de prefixes du projet pour les noms de calques
|
||||
`SUFFIXES` : Liste de suffixes (2 caractères majuscule, séparé par des virgule. ex: 'OL,UL')
|
||||
`SEPARATOR` : Caractère de séparation pour determiner les prefixes, par défaut '_' (ne doit pas être un caractère spécial de regex !)
|
||||
|
||||
### Exposition dans l'UI de fonction native
|
||||
|
||||
|
@ -23,7 +40,7 @@ Expose les options suivantes:
|
|||
- Zoom fit - Ajuste la vue pour que la cam soit vue en totalité dans l'écran (ops `view3d.view_center_camera`)
|
||||
- Onion skin - coche des overlays
|
||||
- autolock layer - coche du sous-menu de l'UI list des layers
|
||||
- X-ray - Option `In Front` dans les propriété de l'objet
|
||||
- In Front - met en avant l'option `In Front` des propriété de l'objet
|
||||
- passepartout de caméra - active-désactive + opactité
|
||||
- liste de boutons pour activer/désactiver les background de caméra (tous ou individuellement) avec icone par type.
|
||||
|
||||
|
@ -55,6 +72,9 @@ Le "couper" est également plus naturel (conserve les points d'extrémité sur l
|
|||
Permet également de copier l'intégralité des layers selectionnés avec le bouton dédié (pas de raccourcis).
|
||||
|
||||
**check files** - série de check écris en dur dans le code. Pratique pour "fixer" rapidement sa scène:
|
||||
la liste est visible et modifiable dans l'onglet "Check list" des preférences d'addons.
|
||||
`Ctrl + Clic` sur le bonton permet de lister les changement sans les appliquer
|
||||
Voilà quelques exemples:
|
||||
- Lock main cam
|
||||
- set scene res to def project res (specified in addon prefs)
|
||||
- set scene percentage at 100:
|
||||
|
@ -64,7 +84,7 @@ Permet également de copier l'intégralité des layers selectionnés avec le bou
|
|||
- GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
||||
- GP stroke placement/projection check (just warn if your not in 'Front')
|
||||
- Warn if there are some disabled animation (and list datapath)
|
||||
- Set onion skin filter to 'All type' (this became default in blender 2.91, guess who asked ;) )
|
||||
- Set onion skin filter to 'All type'
|
||||
|
||||
EDIT: _rotate canvas_ et _box deform_ ont été retiré dans la version 1.0 car déja intégré à l'addon natif **grease pencil tools** depuis la 2.91 (activez simplement cet addon)
|
||||
> **Rotate canvas** (`ctrl + alt + clic-droit`) - Différence avec celui intégré a grease pencil tools : la rotation en vue cam n'est possible que si on est dans une caméra de manipulation (`manip_cam`) pour éviter de casser l'anim le roll de la cam principale.
|
||||
|
@ -88,67 +108,71 @@ Souci connu: Il y a un décalage d'une frame une fois activé sur un nouvel obje
|
|||
|
||||
### À potentiellement mettre de côté (peu utilisé)
|
||||
|
||||
**Tint layer** - Permet de mettre une teinte aléatoire par calque afin de les différencier rapidement. Souci de cet opérateurm, il ne faut pas l'utiliser si on utilise les tints de layer car il en change la couleur.
|
||||
**Tint layer** - Permet de mettre une teinte aléatoire par calque afin de les différencier rapidement. Souci de cet opérateurm, il ne faut pas l'utiliser si la paropriétée `tints` des layers est utile aux projet car il modifie cette couleur.
|
||||
|
||||
<!-- **Colorize** (gros WIP) - un sous ensemble d'outils qui était censé permettre de faire du remplissage bitmap via des color spots en envoyant deux set d'images rendu automatiquement à GMIC en ligne de commande et recalé la séquence de résultat en BG de Cam. Finalement abandonné, pas eu le temps de finir la mise au point (malgré des résultats préliminaires intéressant).
|
||||
Mais trop long a mettre en place, trop hackeu, et surtout c'est dommage de basculer sur du bitmap, la source de couleur doit rester au maximum GP/vecto. -->
|
||||
|
||||
## colorisation
|
||||
|
||||
**Line stopper** - Extension des lignes pour améliorer la fermeture des formes (génère un matériaux à part pour les masquer/supprimer facilement plus tard).
|
||||
Le hack est très simple mais aide beaucoup à fermer les contour pour éviter le leak de l'outils pot de peinture.
|
||||
Permet de diminuer le `leak_size` de ce dernier ce qui corrige certains pins de colo dans les angles obtus.
|
||||
|
||||
**Create empty frame** - Permet de créer des frames vides sur un calques partout où il y a des frames sur les calques supérieur (permet de faire un key to key sur le calque actif ensuite sur les key pré-créée pour faire sa colo).
|
||||
En réalité pour quelque chose de plus pratique pour la colo, il suffit d'ajouter un autre `screen.gp_keyframe_jump` operator en activant le filtre (all layers)
|
||||
TODO: faire un "all layer _above_" ou se baser sur le nouveau filtre natif existant pour cet usage depuis blender 2.91.
|
||||
|
||||
**Render** - chemin de sorties + 2 boutons:
|
||||
- layers individually (popup pour selectionner des calques a rendre individuellement)
|
||||
- layers grouped (popup pour selectionner des layers a rendre ensemble)
|
||||
- utilise le chemin relatif spécifié dans les preférences d'addon pour créer un chemin par groupe comme suit : `pref.location/name/name_####`
|
||||
TODO: profiter du système de render layers (per layer) pour faire un meilleur batch renderer.
|
||||
**Create empty frame** - Permet de créer des frames vides sur un calques en se basant sur les frames des calques choisis (permet de faire un key to key sur le calque actif ensuite sur les key pré-créée pour faire sa colo).
|
||||
Sinon pratique pour la colo, on peut aussi ajouter un autre `screen.gp_keyframe_jump` operator en activant le filtre (all layers) dans les options
|
||||
|
||||
|
||||
## tools supplémentaires
|
||||
|
||||
**Check links** (pop une fenêtre) - Permet de lister les liens de la scène, voir si il y en a des cassés et ouvrir le dossier d'un lien existant.
|
||||
**Check links** (pop une fenêtre) - Permet de lister les liens de la scène, voir si il y en a des cassés et ouvrir le dossier d'un lien existant et copier un lien.
|
||||
|
||||
**Auto update** - Un système de mise à jour est diponible dans les addon prefs, autocheck possible (désactivé par défaut). Utilise [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater))
|
||||
<!-- **Auto update** - Un système de mise à jour est diponible dans les addon prefs, autocheck possible (désactivé par défaut). Utilise [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater)) -->
|
||||
|
||||
## raccourci supplémentaires
|
||||
## raccourcis supplémentaires
|
||||
|
||||
Viewport:
|
||||
|
||||
- `W` (stroke) et `Alt + W` (fill) Sélectionne un calque d'après le trait le plus proche du curseur (en paint mode)
|
||||
|
||||
- `S` (fill) et `Alt + S` (stroke) selectionne un matériaux d'après le trait le plus proche du curseur. (en paint fill mode) (la touche doit pressé/relaché en moins de 200ms)
|
||||
|
||||
- `F2` Pop-up pour renommer le calque actif (en paint et en edit mode)
|
||||
|
||||
- `Insert` Ajoute un nouveau layer (comme dans Krita)
|
||||
|
||||
- `Shift + Insert` Ajoute un nouveau layer et immédiatement apelle le pop-up pour renommer le calque
|
||||
|
||||
- `page up / page down` change le calque actif en estompant temporairement les autres calques (la force d'estompe est personalisable dans les prefs de l'addon)
|
||||
|
||||
- `Shift + E` breakdown sur l'animation d'objet en mode Objet (Fonctionne comme celui disponible sur des bones en pose mode).
|
||||
|
||||
- `Ctrl + Shift + X/C/V` - Couper/Copier/Coller en World Space (indépendamment de la position de l'objet)
|
||||
|
||||
Dopesheet:
|
||||
|
||||
- `Ctrl + Shift + X` Couper une clé et l'envoyer sur un autre calque
|
||||
|
||||
- `Ctrl + Shift + D` Dupliquer une clé et l'envoyer sur un autre calque
|
||||
|
||||
Sculpt mode:
|
||||
|
||||
- point/stroke filter shortcut sur `1`, `2`, `3` en toggle (similaire a l'edit mode)
|
||||
- `1`, `2`, `3` (rangée au-dessus des lettres) Bascule les filtres de selection par Points/Strokes, comme en edit mode.
|
||||
|
||||
Grease pencil 3D cursor: Surcharge du raccourci curseur 3D pour le snapper à la surface du grease pencil. (Raccourci à remplacer manuellement dans la keymap, idname:`view3d.cusor_snap`, idname de l'opérateur de curseur natif `view3d.cursor3d`. Pas forcément utile si il n'y a pas de mix de 2D/3D.)
|
||||
Le mieux reste d'avoir un raccourci dédié, séparé de celui d'origine...
|
||||
<!-- Grease pencil 3D cursor: Surcharge du raccourci curseur 3D pour le snapper à la surface du grease pencil. (Raccourci à remplacer manuellement dans la keymap, idname:`view3d.cusor_snap`, idname de l'opérateur de curseur natif `view3d.cursor3d`. Pas forcément utile si il n'y a pas de mix de 2D/3D.)
|
||||
Le mieux reste d'avoir un raccourci dédié, séparé de celui d'origine... -->
|
||||
|
||||
---
|
||||
|
||||
### TODO:
|
||||
### Idées:
|
||||
|
||||
- Permettre de rendre avec la résolution spécifié dans le nom de la caméra active
|
||||
(utile dans les projet rendu a la résolution du BG mais ou la résolution finale peut être utilisé pour un bout-a-bout)
|
||||
|
||||
- Update du système de "passes" de rendu:
|
||||
- utiliser des render layers + file outputs au lieux de faire des batchs par opacité
|
||||
|
||||
- BG Playblast enhancement:
|
||||
- Tester davantage le playblast BG
|
||||
- Éventuellement mettre une coche de fallback vers le playblast classique (utile en cas de pépin.
|
||||
|
||||
- Faire un import-export des réglage généraux en json (Déjà une bonne partie du code dans Pipe sync)
|
||||
pour set : Résolution du film, dossier palette, render settings
|
||||
pour set : Résolution du film, dossier palette, render settings...
|
||||
|
||||
- opt: exposer les "tool setting" de placement de canvas en permanence dans la sidebar (visible seulement en draw)
|
||||
|
||||
- Déplacer automatiquement la vue "Face" au GP (en fonction des Gpencil view settings)
|
||||
|
||||
- Déplacer les clés de dopesheet en même temps que les clés de GP (Déjà Créer par Tom Viguier sur [Andarta](https://gitlab.com/andarta-pictures)
|
||||
|
||||
- Meilleure table lumineuse (grosse réflexion et travail en perspective)
|
||||
- Déplacer les clés de dopesheet en même temps que les clés de GP (Déjà Créer par Tom Viguier sur les repos d'[Andarta](https://gitlab.com/andarta-pictures)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from .utils import get_addon_prefs
|
|||
|
||||
class GPTB_WT_eraser(WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'PAINT_GPENCIL'
|
||||
bl_context_mode = 'PAINT_GREASE_PENCIL'
|
||||
|
||||
# The prefix of the idname should be your add-on name.
|
||||
bl_idname = "gp.eraser_tool"
|
||||
|
|
662
UI_tools.py
662
UI_tools.py
|
@ -1,14 +1,18 @@
|
|||
from . import addon_updater_ops
|
||||
from .utils import get_addon_prefs
|
||||
# from . import addon_updater_ops
|
||||
from .utils import (get_addon_prefs,
|
||||
anim_status,
|
||||
gp_modifier_status,
|
||||
)
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from bpy.types import Panel
|
||||
|
||||
|
||||
## UI in properties
|
||||
|
||||
### dataprop_panel not used --> transferred to sidebar
|
||||
"""
|
||||
class GPTB_PT_dataprop_panel(bpy.types.Panel):
|
||||
class GPTB_PT_dataprop_panel(Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
# bl_space_type = 'VIEW_3D'
|
||||
|
@ -16,7 +20,7 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
|
|||
# bl_category = "Tool"
|
||||
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
|
||||
bl_label = "Pseudo color"# title
|
||||
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
|
||||
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
|
@ -35,8 +39,8 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
|
|||
|
||||
## UI in Gpencil sidebar menu
|
||||
|
||||
class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
||||
bl_label = "Toolbox"
|
||||
class GPTB_PT_sidebar_panel(Panel):
|
||||
bl_label = "GP Toolbox"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
|
@ -44,23 +48,28 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
|||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# layout.use_property_split = True
|
||||
prefs = get_addon_prefs()
|
||||
rd = context.scene.render
|
||||
# check for update
|
||||
addon_updater_ops.check_for_update_background()
|
||||
# addon_updater_ops.check_for_update_background()
|
||||
|
||||
# layout.label(text='View options:')
|
||||
## flip X cam
|
||||
if context.scene.camera and context.scene.camera.scale.x < 0:
|
||||
# layout.label(text='! Flipped !')
|
||||
row = layout.row(align=True)
|
||||
col = layout.column()
|
||||
|
||||
row.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
|
||||
## flip X cam
|
||||
# layout.label(text='! Flipped !')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
|
||||
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
|
||||
|
||||
row = col.row(align=True)
|
||||
|
||||
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
|
||||
if context.scene.camera and context.scene.camera.scale.x < 0:
|
||||
row.label(text='',icon='LOOP_BACK')
|
||||
else:
|
||||
layout.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
|
||||
|
||||
## draw/manipulation camera
|
||||
col = layout.column()
|
||||
if context.scene.camera and context.scene.camera.name.startswith(('draw', 'obj')):
|
||||
row = col.row(align=True)
|
||||
row.operator('gp.draw_cam_switch', text = 'Main cam', icon = 'OUTLINER_OB_CAMERA')
|
||||
|
@ -86,6 +95,9 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
|||
else:
|
||||
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
|
||||
row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
|
||||
# row = layout.row(align=True)
|
||||
# row.operator('view3d.view_camera_frame_fit', text = 'Custom fit', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT
|
||||
row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER')
|
||||
|
@ -122,21 +134,19 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
|||
## Straight line ops from official greasepencil_tools addon if enabled.
|
||||
# if any(x in context.preferences.addons.keys() for x in ('greasepencil_tools', 'greasepencil-addon')): # check enabled addons
|
||||
if hasattr(bpy.types, 'GP_OT_straight_stroke'): # check if operator exists : bpy.ops.gp.straight_stroke.idname()
|
||||
layout.operator('gp.straight_stroke', icon ="CURVE_PATH")
|
||||
layout.operator(bpy.types.GP_OT_straight_stroke.bl_idname, icon ="CURVE_PATH")
|
||||
|
||||
## Options
|
||||
layout.separator()
|
||||
layout.label(text = 'Options:')
|
||||
col = layout.column()
|
||||
col.label(text = 'Options:')
|
||||
|
||||
## Kf Jump filter
|
||||
layout.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
|
||||
|
||||
col = layout.column()
|
||||
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
|
||||
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
|
||||
|
||||
if context.object and context.object.type == 'GPENCIL':
|
||||
if context.object and context.object.type == 'GREASEPENCIL':
|
||||
# col.prop(context.object.data, 'use_autolock_layers') # not often used
|
||||
col.prop(context.object, 'show_in_front', text='X-ray') # default text "In Front"
|
||||
col.prop(context.object, 'show_in_front') # text='In Front'
|
||||
|
||||
## rename datablock temporary layout
|
||||
if context.object.name != context.object.data.name:
|
||||
|
@ -147,6 +157,7 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
|||
row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True
|
||||
|
||||
## Check base palette
|
||||
if prefs.warn_base_palette and prefs.palette_path:
|
||||
if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
|
||||
box = col.box()
|
||||
box.label(text='Missing base material setup', icon='INFO')
|
||||
|
@ -156,37 +167,19 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
|||
col.label(text='No GP object selected')
|
||||
|
||||
|
||||
col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
||||
|
||||
row = col.row(align=True)
|
||||
## realign / reproject
|
||||
row.operator('gp.realign', icon='AXIS_FRONT')
|
||||
## move in depth
|
||||
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
|
||||
|
||||
## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
|
||||
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
|
||||
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
|
||||
|
||||
## File checker
|
||||
row = col.row(align=True)
|
||||
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
|
||||
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
|
||||
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
## Gpv3: not more edit line (use Curve lines)
|
||||
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
||||
|
||||
# Mention update as notice
|
||||
addon_updater_ops.update_notice_box_ui(self, context)
|
||||
|
||||
# addon_updater_ops.update_notice_box_ui(self, context)
|
||||
|
||||
# row = layout.row(align=False)
|
||||
# row.label(text='arrow choice')
|
||||
# row.operator("my_operator.multi_op", text='', icon='TRIA_LEFT').left = 1
|
||||
# row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0
|
||||
|
||||
class GPTB_PT_anim_manager(bpy.types.Panel):
|
||||
bl_label = "Animation manager"
|
||||
class GPTB_PT_anim_manager(Panel):
|
||||
bl_label = "Animation Manager"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
|
@ -196,38 +189,103 @@ class GPTB_PT_anim_manager(bpy.types.Panel):
|
|||
# def draw_header(self,context):
|
||||
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
|
||||
|
||||
def get_object_by_types(self, context) -> dict:
|
||||
# import time
|
||||
# t0 = time.perf_counter()
|
||||
|
||||
# objs = [o for o in context.scene.objects if o.type not in ('GREASEPENCIL', 'CAMERA')]
|
||||
# gps = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
|
||||
# cams = [o for o in context.scene.objects if o.type == 'CAMERA']
|
||||
objs = []
|
||||
gps = []
|
||||
cams = []
|
||||
for o in context.scene.objects:
|
||||
if o.type not in ('GREASEPENCIL', 'CAMERA'):
|
||||
objs.append(o)
|
||||
elif o.type == 'GREASEPENCIL':
|
||||
gps.append(o)
|
||||
elif o.type == 'CAMERA':
|
||||
cams.append(o)
|
||||
|
||||
# print(f'{time.perf_counter() - t0:.8f}s')
|
||||
|
||||
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams}
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
col = layout.column()
|
||||
## Animation enable disable anim (shift click to select) OP_helpers.GPTB_OT_toggle_mute_animation
|
||||
|
||||
layout.operator('gp.list_disabled_anims')
|
||||
## Objs ()
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Obj anims:')
|
||||
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
|
||||
ops.skip_gp = True
|
||||
ops.skip_obj = False
|
||||
obj_types = self.get_object_by_types(context)
|
||||
|
||||
col.operator('gp.list_disabled_anims')
|
||||
|
||||
## Show Enable / Disable anims
|
||||
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]:
|
||||
on_icon, off_icon = anim_status(obj_types[cat_type])
|
||||
|
||||
subcol = col.column()
|
||||
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||
row = subcol.row(align=True)
|
||||
row.label(text=cat)
|
||||
|
||||
ops = row.operator('gp.toggle_mute_animation', text='ON', icon=on_icon)
|
||||
ops.mode = cat_type
|
||||
ops.mute = False
|
||||
|
||||
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
|
||||
ops.skip_gp = True
|
||||
ops.skip_obj = False
|
||||
ops.mute = True
|
||||
## Gps
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Gp anims:')
|
||||
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
|
||||
ops.skip_gp = False
|
||||
ops.skip_obj = True
|
||||
ops.mute = False
|
||||
|
||||
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
|
||||
ops.skip_gp = False
|
||||
ops.skip_obj = True
|
||||
ops = row.operator('gp.toggle_mute_animation', text='OFF', icon=off_icon)
|
||||
ops.mode = cat_type
|
||||
ops.mute = True
|
||||
|
||||
class GPTB_PT_toolbox_playblast(bpy.types.Panel):
|
||||
## GP modifiers
|
||||
subcol = col.column()
|
||||
|
||||
row = subcol.row(align=True)
|
||||
row.label(text='Gp modifiers:')
|
||||
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
|
||||
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
|
||||
|
||||
## Step Select Frames
|
||||
col.operator('gptb.step_select_frames')
|
||||
|
||||
## Follow curve path
|
||||
col = col.column()
|
||||
row = col.row(align=True)
|
||||
|
||||
if context.object:
|
||||
if context.object.type == 'CURVE' and context.mode in ('OBJECT', 'EDIT_CURVE'):
|
||||
row.operator('object.object_from_curve', text='Back To Object', icon='LOOP_BACK')
|
||||
|
||||
elif (follow_const := context.object.constraints.get('Follow Path')) and follow_const.target:
|
||||
row.operator('object.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE')
|
||||
row.operator('object.remove_follow_path', text='', icon='X')
|
||||
col.label(text=f'{context.object.name} -> {follow_const.target.name}', icon='CON_FOLLOWPATH')
|
||||
if follow_const.use_fixed_location:
|
||||
col.prop(follow_const, 'offset_factor')
|
||||
else:
|
||||
col.prop(follow_const, 'offset')
|
||||
if context.object.location.length != 0: # context.object.location[:] != (0,0,0):
|
||||
col.operator('object.location_clear', text='Offseted Location! Reset', icon='ERROR')
|
||||
# ? Check if object location is animated ? (can be intentional...)
|
||||
|
||||
else:
|
||||
col.operator('object.create_follow_path_curve', text='Create Follow Curve', icon='CURVE_BEZCURVE')
|
||||
|
||||
|
||||
## This can go in an extra category...
|
||||
col = layout.column()
|
||||
col.use_property_split = False
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
if context.scene.gptoolprops.cursor_follow:
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
|
||||
|
||||
|
||||
class GPTB_PT_toolbox_playblast(Panel):
|
||||
bl_label = "Playblast"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
|
@ -249,18 +307,14 @@ class GPTB_PT_toolbox_playblast(bpy.types.Panel):
|
|||
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
|
||||
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
|
||||
|
||||
class GPTB_PT_tint_layers(bpy.types.Panel):
|
||||
bl_label = "Tint layers"
|
||||
class GPTB_PT_tint_layers(Panel):
|
||||
bl_label = "Tint Layers"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.camera
|
||||
|
||||
# def draw_header(self,context):
|
||||
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
|
||||
|
||||
|
@ -270,51 +324,82 @@ class GPTB_PT_tint_layers(bpy.types.Panel):
|
|||
## pseudo color layers
|
||||
# layout.separator()
|
||||
col = layout.column(align = True)
|
||||
row = col.split(align=False, factor=0.63)
|
||||
row.prop(context.scene.gptoolprops, 'autotint_offset')
|
||||
row.prop(context.scene.gptoolprops, 'autotint_namespace')
|
||||
# row = col.split(align=False, factor=0.63)
|
||||
# row = col.row()
|
||||
col.prop(context.scene.gptoolprops, 'autotint_offset', text='Hue Offset')
|
||||
col.prop(context.scene.gptoolprops, 'autotint_namespace')
|
||||
|
||||
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
|
||||
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
|
||||
|
||||
class GPTB_PT_render(bpy.types.Panel):
|
||||
bl_label = "Render"
|
||||
|
||||
class GPTB_PT_checker(Panel):
|
||||
bl_label = "Checker"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.camera
|
||||
|
||||
# def draw_header(self,context):
|
||||
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator('render.render_anim', text = 'Render invividually', icon = 'RENDERLAYERS').mode = 'INDIVIDUAL'#RENDER_STILL #RESTRICT_RENDER_OFF
|
||||
layout.operator('render.render_anim', text = 'Render grouped', icon = 'IMAGE_RGB').mode = 'GROUP'
|
||||
col = layout.column()
|
||||
row = col.row(align=True)
|
||||
## realign / reproject
|
||||
row.operator('gp.realign', icon='AXIS_FRONT')
|
||||
## move in depth
|
||||
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
|
||||
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
row.prop(context.scene.gptoolprops, 'name_for_current_render', text = 'Output name')#icon = 'OUTPUT'
|
||||
row.operator('render.use_active_object_name', text = '', icon='OUTLINER_DATA_GP_LAYER')#icon = 'OUTPUT'
|
||||
## col.operator('gp.batch_reproject_all_frames') # text=Batch Reproject # added to context menu
|
||||
## check drawing alignement
|
||||
col.operator('gp.check_canvas_alignement', icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
||||
|
||||
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS
|
||||
|
||||
blend = bpy.data.filepath
|
||||
if blend:
|
||||
blend = Path(blend)
|
||||
out = blend.parents[1] / "compo" / "base"
|
||||
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
|
||||
## File checker
|
||||
row = col.row(align=True)
|
||||
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
|
||||
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
|
||||
|
||||
|
||||
class GPTB_PT_color(Panel):
|
||||
bl_label = "Color"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
## Create empty frame on layer
|
||||
|
||||
# Material panel as a pop-up (would work if palette dir is separated)
|
||||
# col.operator("wm.call_panel", text="Link Materials Palette", icon='COLOR').name = "GPTB_PT_palettes_linker_ui"
|
||||
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
|
||||
# col.operator("wm.call_panel", text="Link Material Palette", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
|
||||
|
||||
""" # unused : added in Animation Manager
|
||||
class GPTB_PT_extra(Panel):
|
||||
bl_label = "Extra"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
"""
|
||||
|
||||
"""
|
||||
## unused -- (integrated in sidebar_panel)
|
||||
class GPTB_PT_cam_ref_panel(bpy.types.Panel):
|
||||
class GPTB_PT_cam_ref_panel(Panel):
|
||||
bl_label = "Background imgs"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
|
@ -340,38 +425,375 @@ class GPTB_PT_cam_ref_panel(bpy.types.Panel):
|
|||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
||||
"""
|
||||
|
||||
|
||||
def palette_manager_menu(self, context):
|
||||
"""Palette menu to append in existing menu"""
|
||||
# GPENCIL_MT_material_context_menu
|
||||
layout = self.layout
|
||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
||||
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||
layout.separator()
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
layout.operator("gp.copy_active_to_selected_palette", text='Append Materials To Selected', icon='MATERIAL')
|
||||
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
|
||||
layout.separator()
|
||||
layout.operator("wm.call_panel", text="Pop Palette Linker", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
|
||||
layout.operator("gp.load_blend_palette", text='Load Mats From Single Blend', icon='RESTRICT_COLOR_ON').filepath = prefs.palette_path
|
||||
layout.separator()
|
||||
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
|
||||
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
|
||||
layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
|
||||
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
|
||||
layout.separator()
|
||||
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
|
||||
|
||||
|
||||
def expose_use_channel_color_pref(self, context):
|
||||
# add in GreasePencilLayerDisplayPanel (gp dopesheet View > Display)
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.label(text='Use Channel Colors (User preferences):')
|
||||
layout.prop(context.preferences.edit, 'use_anim_channel_group_colors')
|
||||
|
||||
|
||||
#--- Palette Linker Panels
|
||||
|
||||
def palettes_path_ui(self, context):
|
||||
layout = self.layout
|
||||
scn = bpy.context.scene
|
||||
pl_prop = scn.bl_palettes_props
|
||||
col= layout.column()
|
||||
prefs = get_addon_prefs()
|
||||
## Here put the path thing (only to use a non-library)
|
||||
|
||||
# maybe in submenu...
|
||||
row = col.row()
|
||||
# expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT'
|
||||
# row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False)
|
||||
row.prop(pl_prop, 'use_project_path', text='Use Project Palettes')
|
||||
# row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||
|
||||
if pl_prop.use_project_path:
|
||||
## gp toolbox addon prefs path
|
||||
if not prefs.palette_path:
|
||||
col.label(text='GP toolbox Palette Directory Needed', icon='INFO')
|
||||
col.operator('gptb.open_addon_prefs', icon='PREFERENCES')
|
||||
|
||||
# if not prefs.palette_path: # or pl_prop.show_path
|
||||
# col.prop(prefs, 'palette_path', text='Project Dir')
|
||||
#col.label(text='(saved with preferences)')
|
||||
else:
|
||||
## local path
|
||||
if not pl_prop.custom_dir:
|
||||
col.label(text='Need to specify directory')
|
||||
col.prop(pl_prop, 'custom_dir', text='Custom Dir')
|
||||
# if not pl_prop.custom_dir or pl_prop.show_path:
|
||||
# col.prop(pl_prop, 'custom_dir', text='Custom Dir')
|
||||
|
||||
# col.operator('gptb.palette_version_update', text='Update Palette Version') # when update is ready
|
||||
|
||||
|
||||
def palettes_lists_ui(self, context, popup=False):
|
||||
layout = self.layout
|
||||
scn = bpy.context.scene
|
||||
pl_prop = scn.bl_palettes_props
|
||||
col= layout.column()
|
||||
row=col.row()
|
||||
# refresh button
|
||||
txt = 'Project Palettes' if pl_prop.use_project_path else 'Custom Palettes'
|
||||
row.label(text=txt)
|
||||
row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||
|
||||
col= layout.column()
|
||||
row = col.row()
|
||||
|
||||
if popup:
|
||||
blends_minimum_row = 5
|
||||
objects_minimum_row = 25
|
||||
else:
|
||||
blends_minimum_row = 2
|
||||
objects_minimum_row = 4
|
||||
row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx",
|
||||
rows=blends_minimum_row)
|
||||
# side panel
|
||||
# subcol = row.column(align=True)
|
||||
# subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
|
||||
|
||||
## Show object UI list only once blend Uilist is filled ?
|
||||
if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)):
|
||||
col.label(text='Select blend refresh available objects')
|
||||
|
||||
row = col.row()
|
||||
row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx",
|
||||
rows=objects_minimum_row)
|
||||
|
||||
## Show link button in the border of the UI list ?
|
||||
# col.prop(pl_prop, 'import_type')
|
||||
split = col.split(align=True, factor=0.4)
|
||||
split.prop(pl_prop, 'import_type', text='')
|
||||
|
||||
split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path)
|
||||
split.operator('gp.import_obj_palette', text='Palette')
|
||||
|
||||
# button to launch link with combined props (active only if the two items are valids)
|
||||
# str(Path(self.blends) / 'Object' / self.objects
|
||||
|
||||
|
||||
class GPTB_PT_palettes_linker_main_ui(Panel):
|
||||
bl_space_type = 'TOPBAR' # dummy
|
||||
bl_region_type = 'HEADER'
|
||||
# bl_space_type = "VIEW_3D"
|
||||
# bl_region_type = "UI"
|
||||
# bl_category = "Gpencil"
|
||||
bl_label = "Palettes Mat Linker"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
## link button for tests
|
||||
# layout.operator('gp.import_obj_palette', text='Palette')
|
||||
|
||||
# Subpanel are appended to this main UI
|
||||
|
||||
## Or just as One fat panel
|
||||
# palettes_path_ui(self, context)
|
||||
# palettes_lists_ui(self, context)
|
||||
|
||||
class GPTB_PT_palettes_path_ui(Panel):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_label = "Palettes Source" # Source Path
|
||||
# bl_parent_id = "GPTB_PT_palettes_linker_main_ui"
|
||||
bl_parent_id = "GPTB_PT_color"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
palettes_path_ui(self, context)
|
||||
# layout.label()
|
||||
|
||||
# pop-up version of object lists
|
||||
class GPTB_PT_palettes_list_popup(Panel):
|
||||
bl_space_type = 'TOPBAR' # dummy
|
||||
bl_region_type = 'HEADER'
|
||||
bl_category = "Gpencil"
|
||||
bl_label = "Palettes Lists"
|
||||
bl_ui_units_x = 18
|
||||
|
||||
def draw(self, context):
|
||||
palettes_lists_ui(self, context, popup=True)
|
||||
|
||||
class GPTB_PT_palettes_list_ui(Panel):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_label = "Palettes Lists"
|
||||
bl_parent_id = "GPTB_PT_color"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
# layout.label(text="My Select Panel")
|
||||
layout.operator("wm.call_panel", text="", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
|
||||
|
||||
def draw(self, context):
|
||||
palettes_lists_ui(self, context, popup=False)
|
||||
|
||||
|
||||
## bl 3+ UI
|
||||
def asset_browser_ui(self, context):
|
||||
'''Only shows in blender >= 3.0.0'''
|
||||
|
||||
layout = self.layout
|
||||
asset_file_handle = context.asset_file_handle
|
||||
if asset_file_handle is None:
|
||||
# layout.label(text="No asset selected", icon='INFO')
|
||||
layout.label(text='No object/material selected', icon='INFO')
|
||||
return
|
||||
if asset_file_handle.id_type not in ('OBJECT', 'MATERIAL'):
|
||||
layout.label(text='No object/material selected', icon='INFO')
|
||||
return
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
asset_library_ref = context.asset_library_ref
|
||||
## Path to blend
|
||||
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
|
||||
path_to_obj = Path(asset_lib_path) / 'Objects' / asset_file_handle.name
|
||||
|
||||
## respect header choice ?
|
||||
## import_type in (LINK, APPEND, APPEND_REUSE)
|
||||
imp_type = context.space_data.params.import_type
|
||||
if imp_type == 'APPEND':
|
||||
imp_txt = 'Append'
|
||||
elif imp_type == 'APPEND_REUSE':
|
||||
imp_txt = 'Append (Reuse)'
|
||||
else:
|
||||
imp_txt = 'Link'
|
||||
|
||||
if asset_file_handle.id_type == 'MATERIAL':
|
||||
layout.label(text=f'From Mat: {asset_file_handle.name}')
|
||||
if asset_file_handle.id_type == 'OBJECT':
|
||||
layout.label(text=f'From Obj: {asset_file_handle.name}')
|
||||
layout.label(text=f'{imp_txt} Materials To GP Object')
|
||||
layout.operator('gp.palette_linker', text=f'{imp_txt} Materials To GP Object') ## ops
|
||||
|
||||
# layout.label(text='Link Materials to GP Object')
|
||||
|
||||
|
||||
|
||||
# Put back pop-over UI for Grease Pencil stroke interpolation tools native pop hover panel from 2.92
|
||||
class GPTB_PT_tools_grease_pencil_interpolate(Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'HEADER'
|
||||
bl_label = "Interpolate"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if context.gpencil_data is None:
|
||||
return False
|
||||
|
||||
gpd = context.gpencil_data
|
||||
valid_mode = bool(gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode)
|
||||
return bool(context.editable_gpencil_strokes) and valid_mode
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
|
||||
## access active tool settings
|
||||
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
|
||||
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate')
|
||||
|
||||
## custom curve access (still in gp interpolate tools)
|
||||
interpolate_settings = context.tool_settings.gpencil_interpolate
|
||||
# ex : interpolate_settings.interpolation_curve.curves[0].points[1].location
|
||||
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Interpolate Strokes")
|
||||
col.operator("gpencil.interpolate", text="Interpolate")
|
||||
col.operator("gpencil.interpolate_sequence", text="Sequence")
|
||||
col.operator("gpencil.interpolate_reverse", text="Remove Breakdowns")
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Options:")
|
||||
# col.prop(settings, "interpolate_all_layers") # now the enum "layers"
|
||||
gpd = context.gpencil_data
|
||||
if gpd.use_stroke_edit_mode:
|
||||
col.prop(settings, "interpolate_selected_only")
|
||||
col.prop(settings, "layers")
|
||||
col.prop(settings, "flip")
|
||||
col.prop(settings, "smooth_factor")
|
||||
col.prop(settings, "smooth_steps")
|
||||
|
||||
|
||||
'''## Sequence Options
|
||||
seq_settings = context.window_manager.operators.get('GPENCIL_OT_interpolate_sequence')
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Sequence Options:")
|
||||
if not seq_settings:
|
||||
# col.label(text='Launch Interpolate Sequence Once')
|
||||
# col.operator('gpencil.interpolate_sequence',text='Interpolate Sequence Once')
|
||||
col.label(text='Interpolate sequence', icon='INFO')
|
||||
col.label(text='must be launched')
|
||||
col.label(text="once per session")
|
||||
col.label(text="to expose it's properties")
|
||||
return
|
||||
|
||||
col.prop(seq_settings, "step")
|
||||
col.prop(seq_settings, "layers")
|
||||
col.prop(seq_settings, "interpolate_selected_only")
|
||||
col.prop(seq_settings, "flip")
|
||||
col.prop(seq_settings, "smooth_factor")
|
||||
col.prop(seq_settings, "smooth_steps")
|
||||
col.prop(seq_settings, "type")
|
||||
if seq_settings.type == 'CUSTOM':
|
||||
# TODO: Options for loading/saving curve presets?
|
||||
col.template_curve_mapping(interpolate_settings, "interpolation_curve", brush=True,
|
||||
use_negative_slope=True)
|
||||
elif seq_settings.type != 'LINEAR':
|
||||
col.prop(seq_settings, "easing")
|
||||
|
||||
if seq_settings.type == 'BACK':
|
||||
layout.prop(seq_settings, "back")
|
||||
elif seq_settings.type == 'ELASTIC':
|
||||
sub = layout.column(align=True)
|
||||
sub.prop(seq_settings, "amplitude")
|
||||
sub.prop(seq_settings, "period")
|
||||
'''
|
||||
|
||||
|
||||
## recreate property group from operator options
|
||||
# inspect context.window_manager.operators['GPENCIL_OT_interpolate_sequence']
|
||||
# separate options from single interpolation and sequence interpolation
|
||||
|
||||
# class GPTB_PG_interpolate_sequence_prop(bpy.types.PropertyGroup):
|
||||
# interpolate_selected_only : BoolProperty(
|
||||
# name="Selected Only",
|
||||
# description="",
|
||||
# default=True,
|
||||
# options={'HIDDEN'})
|
||||
|
||||
def interpolate_header_ui(self, context):
|
||||
layout = self.layout
|
||||
obj = context.active_object
|
||||
|
||||
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data:
|
||||
gpd = context.gpencil_data
|
||||
else:
|
||||
return
|
||||
|
||||
if gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode:
|
||||
row = layout.row(align=True)
|
||||
row.popover(
|
||||
panel="GPTB_PT_tools_grease_pencil_interpolate",
|
||||
text="Interpolate",
|
||||
)
|
||||
|
||||
classes = (
|
||||
GPTB_PT_sidebar_panel,
|
||||
GPTB_PT_checker,
|
||||
GPTB_PT_anim_manager,
|
||||
GPTB_PT_toolbox_playblast,
|
||||
GPTB_PT_color,
|
||||
GPTB_PT_tint_layers,
|
||||
GPTB_PT_render,
|
||||
## GPTB_PT_cam_ref_panel,
|
||||
GPTB_PT_toolbox_playblast,
|
||||
# GPTB_PT_tools_grease_pencil_interpolate, # WIP
|
||||
|
||||
# palettes linker
|
||||
GPTB_PT_palettes_linker_main_ui, # main panel
|
||||
GPTB_PT_palettes_list_popup, # popup (dummy region)
|
||||
GPTB_PT_palettes_path_ui, # subpanels
|
||||
GPTB_PT_palettes_list_ui, # subpanels
|
||||
# GPTB_PT_extra,
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
|
||||
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
# bpy.types.ASSETBROWSER_PT_metadata.append(asset_browser_ui)
|
||||
|
||||
|
||||
def unregister():
|
||||
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
|
||||
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
@ -392,33 +814,3 @@ def GPdata_toolbox_panel(self, context):
|
|||
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
|
||||
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
|
||||
"""
|
||||
|
||||
|
||||
|
||||
### old
|
||||
|
||||
"""
|
||||
col = layout.column(align = True)
|
||||
col.operator("gpencil.stroke_change_color", text="Move to Color",icon = "COLOR")
|
||||
col.operator("transform.shear", text="Shear")
|
||||
col.operator("gpencil.stroke_cyclical_set", text="Toggle Cyclic").type = 'TOGGLE'
|
||||
col.operator("gpencil.stroke_subdivide", text="Subdivide",icon = "OUTLINER_DATA_MESH")
|
||||
|
||||
row = layout.row(align = True)
|
||||
row.operator("gpencil.stroke_join", text="Join").type = 'JOIN'
|
||||
row.operator("grease_pencil.stroke_separate", text = "Separate")
|
||||
col.operator("gpencil.stroke_flip", text="Flip Direction",icon = "ARROW_LEFTRIGHT")
|
||||
|
||||
col = layout.column(align = True)
|
||||
col.operator("gptools.randomise",icon = 'RNDCURVE')
|
||||
col.operator("gptools.thickness",icon = 'LINE_DATA')
|
||||
col.operator("gptools.angle_split",icon = 'MOD_BEVEL',text='Angle Splitting')
|
||||
col.operator("gptools.stroke_uniform_density",icon = 'MESH_DATA',text = 'Density')
|
||||
|
||||
row = layout.row(align = True)
|
||||
row.prop(settings,"extra_tools",text='',icon = "DOWNARROW_HLT" if settings.extra_tools else "RIGHTARROW",emboss = False)
|
||||
row.label("Extra tools")
|
||||
|
||||
if settings.extra_tools :
|
||||
layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction")
|
||||
"""
|
||||
|
|
709
__init__.py
709
__init__.py
|
@ -1,33 +1,22 @@
|
|||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
bl_info = {
|
||||
"name": "GP toolbox",
|
||||
"description": "Set of tools for Grease Pencil in animation production",
|
||||
"description": "Tool set for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (1, 5, 1),
|
||||
"blender": (2, 91, 0),
|
||||
"version": (4, 0, 4),
|
||||
"blender": (4, 3, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
|
||||
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
|
||||
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
|
||||
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
|
||||
from . import addon_updater_ops
|
||||
|
||||
from .utils import *
|
||||
from .functions import *
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from sys import modules
|
||||
from .utils import get_addon_prefs, draw_kmi
|
||||
|
||||
## GMIC
|
||||
from .GP_guided_colorize import GP_colorize
|
||||
|
@ -41,31 +30,47 @@ from . import OP_helpers
|
|||
from . import OP_keyframe_jump
|
||||
from . import OP_cursor_snap_canvas
|
||||
from . import OP_palettes
|
||||
from . import OP_palettes_linker
|
||||
from . import OP_brushes
|
||||
from . import OP_file_checker
|
||||
from . import OP_render
|
||||
from . import OP_copy_paste
|
||||
from . import OP_realign
|
||||
# from . import OP_flat_reproject # Disabled
|
||||
from . import OP_depth_move
|
||||
from . import OP_key_duplicate_send
|
||||
from . import OP_eraser_brush
|
||||
from . import TOOL_eraser_brush
|
||||
from . import OP_layer_manager
|
||||
from . import OP_layer_picker
|
||||
from . import OP_layer_nav
|
||||
from . import OP_material_picker
|
||||
from . import OP_git_update
|
||||
from . import OP_layer_namespace
|
||||
from . import OP_pseudo_tint
|
||||
from . import OP_follow_curve
|
||||
from . import OP_material_move_to_layer
|
||||
# from . import OP_eraser_brush
|
||||
# from . import TOOL_eraser_brush
|
||||
from . import handler_draw_cam
|
||||
from . import keymaps
|
||||
|
||||
from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers
|
||||
|
||||
from . import UI_tools
|
||||
|
||||
from .properties import GP_PG_ToolsSettings, GP_PG_FixSettings
|
||||
from .properties import (
|
||||
GP_PG_ToolsSettings,
|
||||
GP_PG_FixSettings,
|
||||
GP_PG_namespaces,
|
||||
)
|
||||
|
||||
from bpy.props import (FloatProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
IntProperty)
|
||||
IntProperty,
|
||||
PointerProperty
|
||||
)
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.app.handlers import persistent
|
||||
from pathlib import Path
|
||||
# from .eyedrop import EyeDropper
|
||||
|
@ -100,75 +105,42 @@ def remap_on_save_update(self, context):
|
|||
bpy.app.handlers.save_pre.remove(remap_relative)
|
||||
|
||||
|
||||
|
||||
def update_use_precise_eraser(self, context):
|
||||
|
||||
km, kmi = TOOL_eraser_brush.addon_keymaps[0]
|
||||
|
||||
kmi.active = self.use_precise_eraser
|
||||
|
||||
## precise eraser
|
||||
# def update_use_precise_eraser(self, context):
|
||||
# km, kmi = TOOL_eraser_brush.addon_keymaps[0]
|
||||
# kmi.active = self.use_precise_eraser
|
||||
|
||||
class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
bl_idname = __name__
|
||||
|
||||
use_precise_eraser : BoolProperty(
|
||||
name='Precise Eraser',
|
||||
default=False,
|
||||
update=update_use_precise_eraser
|
||||
)
|
||||
## precise eraser
|
||||
# use_precise_eraser : BoolProperty(
|
||||
# name='Precise Eraser',
|
||||
# default=False,
|
||||
# update=update_use_precise_eraser)
|
||||
|
||||
## tabs
|
||||
|
||||
pref_tabs : EnumProperty(
|
||||
items=(('PREF', "Preferences", "Change some preferences of the modal"),
|
||||
('MAN_OPS', "Operator", "Operator to add Manually"),
|
||||
# ('TUTO', "Tutorial", "How to use the tool"),
|
||||
('KEYS', "Shortcuts", "Customize addon shortcuts"),
|
||||
('MAN_OPS', "Operators", "Operator to add Manually"),
|
||||
('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"),
|
||||
('UPDATE', "Update", "Check and apply updates"),
|
||||
# ('UPDATE', "Update", "Check and apply updates"),
|
||||
# ('TUTO', "Tutorial", "How to use the tool"),
|
||||
# ('KEYMAP', "Keymap", "customise the default keymap"),
|
||||
),
|
||||
default='PREF')
|
||||
|
||||
## addon pref updater props
|
||||
|
||||
auto_check_update : BoolProperty(
|
||||
name="Auto-check for Update",
|
||||
description="If enabled, auto-check for updates using an interval",
|
||||
default=False,
|
||||
)
|
||||
|
||||
updater_intrval_months : IntProperty(
|
||||
name='Months',
|
||||
description="Number of months between checking for updates",
|
||||
default=0,
|
||||
min=0
|
||||
)
|
||||
updater_intrval_days : IntProperty(
|
||||
name='Days',
|
||||
description="Number of days between checking for updates",
|
||||
default=7,
|
||||
min=0,
|
||||
max=31
|
||||
)
|
||||
updater_intrval_hours : IntProperty(
|
||||
name='Hours',
|
||||
description="Number of hours between checking for updates",
|
||||
default=0,
|
||||
min=0,
|
||||
max=23
|
||||
)
|
||||
updater_intrval_minutes : IntProperty(
|
||||
name='Minutes',
|
||||
description="Number of minutes between checking for updates",
|
||||
default=0,
|
||||
min=0,
|
||||
max=59
|
||||
)
|
||||
|
||||
## addon prefs
|
||||
|
||||
## Project preferences
|
||||
#--# PROJECT PREFERENCES #--#
|
||||
# subtype (string) – Enumerator in ['FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'].
|
||||
|
||||
# update variables
|
||||
is_git_repo : BoolProperty(default=False)
|
||||
has_git : BoolProperty(default=False)
|
||||
|
||||
## fps
|
||||
|
||||
use_relative_remap_on_save : BoolProperty(
|
||||
|
@ -200,21 +172,77 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
description="Path relative to blend to place render",
|
||||
default="//render", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
separator : StringProperty(
|
||||
name="Namespace separator",
|
||||
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified",
|
||||
default="_", maxlen=0, subtype='NONE')
|
||||
playblast_path : StringProperty(
|
||||
name="Playblast Path",
|
||||
description="Path to folder for playblasts output",
|
||||
default="//playblast", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
use_env_palettes : BoolProperty(
|
||||
name="Use Project Palettes",
|
||||
description="Load the palette path in environnement at startup (key 'PALETTES')",
|
||||
default=True,
|
||||
)
|
||||
|
||||
palette_path : StringProperty(
|
||||
name="Palettes directory",
|
||||
description="Path to palette containing palette.json files to save and load",
|
||||
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
|
||||
default="", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
|
||||
|
||||
warn_base_palette : BoolProperty(
|
||||
name="Warn if base palette isn't loaded",
|
||||
description="Display a button to load palette base.json if current grease pencil has a no 'line' and 'invisible' materials",
|
||||
default=True,
|
||||
)
|
||||
|
||||
mat_link_exclude : StringProperty(
|
||||
name="Materials Link Exclude",
|
||||
description="List of material name to exclude when using palette linker (separate multiple value with comma, ex: line, rough)",
|
||||
default="line,", maxlen=0)
|
||||
|
||||
use_env_brushes : BoolProperty(
|
||||
name="Use Project Brushes",
|
||||
description="Load the brushes path in environnement at startup (key 'BRUSHES')",
|
||||
default=True,
|
||||
)
|
||||
|
||||
brush_path : StringProperty(
|
||||
name="Brushes directory",
|
||||
description="Path to brushes containing the blends holding the brushes",
|
||||
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
|
||||
|
||||
## namespace
|
||||
separator : StringProperty(
|
||||
name="Separator",
|
||||
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified",
|
||||
default="_", maxlen=0, subtype='NONE')
|
||||
|
||||
## Old one string comma separated prefix/suffix list
|
||||
# prefixes : StringProperty(
|
||||
# name="Layers Prefixes",
|
||||
# description="List of prefixes (two capital letters) available for layers(ex: AN,CO,CL)",
|
||||
# default="", maxlen=0)
|
||||
|
||||
# suffixes : StringProperty(
|
||||
# name="Layers Suffixes",
|
||||
# description="List of suffixes (two capital letters) available for layers(ex: OL,UL)",
|
||||
# default="", maxlen=0)
|
||||
|
||||
prefixes : PointerProperty(type=GP_PG_namespaces)
|
||||
suffixes : PointerProperty(type=GP_PG_namespaces)
|
||||
|
||||
|
||||
# use_env_namespace : BoolProperty(
|
||||
# name="Use Project namespace",
|
||||
# description="Ovewrite prefix/suffix with Project values defined in environnement at startup\n(key 'PREFIXES and SUFFIXES')",
|
||||
# default=True,
|
||||
# )
|
||||
|
||||
show_prefix_buttons : BoolProperty(
|
||||
name="Show Prefix Buttons",
|
||||
description="Show prefix and suffix buttons above layer stack",
|
||||
default=True,
|
||||
)
|
||||
|
||||
## Playblast prefs
|
||||
playblast_auto_play : BoolProperty(
|
||||
name="Playblast auto play",
|
||||
|
@ -228,19 +256,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
default=False,
|
||||
)
|
||||
|
||||
## default active tool to use
|
||||
select_active_tool : EnumProperty(
|
||||
name="Default selection tool", description="Active tool to set when launching check fix scene",
|
||||
default='builtin.select_lasso',
|
||||
items=(
|
||||
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
|
||||
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
|
||||
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
|
||||
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
|
||||
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
|
||||
))
|
||||
|
||||
|
||||
## render settings
|
||||
render_obj_exclusion : StringProperty(
|
||||
name="GP obj exclude filter",
|
||||
|
@ -311,12 +326,65 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
|
||||
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings)
|
||||
|
||||
## GP Layer navigator
|
||||
|
||||
nav_use_fade : BoolProperty(
|
||||
name='Fade Inactive Layers',
|
||||
description='Fade Inactive layers to determine active layer in a glimpse',
|
||||
default=True)
|
||||
|
||||
nav_fade_val : FloatProperty(
|
||||
name='Fade Value',
|
||||
description='Fade value for other layers when navigating (0=invisible)',
|
||||
default=0.1, min=0.0, max=0.95, step=1, precision=2)
|
||||
|
||||
nav_limit : FloatProperty(
|
||||
name='Fade Duration',
|
||||
description='Time of other layer faded when using layer navigation',
|
||||
default=1.4, min=0.1, max=5, step=2, precision=1, subtype='TIME', unit='TIME')
|
||||
|
||||
nav_use_fade_in : BoolProperty(
|
||||
name='Progressive Fade Back',
|
||||
description='Use a fade on other layer when navigating',
|
||||
default=True)
|
||||
|
||||
nav_fade_in_time : FloatProperty(
|
||||
name='Fade-In Time',
|
||||
description='Duration of the fade',
|
||||
default=0.5, min=0.1, max=5, step=2, precision=2, subtype='TIME', unit='TIME')
|
||||
|
||||
nav_interval : FloatProperty(
|
||||
name='Refresh Rate',
|
||||
description='Refresh rate for fade updating (upper value means stepped fade)',
|
||||
default=0.04, min=0.01, max=0.5, step=3, precision=2, subtype='TIME', unit='TIME')
|
||||
|
||||
## Temp cutter
|
||||
# temp_cutter_use_shortcut: BoolProperty(
|
||||
# name = "Use temp cutter Shortcut",
|
||||
# description = "Auto assign shortcut for temp_cutter",
|
||||
# default = True)
|
||||
|
||||
def draw_namespaces_list(self, layout, template_list, pg_name, rows=4):
|
||||
'''Get layout, property group to draw and default row number'''
|
||||
|
||||
pg = getattr(self, pg_name)
|
||||
row = layout.row(align=True)
|
||||
row.template_list(template_list, "", pg, "namespaces", pg, "idx", rows=rows)
|
||||
subcol = row.column(align=True) # Lateral right
|
||||
subcol.operator("gptb.add_namespace_entry", icon="ADD", text="").propname=pg_name
|
||||
subcol.operator("gptb.remove_namespace_entry", icon="REMOVE", text="").propname=pg_name
|
||||
subcol.separator()
|
||||
op_move = subcol.operator("gptb.move_item", icon="TRIA_UP", text="")
|
||||
op_move.propname = pg_name
|
||||
op_move.direction = 'UP'
|
||||
op_move = subcol.operator("gptb.move_item", icon="TRIA_DOWN", text="")
|
||||
op_move.propname = pg_name
|
||||
op_move.direction = 'DOWN'
|
||||
|
||||
## Reset entry (Not needed anymore)
|
||||
# subcol.separator()
|
||||
# subcol.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'PREFIXES'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout## random color
|
||||
# layout.use_property_split = True
|
||||
|
@ -336,14 +404,53 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
row.label(text='Render Resolution')
|
||||
row.prop(self, 'render_res_x', text='Width')
|
||||
row.prop(self, 'render_res_y', text='Height')
|
||||
|
||||
box.prop(self, 'use_relative_remap_on_save')
|
||||
box.prop(self, "render_obj_exclusion", icon='FILTER')#
|
||||
|
||||
subbox = box.box()
|
||||
subbox.label(text='Project folders:')
|
||||
|
||||
## Palette
|
||||
box.label(text='Project folders:')
|
||||
box.prop(self, 'palette_path')
|
||||
box.prop(self, 'brush_path')
|
||||
subbox.prop(self, 'use_env_palettes', text='Use Palettes Environnement Path')
|
||||
subbox.prop(self, 'palette_path')
|
||||
subbox.prop(self, 'warn_base_palette')
|
||||
|
||||
subbox.prop(self, 'mat_link_exclude')
|
||||
|
||||
## Brushes
|
||||
subbox.prop(self, 'use_env_brushes', text='Use Brushes Environnement Path')
|
||||
subbox.prop(self, 'brush_path')
|
||||
|
||||
## render output
|
||||
box.prop(self, 'output_path')
|
||||
box.prop(self, 'use_relative_remap_on_save')
|
||||
subbox.prop(self, 'output_path')
|
||||
|
||||
## namespace
|
||||
subbox = box.box()
|
||||
subbox.label(text='Namespace:')
|
||||
subbox.prop(self, 'separator')
|
||||
subrow = subbox.row()
|
||||
subrow.prop(self, 'show_prefix_buttons', text='Use Prefixes Toggles')
|
||||
|
||||
if self.show_prefix_buttons:
|
||||
rowrow = subrow.row()
|
||||
# Reset Names From Projects
|
||||
rowrow.alignment = 'RIGHT'
|
||||
rowrow.operator('gptb.reset_project_namespaces', text='', icon='BRUSH_DATA')
|
||||
"""
|
||||
row = subbox.row()
|
||||
row.prop(self, 'prefixes')
|
||||
row.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'PREFIXES'
|
||||
row = subbox.row(align=True)
|
||||
row.prop(self, 'suffixes')
|
||||
row.operator('prefs.reset_gp_toolbox_env', text='', icon='LOOP_BACK').mode = 'SUFFIXES'
|
||||
"""
|
||||
|
||||
## Collection UI list version
|
||||
self.draw_namespaces_list(subbox, 'GPTB_UL_namespace_list', 'prefixes', rows=4)
|
||||
subbox.separator()
|
||||
self.draw_namespaces_list(subbox, 'GPTB_UL_namespace_list_suffix', 'suffixes', rows=2)
|
||||
|
||||
|
||||
### TODO add render settings
|
||||
|
||||
|
@ -352,58 +459,131 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
box.label(text='Playblast options:')
|
||||
box.prop(self, 'playblast_auto_play')
|
||||
box.prop(self, 'playblast_auto_open_folder')
|
||||
box.prop(self, 'playblast_path')
|
||||
|
||||
# box.separator()## Keyframe jumper
|
||||
box = layout.box()
|
||||
box.label(text='Keyframe Jump options:')
|
||||
|
||||
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
|
||||
if self.kfj_use_shortcut:
|
||||
prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
|
||||
if self.kfj_prev_keycode:
|
||||
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_prev_ctrl), ('Shift', self.kfj_prev_shift), ('Alt', self.kfj_prev_alt)] if b])
|
||||
text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
|
||||
text = f'Jump Keyframe Prev: {text} (Click to change)'
|
||||
else:
|
||||
text = prompt
|
||||
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
ops.s_keycode = 'kfj_prev_keycode'
|
||||
ops.s_ctrl = 'kfj_prev_ctrl'
|
||||
ops.s_shift = 'kfj_prev_shift'
|
||||
ops.s_alt = 'kfj_prev_alt'
|
||||
## Keyframe jump now displayed in Shortcut Tab
|
||||
# box = layout.box()
|
||||
# box.label(text='Keyframe Jump options:')
|
||||
|
||||
if self.kfj_next_keycode:
|
||||
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
||||
text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
|
||||
text = f'Jump Keyframe Next: {text} (Click to change)'
|
||||
else:
|
||||
text = prompt
|
||||
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
ops.s_keycode = 'kfj_next_keycode'
|
||||
ops.s_ctrl = 'kfj_next_ctrl'
|
||||
ops.s_shift = 'kfj_next_shift'
|
||||
ops.s_alt = 'kfj_next_alt'
|
||||
# box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
|
||||
# if self.kfj_use_shortcut:
|
||||
# prompt = '[TYPE SHORTCUT TO USE (can be with modifiers)]'
|
||||
# if self.kfj_prev_keycode:
|
||||
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_prev_ctrl), ('Shift', self.kfj_prev_shift), ('Alt', self.kfj_prev_alt)] if b])
|
||||
# text = f'{mods}+{self.kfj_prev_keycode}' if mods else self.kfj_prev_keycode
|
||||
# text = f'Jump Keyframe Prev: {text} (Click to change)'
|
||||
# else:
|
||||
# text = prompt
|
||||
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
# ops.s_keycode = 'kfj_prev_keycode'
|
||||
# ops.s_ctrl = 'kfj_prev_ctrl'
|
||||
# ops.s_shift = 'kfj_prev_shift'
|
||||
# ops.s_alt = 'kfj_prev_alt'
|
||||
|
||||
else:
|
||||
box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
||||
box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
||||
# if self.kfj_next_keycode:
|
||||
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
||||
# text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
|
||||
# text = f'Jump Keyframe Next: {text} (Click to change)'
|
||||
# else:
|
||||
# text = prompt
|
||||
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
# ops.s_keycode = 'kfj_next_keycode'
|
||||
# ops.s_ctrl = 'kfj_next_ctrl'
|
||||
# ops.s_shift = 'kfj_next_shift'
|
||||
# ops.s_alt = 'kfj_next_alt'
|
||||
|
||||
## Active tool
|
||||
box = layout.box()
|
||||
box.label(text='Autofix check button options:')
|
||||
box.prop(self, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
||||
|
||||
|
||||
box.prop(self, "render_obj_exclusion", icon='FILTER')#
|
||||
|
||||
## random color character separator
|
||||
box = layout.box()
|
||||
box.label(text='Random color options:')
|
||||
box.prop(self, 'separator')
|
||||
# else:
|
||||
# box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
||||
# box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
||||
|
||||
box = layout.box()
|
||||
box.label(text='Tools options:')
|
||||
box.prop(self, 'use_precise_eraser')
|
||||
|
||||
subbox= box.box()
|
||||
subbox.label(text='Layer Navigation')
|
||||
col = subbox.column()
|
||||
col.prop(self, 'nav_use_fade')
|
||||
if self.nav_use_fade:
|
||||
row = col.row()
|
||||
row.prop(self, 'nav_fade_val')
|
||||
row.prop(self, 'nav_limit')
|
||||
row = subbox.row(align=False)
|
||||
row.prop(self, 'nav_use_fade_in')
|
||||
if self.nav_use_fade_in:
|
||||
row.prop(self, 'nav_fade_in_time', text='Fade Back Time')
|
||||
# row.prop(self, 'nav_interval') # Do not expose refresh rate for now, not usefull to user...
|
||||
|
||||
# box.prop(self, 'use_precise_eraser') # precise eraser
|
||||
|
||||
if self.is_git_repo:
|
||||
box = layout.box()
|
||||
box.label(text='Addon Update')
|
||||
if self.is_git_repo and self.has_git:
|
||||
box.operator('gptb.git_pull', text='Check / Get Last Update', icon='PLUGIN')
|
||||
else:
|
||||
box.label(text='Toolbox can be updated using git')
|
||||
row = box.row()
|
||||
row.operator('wm.url_open', text='Download and install git here', icon='URL').url = 'https://git-scm.com/download/'
|
||||
row.label(text='then restart blender')
|
||||
|
||||
if self.pref_tabs == 'KEYS':
|
||||
# layout.label(text='Shortcuts :')
|
||||
box = layout.box()
|
||||
box.label(text='Shortcuts added by GP toolbox with context scope:')
|
||||
## not available directly :
|
||||
## keymaps.addon_keymaps <<- one two three on sculpt, not exposed
|
||||
## OP_temp_cutter # not active by defaut
|
||||
## TOOL_eraser_brush.addon_keymaps # has a checkbox in
|
||||
|
||||
prev_key_category = ''
|
||||
for kms in [
|
||||
OP_keyframe_jump.addon_keymaps,
|
||||
OP_copy_paste.addon_keymaps,
|
||||
OP_breakdowner.addon_keymaps,
|
||||
OP_key_duplicate_send.addon_keymaps,
|
||||
OP_layer_picker.addon_keymaps,
|
||||
OP_material_picker.addon_keymaps,
|
||||
OP_layer_nav.addon_keymaps,
|
||||
# OP_layer_manager.addon_keymaps, # Do not display, wm.call_panel call panel ops mixed with natives shortcut (F2)
|
||||
]:
|
||||
|
||||
ct = 0
|
||||
for akm, akmi in kms:
|
||||
km = bpy.context.window_manager.keyconfigs.user.keymaps.get(akm.name)
|
||||
if not km:
|
||||
continue
|
||||
key_category = km.name
|
||||
# kmi = km.keymap_items.get(akmi.idname) # get only first idname when multiple entry
|
||||
kmi = None
|
||||
|
||||
## numbering hack, need a better way to find multi idname user keymaps
|
||||
id_ct = 0
|
||||
for km_item in km.keymap_items:
|
||||
if km_item.idname == akmi.idname:
|
||||
if ct > id_ct:
|
||||
id_ct +=1
|
||||
continue
|
||||
|
||||
kmi = km_item
|
||||
ct += 1
|
||||
break
|
||||
|
||||
if not kmi:
|
||||
continue
|
||||
|
||||
## show keymap category (ideally grouped by category)
|
||||
if not prev_key_category:
|
||||
if key_category:
|
||||
box.label(text=key_category)
|
||||
elif key_category and key_category != prev_key_category: # check if has changed singe
|
||||
box.label(text=key_category)
|
||||
|
||||
draw_kmi(km, kmi, box)
|
||||
prev_key_category = key_category
|
||||
|
||||
box.separator()
|
||||
|
||||
if self.pref_tabs == 'MAN_OPS':
|
||||
# layout.separator()## notes
|
||||
|
@ -444,25 +624,147 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
if self.pref_tabs == 'CHECKS':
|
||||
layout.label(text='Following checks will be made when clicking "Check File" button:')
|
||||
col = layout.column()
|
||||
# row = col.row()
|
||||
col.use_property_split = True
|
||||
# col.prop(self.fixprops, 'check_only')
|
||||
col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO')
|
||||
# col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
|
||||
col.separator()
|
||||
col.prop(self.fixprops, 'lock_main_cam')
|
||||
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
|
||||
col.prop(self.fixprops, 'set_res_percentage')
|
||||
col.prop(self.fixprops, 'set_fps', text=f'Reset FPS (to {self.fps})')
|
||||
col.prop(self.fixprops, 'set_slider_n_sync')
|
||||
col.prop(self.fixprops, 'set_cursor_type')
|
||||
col.prop(self.fixprops, 'check_front_axis')
|
||||
col.prop(self.fixprops, 'check_placement')
|
||||
col.prop(self.fixprops, 'set_gp_use_lights_off')
|
||||
col.prop(self.fixprops, 'set_pivot_median_point')
|
||||
col.prop(self.fixprops, 'disable_guide')
|
||||
col.prop(self.fixprops, 'list_disabled_anim')
|
||||
col.prop(self.fixprops, 'list_obj_vis_conflict')
|
||||
col.prop(self.fixprops, 'list_gp_mod_vis_conflict')
|
||||
col.prop(self.fixprops, 'list_broken_mod_targets')
|
||||
col.prop(self.fixprops, 'autokey_add_n_replace')
|
||||
col.prop(self.fixprops, 'remove_redundant_strokes')
|
||||
#-# col.prop(self.fixprops, 'set_cursor_type')
|
||||
|
||||
# col = layout.column()
|
||||
# col.use_property_split = True
|
||||
col.prop(self.fixprops, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
||||
col.prop(self.fixprops, "file_path_type")
|
||||
col.prop(self.fixprops, "lock_object_mode")
|
||||
# row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)')
|
||||
|
||||
# if self.pref_tabs == 'UPDATE':
|
||||
# addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
|
||||
if self.pref_tabs == 'UPDATE':
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
### --- ENV_PROP ---
|
||||
def set_namespace_env(name_env, prop_group):
|
||||
tag_list = os.getenv(name_env)
|
||||
current_pfix = []
|
||||
project_pfix = []
|
||||
|
||||
if tag_list and tag_list.strip():
|
||||
## Force clear (clear also hide): prop_group.namespaces.clear()
|
||||
|
||||
## Get current tag list
|
||||
tag_list = tag_list.strip(', ').split(',')
|
||||
|
||||
current_pfix = [n.tag for n in prop_group.namespaces if n.tag]
|
||||
# for n in prop_group.namespaces:
|
||||
# print(n.tag, n.name)
|
||||
|
||||
for p in tag_list:
|
||||
tag = p.split(':')[0].strip()
|
||||
project_pfix.append(tag)
|
||||
name = '' if not ':' in p else p.split(':')[1].strip()
|
||||
item = None
|
||||
if tag not in current_pfix:
|
||||
item = prop_group.namespaces.add()
|
||||
item.tag = tag
|
||||
item.name = name
|
||||
# print('Loaded project tag:', tag, name)
|
||||
elif name:
|
||||
# get the tag and apply name
|
||||
item = next((n for n in prop_group.namespaces if n.tag == tag), None)
|
||||
if item: # and not item.name.strip()
|
||||
item.name = name
|
||||
# print('Loaded name:', name)
|
||||
|
||||
if item:
|
||||
item.is_project = True
|
||||
|
||||
else:
|
||||
tag_list = []
|
||||
|
||||
# "release" suffix that are not in project anymore
|
||||
for n in prop_group.namespaces:
|
||||
n.is_project = n.tag in project_pfix
|
||||
|
||||
def set_env_properties():
|
||||
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
fps = os.getenv('FPS')
|
||||
prefs.fps = int(fps) if fps else prefs.fps
|
||||
|
||||
render_width = os.getenv('RENDER_WIDTH')
|
||||
prefs.render_res_x = int(render_width) if render_width else prefs.render_res_x
|
||||
|
||||
render_height = os.getenv('RENDER_HEIGHT')
|
||||
prefs.render_res_y = int(render_height) if render_height else prefs.render_res_y
|
||||
|
||||
palettes = os.getenv('PALETTES')
|
||||
if prefs.use_env_palettes:
|
||||
prefs.palette_path = palettes if palettes else prefs.palette_path
|
||||
|
||||
brushes = os.getenv('BRUSHES')
|
||||
if prefs.use_env_brushes:
|
||||
prefs.brush_path = brushes if brushes else prefs.brush_path
|
||||
|
||||
# if prefs.use_env_namespace:
|
||||
|
||||
## Old method with direct string assignment (now a property group)
|
||||
# prefix_list = os.getenv('PREFIXES')
|
||||
# prefs.prefixes = prefix_list if prefix_list else prefs.prefixes
|
||||
# suffix_list = os.getenv('SUFFIXES')
|
||||
# prefs.suffixes = suffix_list if suffix_list else prefs.suffixes
|
||||
|
||||
set_namespace_env('PREFIXES', prefs.prefixes)
|
||||
set_namespace_env('SUFFIXES', prefs.suffixes)
|
||||
|
||||
separator = os.getenv('SEPARATOR')
|
||||
prefs.separator = separator if separator else prefs.separator
|
||||
|
||||
|
||||
class GPTB_set_env_settings(bpy.types.Operator):
|
||||
"""manually reset prefs from project environnement setttings"""
|
||||
bl_idname = "prefs.reset_gp_toolbox_env"
|
||||
bl_label = "Reset prefs from project environnement settings (if any)"
|
||||
|
||||
mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) # 'HIDDEN',
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if self.mode == 'ALL':
|
||||
set_env_properties()
|
||||
elif self.mode == 'PREFIXES':
|
||||
prefix_list = os.getenv('PREFIXES')
|
||||
if not prefix_list:
|
||||
self.report({'ERROR'}, 'No prefix preset to load from project environnement')
|
||||
return {'CANCELLED'}
|
||||
set_namespace_env('PREFIXES', prefs.prefixes)
|
||||
|
||||
elif self.mode == 'SUFFIXES':
|
||||
suffix_list = os.getenv('SUFFIXES')
|
||||
if not suffix_list:
|
||||
self.report({'ERROR'}, 'No suffix preset to load from project environnement')
|
||||
return {'CANCELLED'}
|
||||
set_namespace_env('SUFFIXES', prefs.suffixes)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
### --- REGISTER ---
|
||||
|
@ -472,80 +774,81 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
|
||||
|
||||
classes = (
|
||||
GP_PG_FixSettings,
|
||||
GP_PG_ToolsSettings,
|
||||
GPTB_set_env_settings,
|
||||
GPTB_prefs,
|
||||
GPT_OT_auto_tint_gp_layers,
|
||||
)
|
||||
|
||||
# register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
addon_modules = (
|
||||
OP_helpers,
|
||||
OP_pseudo_tint,
|
||||
OP_keyframe_jump,
|
||||
OP_file_checker,
|
||||
OP_breakdowner,
|
||||
OP_temp_cutter,
|
||||
GP_colorize,
|
||||
OP_playblast_bg,
|
||||
OP_playblast,
|
||||
OP_palettes,
|
||||
OP_palettes_linker,
|
||||
OP_brushes,
|
||||
OP_cursor_snap_canvas,
|
||||
OP_copy_paste,
|
||||
# OP_flat_reproject # Disabled,
|
||||
OP_realign,
|
||||
OP_depth_move,
|
||||
OP_key_duplicate_send,
|
||||
OP_layer_namespace,
|
||||
OP_layer_manager,
|
||||
OP_material_picker,
|
||||
OP_git_update,
|
||||
OP_layer_picker,
|
||||
OP_layer_nav,
|
||||
OP_follow_curve,
|
||||
OP_material_move_to_layer,
|
||||
# OP_eraser_brush,
|
||||
# TOOL_eraser_brush, # experimental eraser brush
|
||||
handler_draw_cam,
|
||||
UI_tools,
|
||||
keymaps,
|
||||
)
|
||||
|
||||
def register():
|
||||
addon_updater_ops.register(bl_info)
|
||||
# bpy.types.Scene.gpfixprops = bpy.props.PointerProperty(type = GP_PG_FixSettings) # used in prefs
|
||||
# Register property group first
|
||||
properties.register()
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
OP_helpers.register()
|
||||
OP_keyframe_jump.register()
|
||||
OP_file_checker.register()
|
||||
OP_breakdowner.register()
|
||||
OP_temp_cutter.register()
|
||||
GP_colorize.register()## GP_guided_colorize.
|
||||
OP_playblast_bg.register()
|
||||
OP_playblast.register()
|
||||
OP_palettes.register()
|
||||
OP_brushes.register()
|
||||
OP_cursor_snap_canvas.register()
|
||||
OP_render.register()
|
||||
OP_copy_paste.register()
|
||||
OP_realign.register()
|
||||
OP_depth_move.register()
|
||||
OP_key_duplicate_send.register()
|
||||
OP_eraser_brush.register()
|
||||
TOOL_eraser_brush.register()
|
||||
handler_draw_cam.register()
|
||||
UI_tools.register()
|
||||
keymaps.register()
|
||||
|
||||
for mod in addon_modules:
|
||||
mod.register()
|
||||
|
||||
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings)
|
||||
|
||||
# add handler (if option is on)
|
||||
set_env_properties()
|
||||
|
||||
if get_addon_prefs().use_relative_remap_on_save:
|
||||
## add handler (if option is on)
|
||||
prefs = get_addon_prefs()
|
||||
if prefs.use_relative_remap_on_save:
|
||||
if not 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
||||
bpy.app.handlers.save_pre.append(remap_relative)
|
||||
|
||||
## Change a variable in prefs if a '.git is detected'
|
||||
prefs.is_git_repo = (Path(__file__).parent / '.git').exists()
|
||||
prefs.has_git = bool(which('git'))
|
||||
|
||||
|
||||
|
||||
def unregister():
|
||||
# remove handler
|
||||
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
||||
bpy.app.handlers.save_pre.remove(remap_relative)
|
||||
|
||||
keymaps.unregister()
|
||||
addon_updater_ops.unregister()
|
||||
for mod in reversed(addon_modules):
|
||||
mod.unregister()
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
UI_tools.unregister()
|
||||
handler_draw_cam.unregister()
|
||||
OP_eraser_brush.unregister()
|
||||
TOOL_eraser_brush.unregister()
|
||||
OP_key_duplicate_send.unregister()
|
||||
OP_depth_move.unregister()
|
||||
OP_realign.unregister()
|
||||
OP_copy_paste.unregister()
|
||||
OP_render.unregister()
|
||||
OP_cursor_snap_canvas.unregister()
|
||||
OP_brushes.unregister()
|
||||
OP_palettes.unregister()
|
||||
OP_file_checker.unregister()
|
||||
OP_helpers.unregister()
|
||||
OP_keyframe_jump.unregister()
|
||||
OP_breakdowner.unregister()
|
||||
OP_temp_cutter.unregister()
|
||||
GP_colorize.unregister()## GP_guided_colorize.
|
||||
OP_playblast_bg.unregister()
|
||||
OP_playblast.unregister()
|
||||
# del bpy.types.Scene.gpfixprops
|
||||
|
||||
properties.unregister()
|
||||
|
||||
del bpy.types.Scene.gptoolprops
|
||||
|
||||
|
||||
|
|
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
53
functions.py
53
functions.py
|
@ -6,24 +6,7 @@ from random import random as rand
|
|||
import numpy as np
|
||||
from bpy_extras.object_utils import world_to_camera_view as cam_space
|
||||
import bmesh
|
||||
from .utils import link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
|
||||
|
||||
|
||||
def get_view_origin_position():
|
||||
#method 1
|
||||
from bpy_extras import view3d_utils
|
||||
region = bpy.context.region
|
||||
rv3d = bpy.context.region_data
|
||||
view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||
print("view_loc1", view_loc)#Dbg
|
||||
|
||||
#method 2
|
||||
r3d = bpy.context.space_data.region_3d
|
||||
view_loc2 = r3d.view_matrix.inverted().translation
|
||||
print("view_loc2", view_loc2)#Dbg
|
||||
if view_loc != view_loc2: print('there might be an errror when finding view coordinate')
|
||||
|
||||
return view_loc
|
||||
from .utils import get_gp_draw_plane, link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
|
||||
|
||||
|
||||
def to_bl_image(array, img):
|
||||
|
@ -116,7 +99,8 @@ def gp_stroke_angle_split (frame,strokes,angle):
|
|||
|
||||
splitted_loops = bm_angle_split(bm,angle)
|
||||
|
||||
frame.strokes.remove(stroke_info['stroke'])
|
||||
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||
frame.drawing.strokes.remove(stroke_info['stroke'])
|
||||
for loop in splitted_loops :
|
||||
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
|
||||
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
|
||||
|
@ -140,6 +124,7 @@ def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
|
|||
|
||||
bm_uniform_density(bm,cam,max_spacing)
|
||||
|
||||
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||
frame.strokes.remove(stroke_info['stroke'])
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
|
@ -166,7 +151,7 @@ def along_stroke(stroke,attr,length,min,max) :
|
|||
|
||||
def randomise_points(mat, points, attr, strength) :
|
||||
for point in points :
|
||||
if attr is 'co' :
|
||||
if attr == 'co' :
|
||||
random_x = (rand()-0.5)
|
||||
random_y = (rand()-0.5)
|
||||
|
||||
|
@ -182,7 +167,6 @@ def randomise_points(mat,points,attr,strength) :
|
|||
setattr(point,attr,value+random*strength)
|
||||
|
||||
|
||||
|
||||
def zoom_to_object(cam, resolution, box, margin=0.01) :
|
||||
min_x= box[0]
|
||||
max_x= box[1]
|
||||
|
@ -233,27 +217,6 @@ def zoom_to_object(cam,resolution,box,margin=0.01) :
|
|||
#print(matrix,resolution)
|
||||
return modelview_matrix,projection_matrix,frame,resolution
|
||||
|
||||
|
||||
|
||||
def set_viewport_matrix(width,height,mat):
|
||||
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
|
||||
|
||||
glViewport(0,0,width,height)
|
||||
|
||||
#glLoadIdentity()
|
||||
|
||||
glMatrixMode(GL_PROJECTION)
|
||||
|
||||
projection = [mat[j][i] for i in range(4) for j in range(4)]
|
||||
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
|
||||
|
||||
#glMatrixMode( GL_MODELVIEW )
|
||||
#glLoadIdentity()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# get object info
|
||||
def get_object_info(mesh_groups, order_list = []) :
|
||||
scene = bpy.context.scene
|
||||
|
@ -379,3 +342,9 @@ def get_object_info(mesh_groups,order_list = []) :
|
|||
|
||||
|
||||
return mesh_info, convert_table
|
||||
|
||||
def redraw_ui() -> None:
|
||||
"""Forces blender to redraw the UI."""
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import bpy
|
||||
import gpu
|
||||
import bgl
|
||||
# import blf
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
@ -30,22 +29,37 @@ def view3d_camera_border_2d(context, cam):
|
|||
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
|
||||
return frame_px
|
||||
|
||||
def vertices_to_line_loop(v_list, closed=True) -> list:
|
||||
'''Take a sequence of vertices
|
||||
return a position lists of segments to create a line loop passing in all points
|
||||
the result is usable with gpu_shader 'LINES'
|
||||
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
|
||||
'''
|
||||
loop = []
|
||||
for i in range(len(v_list) - 1):
|
||||
loop += [v_list[i], v_list[i + 1]]
|
||||
if closed:
|
||||
# Add segment between last and first to close loop
|
||||
loop += [v_list[-1], v_list[0]]
|
||||
return loop
|
||||
|
||||
def draw_cam_frame_callback_2d():
|
||||
context = bpy.context
|
||||
if context.region_data.view_perspective != 'CAMERA':
|
||||
return
|
||||
if context.scene.camera.name != 'draw_cam':
|
||||
if not context.scene.camera or context.scene.camera.name != 'draw_cam':
|
||||
return
|
||||
|
||||
main_cam = context.scene.camera.parent
|
||||
if not main_cam:
|
||||
return
|
||||
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
frame_point = view3d_camera_border_2d(
|
||||
context, context.scene.camera.parent)
|
||||
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
|
||||
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||
|
||||
if context.scene.gptoolprops.drawcam_passepartout:
|
||||
### PASSEPARTOUT
|
||||
|
@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
|
|||
|
||||
### Camera framing trace over
|
||||
|
||||
bgl.glLineWidth(1)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
gpu.state.line_width_set(1.0)
|
||||
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||
|
||||
"""
|
||||
## need to accurately detect viewport background color (difficult)
|
||||
|
@ -135,15 +149,14 @@ def draw_cam_frame_callback_2d():
|
|||
frame_color = (0.0, 0.0, 0.25, 1.0)
|
||||
|
||||
screen_framing = batch_for_shader(
|
||||
shader_2d, 'LINE_LOOP', {"pos": frame_point})
|
||||
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
|
||||
|
||||
shader_2d.bind()
|
||||
shader_2d.uniform_float("color", frame_color)
|
||||
screen_framing.draw(shader_2d)
|
||||
|
||||
# bgl.glLineWidth(1)
|
||||
bgl.glDisable(bgl.GL_LINE_SMOOTH)
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
draw_handle = None
|
||||
|
|
|
@ -8,6 +8,8 @@ def register_keymaps():
|
|||
# km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
|
||||
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
|
||||
|
||||
|
||||
## Sculpt mode toggles
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Sculpt Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
|
||||
kmi = km.keymap_items.new('wm.context_toggle', type='ONE', value='PRESS')
|
||||
|
@ -22,6 +24,12 @@ def register_keymaps():
|
|||
kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_segment'
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
## T temp cutter (need disabling of native T shortcut, maybe expose a button to set the shortcut as user ?)
|
||||
# km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY")
|
||||
# kmi = km.keymap_items.new('gpencil.stroke_cutter', type='LEFTMOUSE', value='PRESS', key_modifier='T')
|
||||
# kmi.properties.flat_caps=False
|
||||
# addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
|
|
144
properties.py
144
properties.py
|
@ -1,4 +1,5 @@
|
|||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
|
@ -8,7 +9,7 @@ from bpy.props import (
|
|||
)
|
||||
|
||||
from .OP_cursor_snap_canvas import cursor_follow_update
|
||||
|
||||
from .OP_layer_manager import layer_name_build
|
||||
|
||||
def change_edit_lines_opacity(self, context):
|
||||
for gp in bpy.data.grease_pencils:
|
||||
|
@ -16,7 +17,21 @@ def change_edit_lines_opacity(self, context):
|
|||
gp.edit_line_color[3]=self.edit_lines_opacity
|
||||
|
||||
|
||||
class GP_PG_FixSettings(bpy.types.PropertyGroup):
|
||||
def update_layer_name(self, context):
|
||||
if not self.layer_name:
|
||||
# never replace by nothing (since there should be prefix/suffix)
|
||||
return
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not context.object.data.layers.active:
|
||||
return
|
||||
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
|
||||
# context.object.data.layers.active.name = self.layer_name
|
||||
|
||||
|
||||
class GP_PG_FixSettings(PropertyGroup):
|
||||
|
||||
|
||||
lock_main_cam : BoolProperty(
|
||||
name="Lock Main Cam",
|
||||
description="Lock the main camera (works only if 'layout' is not in name)",
|
||||
|
@ -25,7 +40,7 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
|
|||
set_scene_res : BoolProperty(
|
||||
name="Reset Scene Resolution",
|
||||
description="Set the scene resolution to current prefs project settings",
|
||||
default=True, options={'HIDDEN'})
|
||||
default=False, options={'HIDDEN'})
|
||||
|
||||
set_res_percentage : BoolProperty(
|
||||
name="Reset Resolution Percentage To 100%",
|
||||
|
@ -42,9 +57,9 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
|
|||
description="Toggle on the use of show slider and sync range",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
set_cursor_type : BoolProperty(
|
||||
name="Set Select Cursor Mode",
|
||||
description="Set the type of the selection cursor (according to addon prefs)",
|
||||
set_gp_use_lights_off : BoolProperty(
|
||||
name="Set Off Use lights On All Gpencil Objects",
|
||||
description="Uncheck Use lights on all grease pencil objects\nAt object level, not layer level (Object properties > Visibility > GP uselight)",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
check_front_axis : BoolProperty(
|
||||
|
@ -65,19 +80,76 @@ class GP_PG_FixSettings(bpy.types.PropertyGroup):
|
|||
disable_guide : BoolProperty(
|
||||
name="Disable Drawing Guide",
|
||||
description="Disable constrained guide in draw mode",
|
||||
default=False, options={'HIDDEN'})
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_disabled_anim : BoolProperty(
|
||||
name="List Disabled Animation",
|
||||
description="Alert if there are disabled animations",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_obj_vis_conflict : BoolProperty(
|
||||
name="List Object Visibility Conflicts",
|
||||
description="Alert if some objects have different hide viewport and hide render values",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_gp_mod_vis_conflict : BoolProperty(
|
||||
name="List GP Object Modifiers Visibility Conflicts",
|
||||
description="Alert if some GP modifier have different show viewport and show render values",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_broken_mod_targets : BoolProperty(
|
||||
name="List GP Object modifiers broken target",
|
||||
description="Alert if some GP modifier have a target layer not existing in layer stack",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
autokey_add_n_replace : BoolProperty(
|
||||
name="Autokey Mode Add and Replace",
|
||||
description="Change autokey mode back to 'Add & Replace'",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
|
||||
remove_redundant_strokes : BoolProperty(
|
||||
name="Remove Redundant Strokes",
|
||||
description="Remove GP strokes duplication. When points are exactly identical within the same frame",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
# set_cursor_type : BoolProperty(
|
||||
# name="Set Select Cursor Mode",
|
||||
# description="Set the type of the selection cursor (according to addon prefs)",
|
||||
# default=True, options={'HIDDEN'})
|
||||
|
||||
## default active tool to use
|
||||
select_active_tool : EnumProperty(
|
||||
name="Set Default Selection Tool", description="Active tool to set when launching check fix scene",
|
||||
default='builtin.select_lasso',
|
||||
items=(
|
||||
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
|
||||
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
|
||||
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
|
||||
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
|
||||
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
|
||||
))
|
||||
|
||||
## check file path mapping type
|
||||
file_path_type : EnumProperty(
|
||||
name="Check File Path Mapping", description="Check if all path in scene are relative, absolute or disable the check",
|
||||
default='RELATIVE',
|
||||
items=(
|
||||
('none', 'No Check', 'Do not check for path type', 0),
|
||||
('RELATIVE', 'Relative', 'Check if all path are relative', 1),
|
||||
('ABSOLUTE', 'Absolute', 'Check if all path are absolute', 2),
|
||||
))
|
||||
|
||||
## check set lock object mode
|
||||
lock_object_mode : EnumProperty(
|
||||
name="Set Lock Object Mode", description="Set 'Lock object mode' parameter check'",
|
||||
default='none',
|
||||
items=(
|
||||
('none', 'Do nothing', 'Do not set', 0),
|
||||
('LOCK', 'Lock object mode', 'Toggle lock object mode On', 1),
|
||||
('UNLOCK', 'Unlock object mode', 'Toggle lock object mode Off', 2),
|
||||
))
|
||||
|
||||
class GP_PG_ToolsSettings(PropertyGroup):
|
||||
eraser_radius : IntProperty(
|
||||
name="Eraser Radius", description="Radius of eraser brush",
|
||||
default=20, min=0, max=500, subtype='PIXEL')
|
||||
|
@ -106,9 +178,15 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
|
|||
name='Cursor Follow', description="3D cursor follow active object animation when activated",
|
||||
default=False, update=cursor_follow_update)
|
||||
|
||||
edit_lines_opacity : FloatProperty(
|
||||
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils",
|
||||
default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
|
||||
cursor_follow_target : bpy.props.PointerProperty(
|
||||
name='Cursor Follow Target',
|
||||
description="Optional target object to follow for cursor instead of active object",
|
||||
type=bpy.types.Object, update=cursor_follow_update)
|
||||
|
||||
## gpv3 : no edit line color anymore
|
||||
# edit_lines_opacity : FloatProperty(
|
||||
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
|
||||
# default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
|
||||
|
||||
## render
|
||||
name_for_current_render : StringProperty(
|
||||
|
@ -127,6 +205,12 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
|
|||
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
|
||||
))
|
||||
|
||||
layer_name : StringProperty(
|
||||
name="Layer Name",
|
||||
description="The layer name, should describe the content of the layer",
|
||||
default="",
|
||||
update=update_layer_name)# update=None, get=None, set=None
|
||||
|
||||
"""
|
||||
reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)
|
||||
render_settings = bpy.props.BoolProperty(default = False)
|
||||
|
@ -147,3 +231,41 @@ class GP_PG_ToolsSettings(bpy.types.PropertyGroup):
|
|||
|
||||
stroke_select = bpy.props.EnumProperty(items = [("POINT","Point",""),("STROKE","Stroke","")],update = update_selection_mode)
|
||||
"""
|
||||
|
||||
class GP_PG_namespace_props(PropertyGroup):
|
||||
|
||||
tag : StringProperty(
|
||||
name="Tag", description="Layer namespace tag (prefix/suffix)",
|
||||
default="")
|
||||
name : StringProperty(
|
||||
name="Name", description="Name that represent this prefix (used as hint and tooltip)",
|
||||
default="")
|
||||
hide : BoolProperty(
|
||||
name="Hide", description="Hide this prefix from layer prefix management",
|
||||
default=False)
|
||||
|
||||
is_project : BoolProperty(
|
||||
name="Project", description="Show this propery was set by project environnement (not deletable if that's the case)",
|
||||
default=False)
|
||||
|
||||
class GP_PG_namespaces(PropertyGroup):
|
||||
idx : IntProperty(default=-1)
|
||||
namespaces : bpy.props.CollectionProperty(type=GP_PG_namespace_props)
|
||||
|
||||
classes = (
|
||||
# Prefix/suiffix prefs prop group
|
||||
GP_PG_namespace_props,
|
||||
GP_PG_namespaces,
|
||||
|
||||
## General toolbox settings
|
||||
GP_PG_FixSettings,
|
||||
GP_PG_ToolsSettings,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -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