Compare commits
No commits in common. "master" and "v1.0.3" have entirely different histories.
748
CHANGELOG.md
748
CHANGELOG.md
|
@ -1,748 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
4.0.3
|
||||
|
||||
changed: File checker doest not fix directly when clicked (also removed choice in preference):
|
||||
- list potential change and display an `Apply Fix`
|
||||
changed: Enhanced visibility conflict list:
|
||||
- also include viewlayer hide value
|
||||
- allow to set all hide value from the state of one of the three
|
||||
- fixed: material move operator
|
||||
|
||||
4.0.1
|
||||
|
||||
- fixed: layer nav operator on page up/down
|
||||
|
||||
4.0.0
|
||||
|
||||
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
|
||||
|
||||
3.3.0
|
||||
|
||||
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
|
||||
|
||||
3.2.0
|
||||
|
||||
- added: UI settings to show GP tool settings placement and orientation
|
||||
- fixed: Bug with reproject orientation settings
|
||||
- added: show current orientation in batch reproject popup UI (if current is selected)
|
||||
|
||||
3.1.0
|
||||
|
||||
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
|
||||
|
||||
3.0.2
|
||||
|
||||
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
|
||||
|
||||
3.0.1
|
||||
|
||||
- fixed: Crash after generating empty frames
|
||||
|
||||
3.0.0
|
||||
|
||||
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
|
||||
- fixed: openGL draw camera frame and passepartout
|
||||
|
||||
2.5.0
|
||||
|
||||
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
|
||||
|
||||
2.4.0
|
||||
|
||||
- changed: Batch reproject consider camera movement and is almost 8x faster
|
||||
- added: Batch reproject have "Current" mode (using current tool setting)
|
||||
|
||||
2.3.4
|
||||
|
||||
- fixed: bug when exporting json palettes containing empty material slots
|
||||
|
||||
2.3.3
|
||||
|
||||
- fixed: Bug with animation manager objects data
|
||||
|
||||
2.3.2
|
||||
|
||||
- fixed: Bug with animation manager when there is empty object in scene
|
||||
|
||||
2.3.1
|
||||
|
||||
- changed: Animation manager show hints when animation is enabled: fully, partially or not at all.
|
||||
|
||||
2.3.0
|
||||
|
||||
- added: Animation manager buttons are colored red when objects have disabled animation
|
||||
- fixed: Animation manager not enabling/disabling Action Groups
|
||||
- fixed: Animation manager `List Disabled Anims` list groups as well
|
||||
|
||||
2.2.3
|
||||
|
||||
- fixed: Type error on realign ops
|
||||
|
||||
2.2.2
|
||||
|
||||
- fixed: draw_cam data not changed when working with multiple camera in a shot
|
||||
|
||||
2.2.1
|
||||
|
||||
- added: class View3D to calculate area 3d related coordinates
|
||||
|
||||
2.2.0
|
||||
|
||||
- added: _Remove redundant stroke_ in File checker (Just list duplicate numbers in "check only" mode)
|
||||
|
||||
2.1.6
|
||||
|
||||
- fixed: Prevent some keymaps to register when blender is launched in background mode
|
||||
|
||||
2.1.5
|
||||
|
||||
- fixed: false positive with checkfile in modifier layer target
|
||||
|
||||
2.1.4
|
||||
|
||||
- fixed: layer change msgbus not working at first activation of the addon
|
||||
|
||||
2.1.3
|
||||
|
||||
- fixed: decoralate Prefix and Suffix UI_lists scroll
|
||||
- fixed: Problem when settings project namespaces
|
||||
- added: Button to reset project namespace (to have the right order)
|
||||
|
||||
2.1.2
|
||||
|
||||
- added: `gp.brush_set` operator to manually assign a brush by name to a shortcut
|
||||
- preferably add shortcut to `Grease Pencil > Grease Pencil Stroke Paint Mode`
|
||||
|
||||
2.1.1
|
||||
|
||||
- added: follow curve show offset property in UI
|
||||
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||
- changed: created follow curve use `fixed offset`
|
||||
|
||||
2.1.1
|
||||
|
||||
- added: follow curve show offset property in UI
|
||||
- added: follow curve show clickable warning if object has non-zero location to reset location
|
||||
- changed: created follow curve use `fixed offset`
|
||||
|
||||
2.1.0
|
||||
|
||||
- added: 3 actions buttons:
|
||||
- create curve with follow path and go into curve edit
|
||||
- go back to object
|
||||
- got to curve edit (if follow path constraint exists with a curve target)
|
||||
- if follow path exists, button to remove constraint
|
||||
|
||||
2.0.11
|
||||
|
||||
- fix: prefix set by project environment
|
||||
|
||||
2.0.10
|
||||
|
||||
- fix: poll error in console
|
||||
|
||||
2.0.9
|
||||
|
||||
- fix: prefix/suffix UIlist actions trigger UI redraw to see changes live
|
||||
- changed: PropertyGroups are now registered in their own file
|
||||
- code: cleanup
|
||||
|
||||
2.0.8
|
||||
|
||||
- changed: suffix as UIlist in prefs,
|
||||
- fix: prefix and suffix register and load
|
||||
|
||||
2.0.7
|
||||
|
||||
- fix: broken auto-fade with gp layer navigation when used with a customized shortcut.
|
||||
- changed: supported version number to 3.0.0
|
||||
|
||||
2.0.6
|
||||
|
||||
- changed: Use prefixes toggle is enabled by default
|
||||
- changed: prefixes are now set in preferences as a reorderable UI list with full name for description
|
||||
- This should not affect `PREFIXES` env variable (`'CO, LN'`)
|
||||
- Now can be also passed as prefix:name pair ex: `'CO : Color, LN : Line`
|
||||
|
||||
2.0.5
|
||||
|
||||
- changed: redo panel for GP layer picker has better name and display active layer name
|
||||
- changed: some operator id_name to expose "add shortcut" in context menu for some button
|
||||
- fix: error with draw_cam handler when no camera is active
|
||||
|
||||
2.0.3
|
||||
|
||||
- changed: `X-ray` to `In Front`, match original object property name
|
||||
|
||||
2.0.2
|
||||
|
||||
- added: possibility to select which point attribute is copied by GP layer copy to clipboard
|
||||
|
||||
2.0.1
|
||||
|
||||
- added: enable/disable camera animation separately in `animation manager`
|
||||
- for cam,disable properties like focal and shift (to be able to work on a fixed cam)
|
||||
- camera is not disable using `obj` anymore
|
||||
|
||||
2.0.0
|
||||
|
||||
- fix: error using playblast (api changes of Blender 3.0+)
|
||||
- added: possibility to change playblast destination in preferences
|
||||
- changed: playblast default is `playblast` folder instead of `images`
|
||||
- changed: completely disable experimental precise eraser
|
||||
|
||||
1.9.9
|
||||
|
||||
- fix: Bug setting GP layers actions on/off
|
||||
- fix: Add GP layers animation datas in `list disabled animation` report
|
||||
|
||||
1.9.8
|
||||
|
||||
- fix: animation manager `GP anim on/off` also toggle layers animation datas
|
||||
|
||||
1.9.7
|
||||
|
||||
- changed: `list modifiers visibility conflict` (used in `check file` or through search menu) now not limited to GP objects type.
|
||||
|
||||
1.9.6
|
||||
|
||||
- fix: icon removed in 3.0
|
||||
|
||||
1.9.5
|
||||
|
||||
- added: `Check file` also check if GP modifiers have broken layer target
|
||||
- changed: `Check file` disable drawing guide is now On by default
|
||||
|
||||
1.9.4
|
||||
|
||||
- feat: renaming using the name_manager take care of changing layer target values in GP modifiers
|
||||
|
||||
1.9.3
|
||||
|
||||
- feat: Add an update button at bottom of preferences if addon is a cloned repository
|
||||
|
||||
1.9.2
|
||||
|
||||
- feat: Palette linker has a name exclusion list in preferences
|
||||
- Avoid linking some material that are prefered local to file
|
||||
- Default item in list : `line`
|
||||
|
||||
1.9.1
|
||||
|
||||
- fix: add error handling on palette linker when blend_path isn't valid anymore
|
||||
- added: file checker entry, Disable all GP object `use lights` (True by default in addon pref `checklist`)
|
||||
- added: WIP of Batch reproject all on cursor.
|
||||
- No UI. In search menu `Flat Reproject Selected On cursor` (idname `gp.batch_flat_reproject`)
|
||||
|
||||
1.9.0
|
||||
|
||||
- feat: New shortcuts:
|
||||
- `F2` in Paint and Edit to rename active layer
|
||||
- `Insert` add a new layer (same as Krita)
|
||||
- `Shift + Insert` add a new layer and immediately pop-up a rename box
|
||||
- `page up / page down` change active layer up/down with a temporary fade (settings in addon prefs)
|
||||
- fix: error when tweaking `gp.duplicate_send_to_layer` shortcut
|
||||
|
||||
1.8.1
|
||||
|
||||
- fix: Gp clipboard paste `Paste layers` don't skip empty frames anymore
|
||||
|
||||
1.8.0
|
||||
|
||||
- feat: palette linker (beta), with pop-up from material stack dropdown
|
||||
- feat: palette name fuzzy match
|
||||
- code: add an open addon preference ops
|
||||
|
||||
1.7.8
|
||||
|
||||
- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)
|
||||
|
||||
1.7.7
|
||||
|
||||
- feat: add copy path to `check link` ops with multiple path representation choices
|
||||
|
||||
1.7.6
|
||||
|
||||
- ui: expose (almost) all keymap added by the addon to allow user for customize/disable as needed
|
||||
- ui: changed some names
|
||||
|
||||
1.7.5
|
||||
|
||||
- feat: Select/set by color and by prefix now works on every displayed dopesheet layer (and react correctly to filters)
|
||||
- ui: exposed user prefs `Channel Group Color` prop in dopesheet > sidebar > View > Display panel
|
||||
- add undo step for `W`'s select layer from closest stroke
|
||||
|
||||
1.7.4
|
||||
|
||||
- added: Pick layer from closest stroke in paint mode using quick press on `W` for stroke (and `alt+W` for fills)
|
||||
- fix: copy-paste keymap error on background rendering
|
||||
|
||||
1.7.3
|
||||
|
||||
- added: show/hide gp modifiers if they are showed in render (in subpanel `Animation Manager`)
|
||||
|
||||
1.7.2
|
||||
|
||||
- added: `Object visibility conflict` in file check
|
||||
- print in console when object have render activated but not viewport (& vice-versa)
|
||||
- Standalone ops "List Object Visibility Conflicts" (`gp.list_object_visibility`)
|
||||
- added: `GP Modifier visibility conflict` in file check.
|
||||
- print in console when gp modifiers have render activated but not viewport (& vice-versa)
|
||||
- Standalone ops "List GP Modifiers Visibility Conflicts" (`gp.list_modifier_visibility`)
|
||||
- code: show_message_box utils can now receive operator in sublist (if 3 element)
|
||||
|
||||
1.7.1
|
||||
|
||||
- feat: Improved `Create Empty Frames` operator with mutliple filters to choose source layers
|
||||
|
||||
1.7.0
|
||||
|
||||
- removed: Obsolete operators and panels
|
||||
- Remove "line closer" panel as it's been a native tool for a while in 2.9x (stop register of `GP_guided_colorize > OP_line_closer`)
|
||||
- Remove "Render" subpanel, obsolete and not adapted to production
|
||||
- ui: clean and refactor
|
||||
- Gp clipboard panel and aimation Manager subpanel layout to aligned columns (gain space)
|
||||
- add `GP` on each panel name for wuick eye search
|
||||
- follow cursor now in animation manager subpanel (should be in an extra menu or removed in the end)
|
||||
|
||||
1.6.9
|
||||
|
||||
- change: material picker (`S` and `Alt+S`) quick trigger, change is only triggered if key is pressed less than 200ms
|
||||
- this is made to let other operator use functionality on long press using `S`
|
||||
- feat: material picker shortcut has now an enum choice to filter targeted stroke (fill, stroke, all)
|
||||
- by default `S` is still fill only
|
||||
- but `Alt+S` is now stroke only instead of all
|
||||
|
||||
1.6.8
|
||||
|
||||
- fix: reproject GP repeating projection on same frames
|
||||
- fix: batch reproject all frame not poping dialog menu
|
||||
|
||||
1.6.7
|
||||
|
||||
- fix: error with operator `OP_key_duplicate_send` poll flooding console
|
||||
|
||||
1.6.6
|
||||
|
||||
- fix: inverted scene resolution from project environnement
|
||||
|
||||
1.6.5
|
||||
|
||||
- feat: check canvas alignement of the Gp object compare to chosen draw axis
|
||||
|
||||
1.6.4
|
||||
|
||||
- fix: disable multi-selection for layer naming manager
|
||||
- the dopesheet selection sometimes still consider layer as selected
|
||||
|
||||
1.6.3
|
||||
|
||||
<!-- - abort :: add checkbox to prevent from loading project environnement namespace -->
|
||||
- add buttons to load manually environnement namespace
|
||||
|
||||
1.6.2
|
||||
|
||||
- disable keymap register for breakdowner on background
|
||||
|
||||
1.6.1
|
||||
|
||||
- removed: Auto Updater that was failing since 2.93
|
||||
- prefs: add a checkbox to disable the "load base palette button" in UI.
|
||||
|
||||
1.6.0
|
||||
|
||||
- feat: Namespace upgrade
|
||||
- support pseudo group naming
|
||||
- add group and indent button
|
||||
- feat: Fill tool shortcut for material color picker (from closest visible stroke)
|
||||
- `S` : get material closest *fill* stroke
|
||||
- `Àlt+S` : get closest stroke (fill or stroke)
|
||||
|
||||
1.5.8
|
||||
|
||||
- feat: Namespace improvement:
|
||||
- new suffixes list that generate suffix buttons
|
||||
- dynamic layer name field that show active (msgbus)
|
||||
- possible to disable the panel with an option
|
||||
|
||||
1.5.7
|
||||
|
||||
- feat: check list, specify in addon pref if you prefer a dry run (check without set)
|
||||
- feat: check file can change the lock object mode state (do nothing by default)
|
||||
|
||||
1.5.6
|
||||
|
||||
- feat: layer drop-down menu include an operator for batch find/replace GP layers (idname: `gp.rename_gp_layers`)
|
||||
|
||||
1.5.5
|
||||
|
||||
- feat: check file: add check for filepath mapping (if all relative or all absolute)
|
||||
- change: check file: disable resolution set by default
|
||||
|
||||
1.5.4
|
||||
|
||||
- feat: Layer manager
|
||||
- select/set layer prefix
|
||||
- select/set layer color
|
||||
- code: refactor name builder function
|
||||
|
||||
1.5.3
|
||||
|
||||
- feat: layer aquick-prefix for layer using pref separator
|
||||
- list editable in addon pref
|
||||
- add interface button above layers
|
||||
- code: added environnement variable for prefix and separator:
|
||||
- `PREFIXES` : list of prefix (comma separated uppercase letters between 1 and 6 character, ex: 'AN,SP,L')
|
||||
- `SEPARATOR` : Separator character to determine prefixes, default is '_' (should not be a special regex character)
|
||||
- UI: add addon prefs namespace ui-box in project settings
|
||||
|
||||
|
||||
1.5.2
|
||||
|
||||
- add environnement variables to set addon preferences project settings at register through `os.getenv('KEY')`:
|
||||
- `RENDER_WIDTH` : resolution x
|
||||
- `RENDER_HEIGHT` : resolution y
|
||||
- `FPS` : project frame rate
|
||||
- `PALETTES` : path to the blends (or json) containing materials palettes
|
||||
- `BRUSHES` : path to the blend containing brushes to load
|
||||
|
||||
1.5.1
|
||||
|
||||
- fix: eraser brush
|
||||
- change: check file has custom check list in addon prefs
|
||||
|
||||
1.5.0
|
||||
|
||||
- feat: Eraser Brush Tool (Need to be enable in the preferences)
|
||||
|
||||
1.4.3
|
||||
|
||||
- feat: load brushes from blend
|
||||
- ui: add load brushes within tool brush dropdown panel and in the top bar in drawmode
|
||||
- pref: Set project brushes folder in addon preferences
|
||||
|
||||
1.4.2
|
||||
|
||||
- feat: new material cleaner in GP layer menu with 3 options
|
||||
- clean material duplication (with sub option to not clear if color settings are different)
|
||||
- fuse material slots that have the same materials
|
||||
- remove empty slots
|
||||
|
||||
1.4.1
|
||||
|
||||
- fix: custom passepartout size limit when dezooming in camera
|
||||
|
||||
1.4.0
|
||||
|
||||
- feat: Passepartout displayed in main cam (permanent draw handler)
|
||||
- UI: add Straight stroke operator button in Toolbox if GP tools official addon is on
|
||||
- UI: placed Toolbox Playblast in a subpanel
|
||||
- UI: removed Onion skin and Autolock layer checkbox (not often used)
|
||||
- UI: sent rarely `cursor follow` to bottom of the panel
|
||||
|
||||
1.3.3
|
||||
|
||||
- feat: show main cam frame when in draw_cam
|
||||
|
||||
1.3.2
|
||||
|
||||
- change: disable manip cam name drawing
|
||||
- code: add initial support for main cam frame draw within camera view
|
||||
|
||||
1.3.1
|
||||
|
||||
- fix: native refresh error that rarely happen that doesn't completely refresh the scene on keyframe jump.
|
||||
- Use a double frame change to ensure refresh.
|
||||
|
||||
1.3.0
|
||||
|
||||
- feat: new duplicate send to layer feaure - `ctrl + shift + D` in GP dopesheet
|
||||
|
||||
1.2.2
|
||||
|
||||
- fix: realign anim return error
|
||||
|
||||
1.2.1
|
||||
|
||||
- fix: Breakdowner initial error check
|
||||
|
||||
1.2.0
|
||||
|
||||
- feat: New depth move operator that handle both perspective and orthographic cam
|
||||
- feat: Realign, added drawing plane checkbox to autoset to Front after realign
|
||||
- UI: Reorganised realign panel
|
||||
- UI: Switched part of the sidebar items to columns intead of basic layout to gain space
|
||||
- doc: Added changelog file (moved list from readme)
|
||||
- doc: relative link to changelog and FR_readme in main readme
|
||||
|
||||
1.1.0
|
||||
|
||||
- Important change : Remap relative is now disabled by default in addon preferences
|
||||
- feat: Add realign operator in sidebar with reproject as true by default
|
||||
- UI: Batch reproject all frames is now in menus. Same places as native reproject
|
||||
|
||||
1.0.9
|
||||
|
||||
- feat: Reproject all frames operator
|
||||
|
||||
1.0.8
|
||||
|
||||
- feat: Keyframe jump filter added in UI to change general behavior. Keymap own jump filter can override this new global settings if specified
|
||||
|
||||
1.0.7
|
||||
|
||||
- feat: Keyframe jump filter by type. User can now choose if the shortcut should jump on a specific keyframe type (All by default)
|
||||
|
||||
1.0.5
|
||||
|
||||
- GP copy-paste : Pasted stroke are now selected (allow to use it to quickly rip/split strokes with cut/paste on the same layer)
|
||||
|
||||
1.0.4
|
||||
|
||||
- UI: Better cam ref exposition in Toolbox panel
|
||||
- Access to opacity
|
||||
- merge activation bool with source type icon
|
||||
- feat: Added a clear active frame operator (`gp.clear_active_frame` to add manually in keymaps)
|
||||
|
||||
1.0.3
|
||||
|
||||
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
|
||||
|
||||
1.0.2
|
||||
|
||||
- pref: Added option to disable always remap relative on save in addon-preference
|
||||
|
||||
1.0.1
|
||||
|
||||
- fix: copy paste problems
|
||||
- Get points uv_properties (used for brushed points)
|
||||
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
|
||||
|
||||
1.0.0
|
||||
|
||||
- Compatible with official grease pencil tools
|
||||
- removed box deform and rotate canvas that existed in other
|
||||
|
||||
0.9.3
|
||||
|
||||
- feat: keyframe jump keys are now auto-binded
|
||||
- UI: added keyframe jump customisation in addon pref
|
||||
- code: split keyframe jump in a separate file with his new key updater
|
||||
|
||||
0.9.2
|
||||
|
||||
- doc: Correct download link (important, bugged the addon install) + update
|
||||
- code: added tracker url
|
||||
- updater: remove updater temp file, reset minimum version, turn off verbose mode
|
||||
|
||||
0.9.1
|
||||
|
||||
- Public release
|
||||
- prefs: added fps as part of project settings
|
||||
- check file use pref fps value (previously used harcoded 24fps value)
|
||||
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
|
||||
- update: apply changes in integrated copy-paste from the last version of standalone addon
|
||||
- doc: Added fully-detailed french readme
|
||||
|
||||
|
||||
0.8.0
|
||||
|
||||
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
|
||||
- stripped associated properties from properties.py and passed as wm props.
|
||||
|
||||
0.7.2
|
||||
|
||||
- fix: Palette importer bug
|
||||
|
||||
0.7.0
|
||||
|
||||
- feat: auto create empty frame on color layer
|
||||
|
||||
0.6.3
|
||||
|
||||
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
|
||||
|
||||
0.6.2
|
||||
|
||||
- feat: colorisation, Option to change stop lines length
|
||||
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
|
||||
- Minor refactor for submodule register
|
||||
|
||||
0.6.1
|
||||
|
||||
- feat: render objects grouped, one anim render with all ticked object using manual output name
|
||||
|
||||
0.6.0
|
||||
|
||||
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
|
||||
|
||||
0.5.9
|
||||
|
||||
- feat: render exporter
|
||||
- Render a selection of GP object isolated from the rest
|
||||
- added exclusions names for GP object listing
|
||||
- setup settings and output according to a name
|
||||
- open render folder
|
||||
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
|
||||
- check file: set scene resolution to settings in prefs (default 2048x1080)
|
||||
|
||||
0.5.8
|
||||
|
||||
- feat: GP material append on active object from single blend file
|
||||
|
||||
0.5.7
|
||||
|
||||
- Added warning message for cursor snapping
|
||||
|
||||
0.5.6
|
||||
|
||||
- check file: added check for placement an projection mode for Gpencil.
|
||||
- add a slider to change edit_lines_opacity globally for all GP data at once
|
||||
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
|
||||
|
||||
0.5.4
|
||||
|
||||
- feat: anim manager in his own GP_toolbox submenu:
|
||||
- button to list disabled anim (allow to quickly check state of the scene)
|
||||
- disable/enable all fcurve in for GP object or other object separately to paint
|
||||
- shift clic to target selection only
|
||||
- check file: added disabled fcurved counter alert with detail in console
|
||||
|
||||
0.5.3
|
||||
|
||||
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
|
||||
- check file option: change select active tool (choice added in addon preferences)
|
||||
|
||||
0.5.2
|
||||
|
||||
- Revert back obj_cam operator for following object (native lock view follow only translation)
|
||||
- Changed method for canvas rotation to more robust rotate axis.
|
||||
- Add operators on link checker to open containing folder/file of link
|
||||
- Refactor: file checkers in their own file
|
||||
|
||||
0.5.1
|
||||
|
||||
- fix: error when empty material slot on GP object.
|
||||
- fix: cursor snap on GP canvas when GP is parented
|
||||
- change: Deleted obj cam (and related set view) operator
|
||||
- change: blacker note background for playblast (stamp_background)
|
||||
- feat: Always playblast from main camera (if in draw_cam)
|
||||
- feat: Handler added to Remap relative on save (pre)
|
||||
- ops: Check for broken links with porposition to find missing files
|
||||
- ops: Added basic hardcoded file checker
|
||||
- Lock main cam
|
||||
- set scene percentage at 100
|
||||
- set show slider and sync range
|
||||
- set fps to 24
|
||||
|
||||
0.4.6
|
||||
|
||||
- feat: basic Palette manager with base material check and warning
|
||||
|
||||
0.4.5
|
||||
|
||||
- open blender config folder from addon preference
|
||||
- fix: obj cam parent on selected object
|
||||
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
|
||||
- freeview : bug when rotating free viewfrom cardianl views
|
||||
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
|
||||
|
||||
|
||||
0.4.4
|
||||
|
||||
- feat: added cursor follow handlers and UI toggle
|
||||
|
||||
0.4.3
|
||||
|
||||
- change playblast out to 'images' and add playblast as name prefix
|
||||
|
||||
0.4.2
|
||||
|
||||
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
|
||||
- fix: canvas rotate works with parented camera !
|
||||
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
|
||||
|
||||
0.4.1
|
||||
|
||||
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
|
||||
|
||||
0.4.0
|
||||
|
||||
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
|
||||
|
||||
0.3.8
|
||||
|
||||
- UI: expose onion skin in interface
|
||||
- UI: expose autolock in interface
|
||||
- UI : putted tint layers in a submenu
|
||||
- code: refactor, pushed most of class register in their owner file
|
||||
- tool: tool to rename current or all grease pencil datablock with different name than container object
|
||||
|
||||
0.3.7
|
||||
|
||||
- UI: new interface with tabs for addon preferences
|
||||
- UI: possible to disable color panel from preference (might be deleted if unusable)
|
||||
- docs: change readme changelog format and correct doc
|
||||
|
||||
0.3.6
|
||||
|
||||
- UI: Stoplines : add a button for quickly set stoplines visibility.
|
||||
|
||||
0.3.5
|
||||
|
||||
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
|
||||
- Fix: Enter key added to valid object-breakdown modal.
|
||||
|
||||
0.3.3
|
||||
|
||||
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
|
||||
|
||||
0.3.3
|
||||
|
||||
- version 1 beta of gmic colorize
|
||||
- variant of `screen.gp_keyframe_jump` through keymap seetings
|
||||
|
||||
0.3.0
|
||||
|
||||
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
|
||||
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
|
||||
|
||||
0.2.3
|
||||
|
||||
- add operator to `screen.gp_keyframe_jump`
|
||||
- add shortcut to rotate canvas
|
||||
- fix duplicate class
|
||||
|
||||
0.2.2
|
||||
|
||||
- separated props resolution_percentage parameter
|
||||
- playblast options for launching folder and opening folder
|
||||
|
||||
0.2.1
|
||||
|
||||
- playblast feature
|
||||
- Button to go zoom 100% or fit screen
|
||||
- display scene resolution with res indicator
|
||||
- Fix reference panel : works with video and display in a box layout.
|
||||
- close pseudo-color panel by default (plan to move it to Gpencil tab)
|
||||
|
||||
0.2.0
|
||||
|
||||
- UI: Toggle camera background images from Toolbox panel
|
||||
- UI: quick access to passepartout
|
||||
- Feature: option to use namespace for pseudo color
|
||||
|
||||
0.1.5
|
||||
|
||||
- added CGC-auto-updater
|
||||
|
||||
0.1.3
|
||||
|
||||
- flip cam x
|
||||
- inital stage of overlay toggle (need pref/multiple pref)
|
||||
|
||||
0.1.2
|
||||
|
||||
- subpanel of GP data (instead of direct append)
|
||||
- initial commit with GP pseudo color
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
import bpy
|
||||
|
||||
# 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,199 +1,33 @@
|
|||
## 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 targeted layers\
|
||||
\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 layer above\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 == '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')
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
gp = obj.data
|
||||
gpl = gp.layers
|
||||
gpl = obj.data.layers
|
||||
gpl.active_index
|
||||
|
||||
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'}
|
||||
## 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'}
|
||||
|
||||
frame_id_list = []
|
||||
for l in tgt_layers:
|
||||
for i, l in enumerate(gpl):
|
||||
# don't list layer below
|
||||
if i <= gpl.active_index:
|
||||
continue
|
||||
# print(l.info, "index:", i)
|
||||
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))
|
||||
|
@ -206,17 +40,17 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
if num in current_frames:
|
||||
continue
|
||||
#Create empty frame
|
||||
gpl.active.frames.new(num)
|
||||
gpl.active.frames.new(num, active=False)
|
||||
fct += 1
|
||||
|
||||
gpl.update()
|
||||
if fct:
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
|
||||
else:
|
||||
self.report({'WARNING'}, f"No frames to create !")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(GP_OT_create_empty_frames)
|
||||
|
||||
|
|
|
@ -5,14 +5,11 @@ from ..utils import (location_to_region,
|
|||
vector_length,
|
||||
draw_gp_stroke,
|
||||
extrapolate_points_by_length,
|
||||
simple_draw_gp_stroke,
|
||||
is_hidden,
|
||||
is_locked)
|
||||
simple_draw_gp_stroke)
|
||||
|
||||
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
|
||||
|
@ -56,14 +53,11 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter = defaultdict(list)
|
||||
plist = []
|
||||
matrix = ob.matrix_world
|
||||
for s in f.drawing.strokes: #add first and last
|
||||
for s in f.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 ?
|
||||
|
@ -76,7 +70,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.position), location_to_region(matrix @ op.position))
|
||||
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
|
||||
# print('gap2d: ', gap2d)
|
||||
if gap2d > tol:
|
||||
continue
|
||||
|
@ -108,16 +102,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter[p].append(op)
|
||||
|
||||
|
||||
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
|
||||
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
|
||||
ctl += 1
|
||||
|
||||
print(f'{ctl} line created')
|
||||
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.active_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 != 'GREASEPENCIL':
|
||||
if ob.type != 'GPENCIL':
|
||||
continue
|
||||
|
||||
mat_id = get_closeline_mat(ob)# get a the closing material
|
||||
|
@ -134,7 +128,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.current_frame()
|
||||
f = l.active_frame
|
||||
## create gap stroke
|
||||
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
|
||||
|
||||
|
@ -149,9 +143,9 @@ def is_deviating_by(s, deviation=0.75):
|
|||
pb = s.points[-2]
|
||||
pc = s.points[-3]
|
||||
|
||||
a = location_to_region(pa.position)
|
||||
b = location_to_region(pb.position)
|
||||
c = location_to_region(pc.position)
|
||||
a = location_to_region(pa.co)
|
||||
b = location_to_region(pb.co)
|
||||
c = location_to_region(pc.co)
|
||||
|
||||
#cb-> compare angle with ba->
|
||||
angle = (b-c).angle(a-b)
|
||||
|
@ -164,16 +158,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]].position)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
|
||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
|
||||
# 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]].position# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].position# ob.matrix_world @
|
||||
a = s.points[id_pair[0]].co# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].co# 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)
|
||||
|
||||
|
@ -194,15 +188,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].position
|
||||
a = s.points[-2].co
|
||||
bp = s.points[-1]#end-point
|
||||
b = bp.position
|
||||
b = bp.co
|
||||
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.position = newb
|
||||
bp.co = newb
|
||||
ct += 1
|
||||
|
||||
return ct
|
||||
|
@ -216,14 +210,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.current_frame()
|
||||
# frame = ob.data.layers.active.active_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.drawing.strokes as we loop in !
|
||||
for s in list(frame.drawing.strokes):
|
||||
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
|
||||
for s in list(frame.strokes):
|
||||
if s.material_index == mat_id:#is a closeline
|
||||
continue
|
||||
if len(s.points) < 2:#not enough point to evaluate
|
||||
|
@ -247,7 +241,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 == 'GREASEPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
|
||||
# mode : bpy.props.StringProperty(
|
||||
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
|
||||
|
@ -279,18 +273,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 is_hidden(l)]
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
continue
|
||||
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
|
||||
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No strokes extended... see console"
|
||||
|
@ -312,7 +306,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 == 'GREASEPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
|
||||
layer_tgt : bpy.props.EnumProperty(
|
||||
name="Extend layers", description="Choose which layer to target",
|
||||
|
@ -340,18 +334,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 is_hidden(l)]
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
continue
|
||||
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
|
||||
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No extension modified... see console"
|
||||
|
@ -373,15 +367,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 == 'GREASEPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ct = 0
|
||||
ob = context.object
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
|
||||
for l in lays:
|
||||
if not l.current_frame():continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
if not l.active_frame:continue
|
||||
for s in l.active_frame.strokes:
|
||||
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
|
||||
ct+=1
|
||||
|
||||
|
@ -403,7 +397,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 == 'GREASEPENCIL'
|
||||
return (context.object is not None)# and context.object.type == 'GPENCIL'
|
||||
|
||||
## draw stuff inside the header (place before main label)
|
||||
# def draw_header(self, context):
|
||||
|
@ -420,7 +414,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 == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
|
||||
if context.object.type == 'GPENCIL' 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')
|
||||
|
|
|
@ -211,23 +211,16 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
|||
## cursors
|
||||
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
|
||||
## start checks
|
||||
if context.area.type != 'VIEW_3D':
|
||||
self.report({'WARNING'}, 'View3D not found, cannot run operator')
|
||||
return {'CANCELLED'}
|
||||
|
||||
message = None
|
||||
if context.area.type != 'VIEW_3D':message = 'View3D not found, cannot run operator'
|
||||
obj = bpy.context.object# better use self.context
|
||||
if not obj:
|
||||
self.report({'WARNING'}, 'No active object')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not obj:message = 'no active object'
|
||||
anim_data = obj.animation_data
|
||||
if not anim_data:
|
||||
self.report({'WARNING'}, f'No animation data on obj: {obj.name}')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not anim_data:message = f'no animation data on obj: {obj.name}'
|
||||
action = anim_data.action
|
||||
if not action:
|
||||
self.report({'WARNING'}, f'No action on animation data of obj: {obj.name}')
|
||||
if not action:message = f'no action on animation data of obj: {obj.name}'
|
||||
if message:
|
||||
self.report({'WARNING'}, message)# ERROR
|
||||
return {'CANCELLED'}
|
||||
|
||||
## initiate variable to use
|
||||
|
@ -302,10 +295,8 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
|||
|
||||
### --- KEYMAP ---
|
||||
|
||||
addon_keymaps = []
|
||||
breakdowner_addon_keymaps = []
|
||||
def register_keymaps():
|
||||
if bpy.app.background:
|
||||
return
|
||||
# pref = get_addon_prefs()
|
||||
# if not pref.breakdowner_use_shortcut:
|
||||
# return
|
||||
|
@ -322,23 +313,24 @@ 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)
|
||||
addon_keymaps.append((km, kmi))
|
||||
breakdowner_addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
if bpy.app.background:
|
||||
return
|
||||
for km, kmi in addon_keymaps:
|
||||
for km, kmi in breakdowner_addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
|
||||
addon_keymaps.clear()
|
||||
breakdowner_addon_keymaps.clear()
|
||||
# del breakdowner_addon_keymaps[:]
|
||||
|
||||
### --- 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)
|
||||
|
||||
|
|
100
OP_brushes.py
100
OP_brushes.py
|
@ -1,100 +0,0 @@
|
|||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from pathlib import Path
|
||||
from .utils import get_addon_prefs
|
||||
|
||||
|
||||
def get_brushes(blend_fp):
|
||||
'''Get all brush from passed blend that aren't already in there (can take Path object)'''
|
||||
cur_brushes = [b.name for b in bpy.data.brushes]
|
||||
with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
|
||||
# load brushes if not already there
|
||||
data_to.brushes = [b for b in data_from.brushes if not b in cur_brushes]
|
||||
|
||||
## force fake user for appended the brushes
|
||||
for b in data_to.brushes:
|
||||
print(f'Append Brush: {b.name}')
|
||||
b.use_fake_user = True
|
||||
|
||||
return len(data_to.brushes)
|
||||
|
||||
|
||||
class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "gp.load_brushes"
|
||||
bl_label = "Load Brushes"
|
||||
bl_description = "Load all brushes from chosen blend file in current if brushes aren't already there\nIf a replacement is needed, delete the previous brush before"
|
||||
#bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default='*.blend', options={'HIDDEN'} )
|
||||
|
||||
filepath : bpy.props.StringProperty(
|
||||
name="File Path",
|
||||
description="File path used for import",
|
||||
maxlen= 1024)
|
||||
|
||||
def execute(self, context):
|
||||
print(f'Appending brushes from file : {self.filepath}')
|
||||
bct = get_brushes(self.filepath)
|
||||
if bct:
|
||||
self.report({'INFO'}, f'{bct} brushes appended')
|
||||
else:
|
||||
self.report({'WARNING'}, 'Brushes are already there (if need to re-import, delete first)')
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_brush_set(bpy.types.Operator):
|
||||
bl_idname = "gp.brush_set"
|
||||
bl_label = "Set Brush"
|
||||
bl_description = "Set Gpencil brush"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
brush_name : bpy.props.StringProperty(name='Brush', description='Name of the brush to use')
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
brush = bpy.data.brushes.get(self.brush_name)
|
||||
if not brush:
|
||||
self.report({'ERROR'}, f'Brush "{self.brush_name}" not found')
|
||||
return {"CANCELLED"}
|
||||
context.scene.tool_settings.gpencil_paint.brush = brush
|
||||
return {"FINISHED"}
|
||||
|
||||
### -- MENU ENTRY --
|
||||
|
||||
def load_brush_ui(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
|
||||
|
||||
def load_brush_top_bar_ui(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
|
||||
|
||||
classes = (
|
||||
GPTB_OT_load_brushes,
|
||||
GPTB_OT_brush_set,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui)
|
||||
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
444
OP_copy_paste.py
444
OP_copy_paste.py
|
@ -1,15 +1,38 @@
|
|||
## 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
|
||||
# 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" }
|
||||
|
||||
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 .utils import is_locked, is_hidden
|
||||
# from pprint import pprint
|
||||
|
||||
def convertAttr(Attr):
|
||||
'''Convert given value to a Json serializable format'''
|
||||
|
@ -22,109 +45,100 @@ def convertAttr(Attr):
|
|||
else:
|
||||
return(Attr)
|
||||
|
||||
def getMatrix(layer) :
|
||||
def getMatrix (layer) :
|
||||
matrix = mathutils.Matrix.Identity(4)
|
||||
|
||||
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 = parent.matrix_world @ layer.matrix_parent_inverse
|
||||
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
|
||||
else :
|
||||
matrix = layer.parent.matrix_world @ layer.matrix_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,
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
|
||||
def dump_gp_point(p, l, obj):
|
||||
'''add properties of a given points to a dic and return it'''
|
||||
point_dict = {}
|
||||
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
|
||||
pdic = {}
|
||||
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
|
||||
#for att in point_attr_list:
|
||||
# point_dict[att] = convertAttr(getattr(p, att))
|
||||
# pdic[att] = convertAttr(getattr(p, att))
|
||||
if l.parent:
|
||||
mat = getMatrix(l)
|
||||
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
|
||||
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
|
||||
else:
|
||||
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'))
|
||||
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'))
|
||||
|
||||
## get vertex color (long...)
|
||||
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
||||
point_dict['vertex_color'] = convertAttr(p.vertex_color)
|
||||
if p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
||||
pdic['vertex_color'] = convertAttr(p.vertex_color)
|
||||
|
||||
if rotation and p.rotation != 0.0:
|
||||
point_dict['rotation'] = convertAttr(p.rotation)
|
||||
## 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)
|
||||
|
||||
## No time infos
|
||||
# if delta_time and p.delta_time != 0.0:
|
||||
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
|
||||
return pdic
|
||||
|
||||
return point_dict
|
||||
|
||||
def dump_gp_stroke_range(s, sid, l, obj,
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||
def dump_gp_stroke_range(s, sid, l, obj):
|
||||
'''Get a grease pencil stroke and return a dic with attribute
|
||||
(points attribute being a dic of dics to store points and their attributes)
|
||||
'''
|
||||
|
||||
stroke_dict = {}
|
||||
# stroke_attr_list = ('line_width',)
|
||||
# for att in stroke_attr_list:
|
||||
# stroke_dict[att] = getattr(s, att)
|
||||
sdic = {}
|
||||
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
|
||||
for att in stroke_attr_list:
|
||||
sdic[att] = getattr(s, att)
|
||||
|
||||
## Dump following these value only if they are non default
|
||||
if s.material_index != 0:
|
||||
stroke_dict['material_index'] = s.material_index
|
||||
sdic['material_index'] = s.material_index
|
||||
|
||||
if s.cyclic:
|
||||
stroke_dict['cyclic'] = s.cyclic
|
||||
if getattr(s, 'draw_cyclic', None):# pre-2.92
|
||||
sdic['draw_cyclic'] = s.draw_cyclic
|
||||
|
||||
if s.softness != 0.0:
|
||||
stroke_dict['softness'] = s.softness
|
||||
if getattr(s, 'use_cyclic', None):# from 2.92
|
||||
sdic['use_cyclic'] = s.use_cyclic
|
||||
|
||||
if s.aspect_ratio != 1.0:
|
||||
stroke_dict['aspect_ratio'] = s.aspect_ratio
|
||||
if s.uv_scale != 1.0:
|
||||
sdic['uv_scale'] = s.uv_scale
|
||||
|
||||
if s.start_cap != 0:
|
||||
stroke_dict['start_cap'] = s.start_cap
|
||||
if s.uv_rotation != 0.0:
|
||||
sdic['uv_rotation'] = s.uv_rotation
|
||||
|
||||
if s.end_cap != 0:
|
||||
stroke_dict['end_cap'] = s.end_cap
|
||||
if s.hardness != 1.0:
|
||||
sdic['hardness'] = s.hardness
|
||||
|
||||
if fill_color and s.fill_color[:] != (0,0,0,0):
|
||||
stroke_dict['fill_color'] = convertAttr(s.fill_color)
|
||||
if s.uv_translation != Vector((0.0, 0.0)):
|
||||
sdic['uv_translation'] = convertAttr(s.uv_translation)
|
||||
|
||||
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
|
||||
if s.vertex_color_fill[:] != (0,0,0,0):
|
||||
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
|
||||
|
||||
points = []
|
||||
if sid is None: # no ids, just full points...
|
||||
if sid is None:#no ids, just full points...
|
||||
for p in s.points:
|
||||
points.append(dump_gp_point(p, l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||
points.append(dump_gp_point(p,l,obj))
|
||||
else:
|
||||
for pid in sid:
|
||||
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
|
||||
points.append(dump_gp_point(s.points[pid],l,obj))
|
||||
sdic['points'] = points
|
||||
return sdic
|
||||
|
||||
|
||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
||||
|
||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow filter)
|
||||
'''
|
||||
copy all visibles selected strokes on active frame
|
||||
layers can be None, a single layer object or list of layer object as filter
|
||||
|
@ -139,50 +153,46 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|||
# if not color:#get active color name
|
||||
# color = gp.palettes.active.colors.active.name
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
|
||||
#by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]#[]
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
#if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
||||
stroke_list = [] # one stroke list for all layers.
|
||||
stroke_list = []#one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.current_frame()
|
||||
f = l.active_frame
|
||||
|
||||
if f: # active frame can be None
|
||||
if f:#active frame can be None
|
||||
if not copy:
|
||||
staylist = [] # init part of strokes that must survive on this layer
|
||||
staylist = []#init part of strokes that must survive on this layer
|
||||
|
||||
rm_list = [] # init strokes that must be removed from this layer
|
||||
for s_index, stroke in enumerate(f.drawing.strokes):
|
||||
if stroke.select:
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
# separate in multiple stroke if parts of the strokes a selected.
|
||||
sel = [i for i, p in enumerate(stroke.points) if p.select]
|
||||
substrokes = [] # list of list containing isolated selection
|
||||
|
||||
# continuity stroke have same substract result between point index and enumerator
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
|
||||
sel = [i for i, p in enumerate(s.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
|
||||
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(stroke, ss, l, obj))
|
||||
if len(ss) > 1:#avoid copy isolated points
|
||||
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
||||
|
||||
# Cutting operation
|
||||
#Cutting operation
|
||||
if not copy:
|
||||
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)
|
||||
maxindex = len(s.points)-1
|
||||
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier
|
||||
f.strokes.remove(s)
|
||||
else:
|
||||
neg = [i for i, p in enumerate(stroke.points) if not p.select]
|
||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
||||
|
||||
staying = []
|
||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||
group = list(map(itemgetter(1), g))
|
||||
# extend group to avoid gap when cut, a bit dirty
|
||||
#extend group to avoid gap when cut, a bit dirty
|
||||
if group[0] > 0:
|
||||
group.insert(0,group[0]-1)
|
||||
if group[-1] < maxindex:
|
||||
|
@ -191,30 +201,37 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|||
|
||||
for ns in staying:
|
||||
if len(ns) > 1:
|
||||
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
|
||||
# make a negative list containing all last index
|
||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
||||
#make a negative list containing all last index
|
||||
|
||||
if rm_list:
|
||||
f.drawing.remove_strokes(indices=rm_list)
|
||||
|
||||
'''#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 not copy:
|
||||
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
|
||||
|
||||
# delete all selected strokes...
|
||||
if selected_ids:
|
||||
f.drawing.remove_strokes(indices=selected_ids)
|
||||
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
f.strokes.remove(s)
|
||||
# ...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.drawing.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.strokes):
|
||||
l.frames.remove(f)
|
||||
|
||||
|
||||
|
||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
#print(stroke_list)
|
||||
return stroke_list
|
||||
|
@ -236,7 +253,7 @@ def copy_all_strokes(layers=None):
|
|||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -244,12 +261,12 @@ def copy_all_strokes(layers=None):
|
|||
stroke_list = []# one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.current_frame()
|
||||
f = l.active_frame
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.drawing.strokes:
|
||||
for s in f.strokes:
|
||||
## full stroke version
|
||||
# if s.select:
|
||||
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
|
||||
|
@ -259,11 +276,11 @@ def copy_all_strokes(layers=None):
|
|||
return stroke_list
|
||||
"""
|
||||
|
||||
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):
|
||||
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
|
||||
'''
|
||||
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
|
||||
|
@ -276,7 +293,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 is_hidden(l) and not is_locked(l)] # include locked ?
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -284,69 +301,68 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
|||
stroke_list = []
|
||||
|
||||
for l in layers:
|
||||
f = l.current_frame()
|
||||
f = l.active_frame
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.drawing.strokes:
|
||||
for s in f.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,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
|
||||
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj) )
|
||||
|
||||
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
#print(stroke_list)
|
||||
return stroke_list
|
||||
|
||||
def add_stroke(s, frame, layer, obj, select=False):
|
||||
def add_stroke(s, frame, layer, obj):
|
||||
'''add stroke on a given frame, (layer is for parentage setting)'''
|
||||
# print(3*'-',s)
|
||||
pts_to_add = len(s['points'])
|
||||
frame.drawing.add_strokes([pts_to_add])
|
||||
ns = frame.strokes.new()
|
||||
|
||||
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()
|
||||
|
||||
if layer.parent:
|
||||
layer_matrix = getMatrix(layer).inverted()
|
||||
transform_matrix = ob_mat_inv @ layer_matrix
|
||||
else:
|
||||
transform_matrix = ob_mat_inv
|
||||
## patch pressure 1
|
||||
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
|
||||
|
||||
## Set points attributes
|
||||
if layer.is_parented:
|
||||
mat = getMatrix(layer).inverted()
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'position':
|
||||
if k == 'co':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
|
||||
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
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):
|
||||
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
|
||||
'''
|
||||
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 set selected
|
||||
if stroke list is empty create an empty frame at current frame
|
||||
'''
|
||||
scene = bpy.context.scene
|
||||
obj = bpy.context.object
|
||||
|
@ -359,8 +375,8 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
|
||||
fnum = scene.frame_current
|
||||
target_frame = False
|
||||
act = layer.current_frame()
|
||||
## set frame if needed
|
||||
act = layer.active_frame
|
||||
for s in stroke_list:
|
||||
if act:
|
||||
if use_current_frame or act.frame_number == fnum:
|
||||
#work on current frame if exists
|
||||
|
@ -372,10 +388,12 @@ 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)
|
||||
|
||||
# print(len(stroke_list), 'strokes pasted')
|
||||
add_stroke(s, target_frame, layer, obj)
|
||||
'''
|
||||
for s in stroke_data:
|
||||
add_stroke(s, target_frame)
|
||||
'''
|
||||
print(len(stroke_list), 'strokes pasted')
|
||||
|
||||
|
||||
### OPERATORS
|
||||
|
@ -383,24 +401,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 text in paperclip"
|
||||
bl_description = "Copy strokes 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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
#ct = check_radius()
|
||||
#ct = check_pressure()
|
||||
strokelist = copycut_strokes(copy=True, keep_empty=True)
|
||||
if not strokelist:
|
||||
self.report({'ERROR'}, 'Nothing to copy')
|
||||
self.report({'ERROR'},'rien a copier')
|
||||
return {"CANCELLED"}
|
||||
bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
|
||||
#if ct:
|
||||
|
@ -413,20 +431,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 text in paperclip"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Cut strokes to str in paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True)#ct = check_pressure()
|
||||
if not strokelist:
|
||||
self.report({'ERROR'},'Nothing to cut')
|
||||
return {"CANCELLED"}
|
||||
|
@ -443,10 +461,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected to paste on')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
|
@ -460,7 +478,7 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
|||
return {"CANCELLED"}
|
||||
|
||||
print('data loaded', time() - t0)
|
||||
add_multiple_strokes(data, use_current_frame=True, select=True)
|
||||
add_multiple_strokes(data, use_current_frame=True)
|
||||
print('total_time', time() - t0)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
@ -469,47 +487,14 @@ 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 from selected layers to str in paperclip"
|
||||
bl_label = "GP Copy multi strokes"
|
||||
bl_description = "Copy multiple layers>frames>strokes (unlocked and unhided ones) 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 == '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
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
bake_moves = True
|
||||
|
@ -519,35 +504,32 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
t0 = time()
|
||||
#ct = check_radius()
|
||||
#ct = check_pressure()
|
||||
layerdic = {}
|
||||
|
||||
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
|
||||
layerpool = [l for l in gpl if not l.hide and l.select]# and not l.lock
|
||||
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"}
|
||||
|
||||
if not bake_moves: # copy only drawed frames as is.
|
||||
if not bake_moves:# copy only drawed frames as is.
|
||||
for l in layerpool:
|
||||
if not l.frames:
|
||||
continue# skip empty layers
|
||||
|
||||
frame_dic = {}
|
||||
for f in l.frames:
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
if skip_empty_frame and not len(f.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,
|
||||
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||
context.scene.frame_set(f.frame_number)#use matrix of this frame
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
|
||||
|
||||
frame_dic[f.frame_number] = strokelist
|
||||
|
||||
layerdic[l.name] = frame_dic
|
||||
layerdic[l.info] = frame_dic
|
||||
|
||||
else: # bake position: copy frame where object as moved even if frame is unchanged
|
||||
else:# bake position: copy frame where object as moved even if frame is unchanged
|
||||
for l in layerpool:
|
||||
print('dump layer:', l.name)
|
||||
if not l.frames:
|
||||
continue# skip empty layers
|
||||
|
||||
|
@ -559,7 +541,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
curmat = prevmat = obj.matrix_world.copy()
|
||||
|
||||
for i in range(context.scene.frame_start, context.scene.frame_end):
|
||||
context.scene.frame_set(i) # use matrix of this frame
|
||||
context.scene.frame_set(i)#use matrix of this frame
|
||||
curmat = obj.matrix_world.copy()
|
||||
|
||||
# if object has moved or current time is on a draw key
|
||||
|
@ -571,17 +553,14 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
break
|
||||
|
||||
## skip empty frame if specified
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
if skip_empty_frame and not len(f.strokes):
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
|
||||
frame_dic[i] = strokelist
|
||||
|
||||
prevmat = curmat
|
||||
layerdic[l.name] = frame_dic
|
||||
layerdic[l.info] = frame_dic
|
||||
|
||||
## All to clipboard manager
|
||||
bpy.context.window_manager.clipboard = json.dumps(layerdic)
|
||||
|
@ -594,21 +573,21 @@ 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 on active layer"
|
||||
bl_label = "GP paste multi strokes"
|
||||
bl_description = "Paste multiple layers>frames>strokes from paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
org_frame = context.scene.frame_current
|
||||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
t0 = time()
|
||||
# add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
|
||||
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
|
||||
try:
|
||||
data = json.loads(bpy.context.window_manager.clipboard)
|
||||
except:
|
||||
|
@ -629,8 +608,8 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
|
|||
if not layer:
|
||||
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 occurence
|
||||
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
|
||||
|
||||
print('total_time', time() - t0)
|
||||
|
||||
|
@ -652,16 +631,13 @@ class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
|
|||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
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')
|
||||
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')
|
||||
|
||||
###---TEST zone
|
||||
|
||||
|
@ -730,9 +706,6 @@ GPCLIP_PT_clipboard_ui,
|
|||
)
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
|
@ -740,9 +713,6 @@ def register():
|
|||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
unregister_keymaps()
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
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
|
||||
|
@ -15,7 +13,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
# return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
#print('-!SNAP!-')
|
||||
|
@ -25,7 +23,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
def execute(self, context):
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
if not context.object or context.object.type != 'GPENCIL':
|
||||
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"}
|
||||
|
@ -50,7 +48,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
if warning:
|
||||
self.report({'WARNING'}, ', '.join(warning))
|
||||
|
||||
plane_co, plane_no = get_gp_draw_plane()
|
||||
plane_co, plane_no = get_gp_draw_plane(context)
|
||||
|
||||
if not plane_co:#default to object location
|
||||
plane_co = context.object.matrix_world.to_translation()#context.object.location
|
||||
|
@ -107,24 +105,20 @@ 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):
|
||||
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
|
||||
|
@ -135,13 +129,11 @@ def cursor_follow_update(self, context):
|
|||
|
||||
def cursor_follow(scene):
|
||||
'''Handler to make the cursor follow active object matrix changes on frame change'''
|
||||
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:
|
||||
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
|
||||
if not bpy.context.object:
|
||||
return
|
||||
global prev_matrix
|
||||
ob = bpy.context.object
|
||||
current_matrix = ob.matrix_world
|
||||
if not prev_matrix:
|
||||
prev_matrix = current_matrix.copy()
|
||||
|
@ -155,43 +147,14 @@ 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,
|
||||
|
@ -200,18 +163,14 @@ 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
|
||||
|
||||
## 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'
|
||||
# bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
||||
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
|
||||
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
|
||||
|
||||
|
@ -221,5 +180,3 @@ 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)
|
120
OP_depth_move.py
120
OP_depth_move.py
|
@ -1,120 +0,0 @@
|
|||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
class ODM_OT_depth_move(bpy.types.Operator):
|
||||
bl_idname = "object.depth_proportional_move"
|
||||
bl_label = "Depth move"
|
||||
bl_description = "Move object in the depth from camera POV while retaining same size in framing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type != 'CAMERA' # and context.scene.camera
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.init_mouse_x = event.mouse_x
|
||||
self.cam = bpy.context.scene.camera
|
||||
if not self.cam:
|
||||
self.report({'ERROR'}, 'No active camera')
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.cam_pos = self.cam.matrix_world.translation
|
||||
self.mode = 'distance'
|
||||
self.objects = [o for o in context.selected_objects if o.type != 'CAMERA']
|
||||
self.init_mats = [o.matrix_world.copy() for o in self.objects]
|
||||
|
||||
if self.cam.data.type == 'ORTHO':
|
||||
context.area.header_text_set(f'Move factor: 0.00')
|
||||
# distance is view vector based
|
||||
self.view_vector = Vector((0,0,-1))
|
||||
self.view_vector.rotate(self.cam.matrix_world)
|
||||
else:
|
||||
self.init_vecs = [o.matrix_world.translation - self.cam_pos for o in self.objects]
|
||||
self.init_dists = [v.length for v in self.init_vecs]
|
||||
context.area.header_text_set(f'Move factor: 0.00 | Mode: {self.mode} (M to switch)')
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
if self.mode == 'distance':
|
||||
factor = 0.1
|
||||
if event.shift:
|
||||
factor = 0.01
|
||||
|
||||
else:
|
||||
# Smaller factor for proportional dist
|
||||
factor = 0.01
|
||||
if event.shift:
|
||||
factor = 0.001
|
||||
|
||||
if event.type in {'MOUSEMOVE'}:
|
||||
diff = (event.mouse_x - self.init_mouse_x) * factor
|
||||
|
||||
if self.cam.data.type == 'ORTHO':
|
||||
# just push in view vector direction
|
||||
context.area.header_text_set(f'Move factor: {diff:.2f}')
|
||||
for i, obj in enumerate(self.objects):
|
||||
new_vec = self.init_mats[i].translation + (self.view_vector * diff)
|
||||
obj.matrix_world.translation = new_vec
|
||||
else:
|
||||
# Push from camera point and scale accordingly
|
||||
context.area.header_text_set(f'Move factor: {diff:.2f} | Mode: {self.mode} (M to switch)')
|
||||
|
||||
for i, obj in enumerate(self.objects):
|
||||
if self.mode == 'distance':
|
||||
## move with the same length for everyone
|
||||
new_vec = self.init_vecs[i] + (self.init_vecs[i].normalized() * diff)
|
||||
|
||||
else:
|
||||
## move with proportional factor from individual distance vector to camera
|
||||
new_vec = self.init_vecs[i] + (self.init_vecs[i] * diff)
|
||||
|
||||
obj.matrix_world.translation = self.cam_pos + new_vec
|
||||
dist_percentage = new_vec.length / self.init_dists[i]
|
||||
|
||||
obj.scale = self.init_mats[i].to_scale() * dist_percentage
|
||||
|
||||
if event.type in {'M'} and event.value == 'PRESS':
|
||||
# Switch mode
|
||||
self.mode = 'distance' if self.mode == 'proportional' else 'proportional'
|
||||
|
||||
if event.type in {'LEFTMOUSE'} and event.value == 'PRESS':
|
||||
context.area.header_text_set(None)
|
||||
return {"FINISHED"}
|
||||
|
||||
if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS':
|
||||
for i, obj in enumerate(self.objects):
|
||||
obj.matrix_world = self.init_mats[i]
|
||||
context.area.header_text_set(None)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
""" # Own standalone panel
|
||||
class ODM_PT_sudden_depth_panel(bpy.types.Panel):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_label = "Depth move"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS')
|
||||
"""
|
||||
|
||||
|
||||
### --- REGISTER ---
|
||||
|
||||
classes=(
|
||||
ODM_OT_depth_move,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -1,517 +0,0 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import gpu
|
||||
from time import time
|
||||
from mathutils import Vector, Matrix, Euler
|
||||
from mathutils.kdtree import KDTree
|
||||
from mathutils.geometry import intersect_line_plane, intersect_line_sphere_2d, intersect_line_line
|
||||
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d, \
|
||||
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
|
||||
from time import time
|
||||
from math import pi, cos, sin
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
|
||||
def get_gp_mat(gp, name, set_active=False):
|
||||
mat = bpy.data.materials.get(name)
|
||||
if not mat:
|
||||
mat = bpy.data.materials.new(name)
|
||||
bpy.data.materials.create_gpencil_data(mat)
|
||||
|
||||
if mat not in gp.data.materials[:]:
|
||||
gp.data.materials.append(mat)
|
||||
mat_index = gp.data.materials[:].index(mat)
|
||||
|
||||
if set_active:
|
||||
gp.active_material_index = mat_index
|
||||
|
||||
return mat
|
||||
|
||||
def get_gp_frame(gp_layer, frame=None):
|
||||
if frame is None:
|
||||
frame = bpy.context.scene.frame_current
|
||||
gp_frame = next((f for f in gp_layer.frames if f.frame_number==frame), None)
|
||||
if not gp_frame:
|
||||
gp_frame = gp_layer.frames.new(frame)
|
||||
|
||||
return gp_frame
|
||||
|
||||
def get_gp_layer(gp, name=None):
|
||||
if not name:
|
||||
return gp.data.layers.active
|
||||
|
||||
layer = gp.data.layers.get(name)
|
||||
if not layer:
|
||||
layer = gp.data.layers.new(name)
|
||||
|
||||
gp.data.layers.active = layer
|
||||
|
||||
return layer
|
||||
|
||||
def co_2d_to_3d(co, depth=0.1):
|
||||
area = bpy.context.area
|
||||
region = bpy.context.region
|
||||
rv3d = area.spaces.active.region_3d
|
||||
view_mat = rv3d.view_matrix.inverted()
|
||||
org = view_mat.to_translation()
|
||||
|
||||
depth_3d = view_mat @ Vector((0, 0, -depth))
|
||||
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||
|
||||
return region_2d_to_location_3d(region, rv3d, co, depth_3d)
|
||||
|
||||
|
||||
#vec = (region_2d_to_origin_3d(region, rv3d, co) - org).normalized()
|
||||
|
||||
return org + vec
|
||||
|
||||
return org + region_2d_to_vector_3d(region, rv3d , co)
|
||||
|
||||
def get_cuts_data(strokes, mouse, radius):
|
||||
gp = bpy.context.object
|
||||
|
||||
area = bpy.context.area
|
||||
region = bpy.context.region
|
||||
rv3d = area.spaces.active.region_3d
|
||||
view_mat = rv3d.view_matrix.inverted()
|
||||
org = view_mat.to_translation()
|
||||
mat = gp.matrix_world
|
||||
|
||||
cuts_data = []
|
||||
for s in strokes:
|
||||
is_polyline = 2<=len(s.points)<=5
|
||||
|
||||
if not is_polyline and not s.select:
|
||||
continue
|
||||
|
||||
print('Cut Stroke', s)
|
||||
for i, p in enumerate(s.points):
|
||||
|
||||
if not p.select and not is_polyline:
|
||||
continue
|
||||
# Test if the next or previous is unselected
|
||||
|
||||
edges = []
|
||||
|
||||
if i > 0:
|
||||
prev_p = s.points[i-1]
|
||||
if not prev_p.select and not is_polyline:
|
||||
edges.append((i-1, i))
|
||||
|
||||
if i < len(s.points)-1:
|
||||
next_p = s.points[i+1]
|
||||
if not next_p.select or is_polyline:
|
||||
edges.append((i, i+1))
|
||||
|
||||
for p1_index, p2_index in edges:
|
||||
p1 = s.points[p1_index]
|
||||
p2 = s.points[p2_index]
|
||||
|
||||
length_3d = (p2.co-p1.co).length
|
||||
|
||||
p1_3d = mat @ p1.co
|
||||
p2_3d = mat @ p2.co
|
||||
|
||||
p1_2d = location_3d_to_region_2d(region, rv3d, p1_3d)
|
||||
p2_2d = location_3d_to_region_2d(region, rv3d, p2_3d)
|
||||
|
||||
if p1_2d is None or p2_2d is None:
|
||||
continue
|
||||
|
||||
length_2d = (p2_2d-p1_2d).length
|
||||
|
||||
if length_2d <= 4:
|
||||
continue
|
||||
|
||||
intersects = intersect_line_sphere_2d(p1_2d, p2_2d, mouse, radius+2)
|
||||
intersects = [i for i in intersects if i is not None]
|
||||
if not intersects:
|
||||
continue
|
||||
|
||||
close_points = [(p1_2d-i).length < 1 or (p2_2d-i).length < 1 for i in intersects]
|
||||
if any(close_points):
|
||||
#print('close_points', close_points)
|
||||
continue
|
||||
|
||||
|
||||
print('intersects', intersects)
|
||||
|
||||
line_intersects = []
|
||||
for i_2d in intersects:
|
||||
#factor = ((i_2d-p1_2d).length) / length_2d
|
||||
#factor_3d = factor_2d * length_3d
|
||||
#vec = region_2d_to_vector_3d(region, rv3d, i_2d)
|
||||
#p3_3d = region_2d_to_location_3d(region, rv3d, i_2d, org)
|
||||
#p4_3d = region_2d_to_origin_3d(region, rv3d, i_2d)
|
||||
|
||||
p3_3d = co_2d_to_3d(i_2d, 0.1)
|
||||
p4_3d = co_2d_to_3d(i_2d, 1000)
|
||||
|
||||
#bpy.context.scene.cursor.location = p4_3d
|
||||
|
||||
line_intersect = intersect_line_line(p1_3d, p2_3d, p3_3d, p4_3d)
|
||||
if not line_intersect:
|
||||
continue
|
||||
|
||||
|
||||
i1_3d, _ = line_intersect
|
||||
|
||||
line_intersects += [i1_3d]
|
||||
|
||||
#context.scene.cursor.location = i1_3d
|
||||
|
||||
print('line_intersects', line_intersects)
|
||||
if line_intersects:
|
||||
line_intersects.sort(key=lambda x : (x-p1_3d).length)
|
||||
#cut_data[-1].sort(key=lambda x : (x-p1_3d).length)
|
||||
cut_data = [p1_index, p2_index, s, line_intersects]
|
||||
cuts_data.append(cut_data)
|
||||
|
||||
return cuts_data
|
||||
|
||||
|
||||
def circle(x, y, radius, segments):
|
||||
coords = []
|
||||
m = (1.0 / (segments - 1)) * (pi * 2)
|
||||
|
||||
for p in range(segments):
|
||||
p1 = x + cos(m * p) * radius
|
||||
p2 = y + sin(m * p) * radius
|
||||
coords.append((p1, p2))
|
||||
return coords
|
||||
|
||||
|
||||
class GPTB_OT_eraser(Operator):
|
||||
"""Draw a line with the mouse"""
|
||||
bl_idname = "gp.eraser"
|
||||
bl_label = "Eraser Brush"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def draw_callback_px(self):
|
||||
gpu.state.blend_set('ALPHA')
|
||||
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
|
||||
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
|
||||
|
||||
area = bpy.context.area
|
||||
#region = bpy.context.region
|
||||
#rv3d = area.spaces.active.region_3d
|
||||
|
||||
bg_color = area.spaces.active.shading.background_color
|
||||
#print(bg_color)
|
||||
|
||||
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (1, 1, 1, 1))
|
||||
for mouse, radius in self.mouse_path:
|
||||
circle_co = circle(*mouse, radius, 24)
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": circle_co})
|
||||
batch.draw(shader)
|
||||
|
||||
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
|
||||
'''
|
||||
def draw_holdout(self, context, event):
|
||||
gp = context.object
|
||||
mat_inv = gp.matrix_world.inverted()
|
||||
mouse_3d = co_2d_to_3d(self.mouse)
|
||||
radius_3d = co_2d_to_3d(self.mouse + Vector((self.radius, 0)))
|
||||
search_radius = (radius_3d-mouse_3d).length
|
||||
|
||||
#print('search_radius', search_radius)
|
||||
#print('radius', self.radius)
|
||||
|
||||
#bpy.context.scene.cursor.location = mouse_3d
|
||||
|
||||
for gp_frame, hld_stroke in self.hld_strokes:
|
||||
#print('Add Point')
|
||||
|
||||
hld_stroke.points.add(count=1)
|
||||
p = hld_stroke.points[-1]
|
||||
p.position = mat_inv @ mouse_3d
|
||||
p.pressure = search_radius * 2000
|
||||
|
||||
#context.scene.cursor.location = mouse_3d
|
||||
'''
|
||||
def get_radius(self, context, event):
|
||||
pressure = event.pressure or 1
|
||||
return context.scene.gptoolprops.eraser_radius * pressure
|
||||
|
||||
|
||||
|
||||
|
||||
def erase(self, context, event):
|
||||
gp = context.object
|
||||
mat_inv = gp.matrix_world.inverted()
|
||||
|
||||
new_points = []
|
||||
|
||||
#print(self.cuts_data)
|
||||
|
||||
# for f in self.gp_frames:
|
||||
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
|
||||
# f.drawing.strokes.remove(s)
|
||||
|
||||
#gp.data.materials.pop(index=self.hld_index)
|
||||
#bpy.data.materials.remove(self.hld_mat)
|
||||
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
|
||||
#bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
#for cut_data in self.cuts_data:
|
||||
|
||||
# print(cut_data, len(cut_data))
|
||||
t0 = time()
|
||||
print()
|
||||
|
||||
print('Number of cuts', len(self.mouse_path))
|
||||
|
||||
for mouse, radius in self.mouse_path:
|
||||
t1 = time()
|
||||
|
||||
print()
|
||||
x, y = mouse
|
||||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
|
||||
#print('select_circle', time()-t1)
|
||||
|
||||
t2 = time()
|
||||
cut_data = get_cuts_data(strokes, mouse, radius)
|
||||
#print('get_cuts_data', time()-t2)
|
||||
|
||||
#print([s for s in strokes if s.select])
|
||||
print('cut_data', cut_data)
|
||||
|
||||
t3 = time()
|
||||
for p1_index, p2_index, stroke, intersects in cut_data[::-1]:
|
||||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
|
||||
#print('p1_index', p1_index)
|
||||
#print('p2_index', p2_index)
|
||||
|
||||
p1 = stroke.points[p1_index]
|
||||
p2 = stroke.points[p2_index]
|
||||
|
||||
p1.select = True
|
||||
p2.select = True
|
||||
|
||||
number_cuts = len(intersects)
|
||||
|
||||
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
|
||||
|
||||
new_p1 = stroke.points[p1_index+1]
|
||||
new_p1.position = mat_inv@intersects[0]
|
||||
new_points += [(stroke, p1_index+1)]
|
||||
|
||||
#print('number_cuts', number_cuts)
|
||||
|
||||
if number_cuts == 2:
|
||||
new_p2 = stroke.points[p1_index+2]
|
||||
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||
#new_points += [new_p2]
|
||||
|
||||
new_p3 = stroke.points[p1_index+3]
|
||||
new_p3.position = mat_inv@intersects[1]
|
||||
new_points += [(stroke, p1_index+3)]
|
||||
|
||||
#print('subdivide', time() - t3)
|
||||
|
||||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
'''
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
|
||||
|
||||
bpy.ops.gpencil.select_less()
|
||||
|
||||
for p in tip_points:
|
||||
p.select = True
|
||||
for stroke, index in new_points:
|
||||
stroke.points[index].select = False
|
||||
'''
|
||||
|
||||
t4 = time()
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
|
||||
if selected_strokes:
|
||||
bpy.ops.gpencil.delete(type='POINTS')
|
||||
|
||||
print('remove points', time()- t4)
|
||||
|
||||
|
||||
#print('Total one cut', time()-t1)
|
||||
|
||||
#print('Total all cuts', time()-t0)
|
||||
#bpy.ops.gpencil.select_less()
|
||||
#for stroke, index in new_points:
|
||||
# stroke.points[index].select = False
|
||||
|
||||
#bpy.ops.object.mode_set(mode='OBJECT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
|
||||
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
|
||||
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
|
||||
def modal(self, context, event):
|
||||
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
|
||||
self.radius = self.get_radius(context, event)
|
||||
context.area.tag_redraw()
|
||||
|
||||
if event.type == 'LEFTMOUSE':
|
||||
#self.mouse = mouse
|
||||
#self.mouse_path.append((self.mouse, self.radius))
|
||||
self.erase(context, event)
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||
return {'FINISHED'}
|
||||
|
||||
if (self.mouse-self.mouse_prev).length < max(self.radius/1.33, 2):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
self.mouse_prev = self.mouse
|
||||
|
||||
if event.type in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:
|
||||
#self.draw_holdout(context, event)
|
||||
self.mouse_path.append((self.mouse, self.radius))
|
||||
#self.update_cuts_data(context, event)
|
||||
#self.erase(context, event)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
gp = context.object
|
||||
matrix = gp.matrix_world
|
||||
|
||||
self.gpencil_selectmode_edit = context.scene.tool_settings.gpencil_selectmode_edit
|
||||
self.radius = self.get_radius(context, event)
|
||||
self.mouse_prev = self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
self.mouse_path = [(self.mouse_prev, self.radius)]
|
||||
|
||||
area = context.area
|
||||
region = context.region
|
||||
w, h = region.width, region.height
|
||||
|
||||
rv3d = area.spaces.active.region_3d
|
||||
view_mat = rv3d.view_matrix.inverted()
|
||||
org = self.org = view_mat.to_translation()
|
||||
|
||||
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||
|
||||
|
||||
#print('ORG', org)
|
||||
#print('view_mat', view_mat)
|
||||
|
||||
|
||||
self.cuts_data = []
|
||||
|
||||
#org = self.view_mat @ Vector((0, 0, -10))
|
||||
#self.plane_no = self.plane_co-self.org
|
||||
|
||||
#bottom_left = region_2d_to_location_3d(region, rv3d , (0, 0), self.plane_co)
|
||||
#bottom_right = region_2d_to_location_3d(region, rv3d , (0, w), self.plane_co)
|
||||
|
||||
#bottom_left = intersect_line_plane(self.org, bottom_left, self.plane_co, self.plane_no)
|
||||
#bottom_right = intersect_line_plane(self.org, bottom_right, self.plane_co, self.plane_no)
|
||||
|
||||
#self.scale_fac = (bottom_right-bottom_left).length / w
|
||||
|
||||
#print('scale_fac', self.scale_fac)
|
||||
#depth_location = view_mat @ Vector((0, 0, -1))
|
||||
#context.scene.cursor.location = depth_location
|
||||
|
||||
#plane_2d = [(0, 0), (0, h), (w, h), (w, h)]
|
||||
#plane_3d = [region_2d_to_location_3d(p)]
|
||||
|
||||
|
||||
t0 = time()
|
||||
gp_mats = gp.data.materials
|
||||
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
|
||||
self.gp_frames = [l.current_frame() for l in gp_layers]
|
||||
'''
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
|
||||
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
|
||||
print('get_gp_points', time()-t0)
|
||||
|
||||
t0 = time()
|
||||
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||
points_data = [(s, f, m, p, org + ((matrix @ p.position)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||
print('points_to_2d', time()-t0)
|
||||
|
||||
#print(points_data)
|
||||
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
|
||||
|
||||
#for s, f, m, p, co in self.points_data:
|
||||
# p.position = co
|
||||
|
||||
|
||||
t0 = time()
|
||||
self.kd_tree = KDTree(len(self.points_data))
|
||||
for i, point_data in enumerate(self.points_data):
|
||||
s, f, m, p, co = point_data
|
||||
self.kd_tree.insert(co, i)
|
||||
|
||||
self.kd_tree.balance()
|
||||
print('create kdtree', time()-t0)
|
||||
'''
|
||||
|
||||
'''
|
||||
# Create holdout mat
|
||||
self.hld_mat = get_gp_mat(gp, name='Eraser Holdout Stroke')
|
||||
self.hld_mat.grease_pencil.use_stroke_holdout = True
|
||||
self.hld_mat.grease_pencil.show_stroke = True
|
||||
self.hld_mat.grease_pencil.show_fill = False
|
||||
self.hld_mat.grease_pencil.use_overlap_strokes = True
|
||||
|
||||
self.hld_index = gp_mats[:].index(self.hld_mat)
|
||||
|
||||
self.hld_strokes = []
|
||||
for f in self.gp_frames:
|
||||
hld_stroke = f.drawing.strokes.new()
|
||||
hld_stroke.start_cap_mode = 'ROUND'
|
||||
hld_stroke.end_cap_mode = 'ROUND'
|
||||
hld_stroke.material_index = self.hld_index
|
||||
|
||||
#hld_stroke.line_width = self.radius
|
||||
|
||||
self.hld_strokes.append((f, hld_stroke))
|
||||
|
||||
self.draw_holdout(context, event)
|
||||
'''
|
||||
|
||||
|
||||
context.area.tag_redraw()
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
### --- REGISTER ---
|
||||
|
||||
classes=(
|
||||
GPTB_OT_eraser,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -1,47 +1,20 @@
|
|||
import bpy
|
||||
import os
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from .utils import show_message_box, get_addon_prefs
|
||||
|
||||
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 = "Check File"
|
||||
bl_label = "File check"
|
||||
bl_description = "Check / correct some aspect of the file, properties and such and report"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
## list of actions :
|
||||
# Lock main cam
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.region_data.view_perspective == 'CAMERA'
|
||||
|
||||
## list of action :
|
||||
# Lock main cam:
|
||||
# set scene res
|
||||
# set scene percentage at 100:
|
||||
# set show slider and sync range
|
||||
|
@ -50,40 +23,14 @@ 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 = utils.get_addon_prefs()
|
||||
fix = prefs.fixprops
|
||||
prefs = get_addon_prefs()
|
||||
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
|
||||
if not 'layout' in Path(bpy.data.filepath).stem:#dont touch layout cameras
|
||||
if context.scene.camera:
|
||||
cam = context.scene.camera
|
||||
if cam.name == 'draw_cam' and cam.parent:
|
||||
|
@ -95,78 +42,57 @@ 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
|
||||
if fix.set_scene_res:
|
||||
rx, ry = prefs.render_res_x, prefs.render_res_y
|
||||
# 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
|
||||
if fix.set_slider_n_sync:
|
||||
for window in bpy.context.window_manager.windows:
|
||||
screen = window.screen
|
||||
for area in screen.areas:
|
||||
if area.type == 'DOPESHEET_EDITOR':
|
||||
if hasattr(area.spaces[0], 'show_sliders'):
|
||||
setattr(area.spaces[0], 'show_sliders', True)
|
||||
|
||||
if hasattr(area.spaces[0], 'show_locked_time'):
|
||||
setattr(area.spaces[0], 'show_locked_time', True)
|
||||
|
||||
## set cursor type
|
||||
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
|
||||
tool = fix.select_active_tool
|
||||
## set fps according to preferences settings
|
||||
if context.scene.render.fps != prefs.fps:
|
||||
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
|
||||
context.scene.render.fps = prefs.fps
|
||||
|
||||
## set cursor type (according to prefs ?)
|
||||
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
|
||||
tool = prefs.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:
|
||||
# problems.append(f'Activated Gp additive drawing mode (snowflake)')
|
||||
# context.scene.tool_settings.use_gpencil_draw_additive = True
|
||||
|
||||
## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
|
||||
if not context.scene.tool_settings.use_gpencil_draw_additive:
|
||||
problems.append(f'Activated Gp additive drawing mode (snowflake)')
|
||||
context.scene.tool_settings.use_gpencil_draw_additive = True
|
||||
|
||||
## GP stroke placement/projection check
|
||||
if fix.check_front_axis:
|
||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
|
||||
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
|
||||
|
||||
if fix.check_placement:
|
||||
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
|
||||
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:
|
||||
fcu_ct = 0
|
||||
for act in bpy.data.actions:
|
||||
if not act.users:
|
||||
|
@ -176,114 +102,19 @@ 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 in console)')
|
||||
|
||||
## Object visibility conflict
|
||||
if fix.list_obj_vis_conflict:
|
||||
viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
if not (o.hide_get() == o.hide_viewport == o.hide_render):
|
||||
hv = 'No' if o.hide_get() else 'Yes'
|
||||
vp = 'No' if o.hide_viewport else 'Yes'
|
||||
rd = 'No' if o.hide_render else 'Yes'
|
||||
viz_ct += 1
|
||||
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
|
||||
if viz_ct:
|
||||
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
||||
|
||||
## GP modifiers visibility conflict
|
||||
if fix.list_gp_mod_vis_conflict:
|
||||
mod_viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
vp = 'Yes' if m.show_viewport else 'No'
|
||||
rd = 'Yes' if m.show_render else 'No'
|
||||
mod_viz_ct += 1
|
||||
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
|
||||
|
||||
if mod_viz_ct:
|
||||
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
|
||||
|
||||
## check if GP modifier have broken layer targets
|
||||
if fix.list_broken_mod_targets:
|
||||
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
|
||||
lay_name_list = [l.name for l in o.data.layers]
|
||||
for m in o.modifiers:
|
||||
if not hasattr(m, 'layer_filter'):
|
||||
continue
|
||||
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
|
||||
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
|
||||
print(mess)
|
||||
problems.append(mess)
|
||||
|
||||
## Use median point
|
||||
if fix.set_pivot_median_point:
|
||||
if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
|
||||
problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
|
||||
if apply:
|
||||
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
|
||||
|
||||
if fix.disable_guide:
|
||||
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
|
||||
problems.append(f"Disabled Draw Guide")
|
||||
if apply:
|
||||
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
|
||||
|
||||
if fix.autokey_add_n_replace:
|
||||
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
|
||||
problems.append(f"Autokey mode reset to 'Add & Replace'")
|
||||
if apply:
|
||||
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
|
||||
|
||||
if fix.file_path_type != 'none':
|
||||
pathes = []
|
||||
for p in bpy.utils.blend_paths():
|
||||
if fix.file_path_type == 'RELATIVE':
|
||||
if not p.startswith('//'):
|
||||
pathes.append(p)
|
||||
elif fix.file_path_type == 'ABSOLUTE':
|
||||
if p.startswith('//'):
|
||||
pathes.append(p)
|
||||
if pathes:
|
||||
mess = f'{len(pathes)}/{len(bpy.utils.blend_paths())} paths not {fix.file_path_type.lower()} (see console)'
|
||||
problems.append(mess)
|
||||
print(mess)
|
||||
print('\n'.join(pathes))
|
||||
print('-')
|
||||
|
||||
if fix.lock_object_mode != 'none':
|
||||
lockmode = bpy.context.scene.tool_settings.lock_object_mode
|
||||
if fix.lock_object_mode == 'LOCK':
|
||||
if not lockmode:
|
||||
problems.append(f"Lock object mode toggled On")
|
||||
if apply:
|
||||
bpy.context.scene.tool_settings.lock_object_mode = True
|
||||
|
||||
elif fix.lock_object_mode == 'UNLOCK':
|
||||
if lockmode:
|
||||
problems.append(f"Lock object mode toggled Off")
|
||||
if apply:
|
||||
bpy.context.scene.tool_settings.lock_object_mode = False
|
||||
|
||||
if fix.remove_redundant_strokes:
|
||||
ct = remove_stroke_exact_duplications(apply=apply)
|
||||
if ct > 0:
|
||||
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
|
||||
problems.append(mess)
|
||||
|
||||
# ## Set onion skin filter to 'All type'
|
||||
# fix_kf_type = 0
|
||||
# for gp in bpy.data.grease_pencils:#from data
|
||||
# if not gp.is_annotation:
|
||||
# if gp.onion_keyframe_type != 'ALL':
|
||||
# gp.onion_keyframe_type = 'ALL'
|
||||
# fix_kf_type += 1
|
||||
# if fix_kf_type:
|
||||
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
||||
problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
|
||||
|
||||
## Set onion skin filter to 'All type'
|
||||
fix_kf_type = 0
|
||||
for gp in bpy.data.grease_pencils:#from data
|
||||
if not gp.is_annotation:
|
||||
if gp.onion_keyframe_type != 'ALL':
|
||||
gp.onion_keyframe_type = 'ALL'
|
||||
fix_kf_type += 1
|
||||
if fix_kf_type:
|
||||
problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
||||
# for ob in context.scene.objects:#from object
|
||||
# if ob.type == 'GREASEPENCIL':
|
||||
# if ob.type == 'GPENCIL':
|
||||
# ob.data.onion_keyframe_type = 'ALL'
|
||||
|
||||
#### --- print fix/problems report
|
||||
|
@ -294,92 +125,47 @@ 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
|
||||
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
|
||||
utils.show_message_box(problems, _title = title, _icon = 'INFO')
|
||||
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO')
|
||||
else:
|
||||
self.report({'INFO'}, 'All good')
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_copy_string_to_clipboard(bpy.types.Operator):
|
||||
bl_idname = "gp.copy_string_to_clipboard"
|
||||
bl_label = "Copy String"
|
||||
bl_description = "Copy passed string to clipboard"
|
||||
""" 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"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
if not self.string:
|
||||
# self.report({'ERROR'}, 'Nothing to copy')
|
||||
return {"CANCELLED"}
|
||||
bpy.context.window_manager.clipboard = self.string
|
||||
self.report({'INFO'}, f'Copied: {self.string}')
|
||||
return {"FINISHED"}
|
||||
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()
|
||||
|
||||
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"}
|
||||
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"} """
|
||||
|
||||
|
||||
class GPTB_OT_links_checker(bpy.types.Operator):
|
||||
|
@ -408,28 +194,14 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
|||
|
||||
|
||||
layout.separator()
|
||||
# layout = layout.column() # thinner linespace
|
||||
for l in self.all_lnks:
|
||||
if l[1] == 'CANCEL':
|
||||
layout.label(text=l[0], icon=l[1])
|
||||
continue
|
||||
|
||||
if l[1] == 'LIBRARY_DATA_BROKEN':
|
||||
split=layout.split(factor=0.85)
|
||||
split.label(text=l[0], icon=l[1])
|
||||
# layout.label(text=l[0], icon=l[1])
|
||||
layout.label(text=l[0], icon=l[1])
|
||||
else:
|
||||
split=layout.split(factor=0.70, align=True)
|
||||
split=layout.split(factor=0.75)
|
||||
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
|
||||
split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
|
||||
split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()#os.path.abspath(bpy.path.abspath(dirname(l[0])))
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.all_lnks = []
|
||||
|
@ -438,32 +210,19 @@ 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)):
|
||||
# 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
|
||||
lfp = Path(lib)
|
||||
realib = Path(current)
|
||||
if not lfp.exists():
|
||||
self.broke_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
|
||||
else:
|
||||
if realib.as_posix().startswith('//'):
|
||||
rel_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
|
||||
else:
|
||||
abs_ct += 1
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )
|
||||
except:
|
||||
self.broke_ct += 1
|
||||
self.all_lnks.append( (f"{current}" , 'CANCEL') ) # error accessing file
|
||||
|
||||
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
|
||||
|
||||
if not self.all_lnks:
|
||||
self.report({'INFO'}, 'No external links in files')
|
||||
|
@ -485,221 +244,50 @@ 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"}
|
||||
popup_width = 800
|
||||
if maxlen < 50:
|
||||
popup_width = 500
|
||||
elif maxlen > 100:
|
||||
popup_width = 1000
|
||||
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)
|
||||
|
||||
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"
|
||||
'''### OLD
|
||||
class GPTB_OT_check_scene(bpy.types.Operator):
|
||||
bl_idname = "gp.scene_check"
|
||||
bl_label = "Check GP scene"
|
||||
bl_description = "Check and fix scene settings"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
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
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
## 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 ?
|
||||
|
||||
### -- 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"}
|
||||
## 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
|
||||
|
||||
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'}
|
||||
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
|
||||
|
||||
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"}
|
||||
## disable autolock
|
||||
context.scene.tool_settings.lock_object_mode = False
|
||||
|
||||
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'}
|
||||
return {"FINISHED"}
|
||||
'''
|
||||
|
||||
classes = (
|
||||
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_check_scene,
|
||||
GPTB_OT_file_checker,
|
||||
GPTB_OT_links_checker,
|
||||
)
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
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)
|
|
@ -1,214 +0,0 @@
|
|||
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)
|
|
@ -1,84 +0,0 @@
|
|||
import bpy
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import os
|
||||
from os.path import basename
|
||||
# import re
|
||||
|
||||
from .utils import show_message_box
|
||||
|
||||
"""## not used for now
|
||||
class GPTB_OT_check_git(bpy.types.Operator):
|
||||
'''check if git is in path'''
|
||||
bl_idname = "gptb.check_git"
|
||||
bl_label = "Check if git is in system path"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ok = shutil.which('git')
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if self.ok:
|
||||
layout.label(text='Ok ! git is in system PATH', icon='INFO')
|
||||
else:
|
||||
layout.label(text='Git is not in system PATH', icon='CANCEL')
|
||||
layout.operator('wm.url_open', text='Download And Install From Here', icon='URL').url = 'https://git-scm.com/download/'
|
||||
"""
|
||||
|
||||
|
||||
def git_update(folder: str) -> str:
|
||||
''' Try to git pull fast foward only in passed folder and return console output'''
|
||||
os.chdir(folder)
|
||||
name = basename(folder)
|
||||
print(f'Pulling in {name}')
|
||||
pull_cmd = ['git', 'pull', '--ff-only'] # git pull --ff-only
|
||||
pull_ret = subprocess.check_output(pull_cmd)
|
||||
return pull_ret.decode()
|
||||
|
||||
|
||||
class GPTB_OT_git_pull(bpy.types.Operator):
|
||||
"""Update addon with git pull if possible"""
|
||||
bl_idname = "gptb.git_pull"
|
||||
bl_label = "Gptoolbox Git Pull Update"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
# def invoke(self, context, event):
|
||||
# return self.execute(context)
|
||||
|
||||
# def draw(self, context):
|
||||
|
||||
def execute(self, context):
|
||||
if not shutil.which('git'):
|
||||
self.report({'ERROR'}, 'Git not found in path, if just installed, restart Blender/Computer')
|
||||
return {'CANCELLED'}
|
||||
|
||||
ret = git_update(Path(__file__).parent.as_posix())
|
||||
|
||||
if 'Already up to date' in ret:
|
||||
self.report({'INFO'}, 'Already up to date')
|
||||
show_message_box(ret.rstrip('\n').split('\n'))
|
||||
elif 'Fast-forward' in ret and 'Updating' in ret:
|
||||
self.report({'INFO'}, 'Updated ! Restart Blender')
|
||||
show_message_box(['Updated! Restart Blender.'] + ret.rstrip('\n').split('\n'))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
classes = (
|
||||
# GPTB_OT_check_git,
|
||||
GPTB_OT_git_pull,
|
||||
)
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
455
OP_helpers.py
455
OP_helpers.py
|
@ -1,19 +1,12 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
import math
|
||||
|
||||
from time import ctime
|
||||
from mathutils import Vector #, Matrix
|
||||
from mathutils import Vector#, Matrix
|
||||
from pathlib import Path
|
||||
from math import radians
|
||||
from bpy.types import Operator
|
||||
from .utils import get_gp_objects, set_collection, show_message_box
|
||||
|
||||
from .view3d_utils import View3D
|
||||
from . import utils
|
||||
|
||||
class GPTB_OT_copy_text(Operator):
|
||||
class GPTB_OT_copy_text(bpy.types.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"}
|
||||
|
||||
|
@ -25,53 +18,23 @@ class GPTB_OT_copy_text(Operator):
|
|||
self.report({'INFO'}, mess)
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_flipx_view(Operator):
|
||||
bl_idname = "view3d.camera_mirror_flipx"
|
||||
bl_label = "Cam Mirror Flipx"
|
||||
class GPTB_OT_flipx_view(bpy.types.Operator):
|
||||
bl_idname = "gp.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.area.type == 'VIEW_3D' and \
|
||||
context.region_data.view_perspective == 'CAMERA'
|
||||
return context.region_data.view_perspective == 'CAMERA'
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.camera.scale.x *= -1
|
||||
return {"FINISHED"}
|
||||
|
||||
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):
|
||||
class GPTB_OT_rename_data_from_obj(bpy.types.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"}
|
||||
|
||||
|
@ -79,7 +42,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
if not self.rename_all:
|
||||
|
@ -93,7 +56,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
|
|||
else:
|
||||
oblist = []
|
||||
for o in context.scene.objects:
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.type == 'GPENCIL':
|
||||
if o.name == o.data.name:
|
||||
continue
|
||||
oblist.append(f'{o.data.name} -> {o.name}')
|
||||
|
@ -150,9 +113,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(Operator):
|
||||
class GPTB_OT_draw_cam(bpy.types.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"}
|
||||
|
||||
|
@ -190,11 +153,10 @@ class GPTB_OT_draw_cam(Operator):
|
|||
# get main cam and error if not available
|
||||
if drawcam.name == 'draw_cam':
|
||||
maincam = drawcam.parent
|
||||
maincam.data.show_passepartout = context.scene.gptoolprops.drawcam_passepartout
|
||||
|
||||
else:
|
||||
maincam = None
|
||||
main_name = drawcam.get('maincam_name')# Custom prop with previous active cam.
|
||||
main_name = drawcam.get('maincam_name')# Custom prop with previous avtive cam.
|
||||
if main_name:
|
||||
maincam = context.scene.objects.get(main_name)
|
||||
|
||||
|
@ -207,7 +169,7 @@ class GPTB_OT_draw_cam(Operator):
|
|||
|
||||
# dcam_col = bpy.data.collections.get(camcol_name)
|
||||
# if not dcam_col:
|
||||
utils.set_collection(drawcam, camcol_name)
|
||||
set_collection(drawcam, camcol_name)
|
||||
|
||||
# Swap to it, unhide if necessary and hide previous
|
||||
context.scene.camera = maincam
|
||||
|
@ -225,22 +187,16 @@ class GPTB_OT_draw_cam(Operator):
|
|||
if not drawcam:
|
||||
created=True
|
||||
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
|
||||
utils.set_collection(drawcam, 'manip_cams')
|
||||
drawcam.show_name = True
|
||||
set_collection(drawcam, 'manip_cams')
|
||||
|
||||
if dcam_name == 'draw_cam':
|
||||
drawcam.parent = maincam
|
||||
if created: # set to main at creation time
|
||||
if created:#set to main at creation time
|
||||
drawcam.matrix_world = maincam.matrix_world
|
||||
drawcam.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
|
||||
|
@ -250,7 +206,7 @@ class GPTB_OT_draw_cam(Operator):
|
|||
drawcam.parent = act
|
||||
vec = Vector((0,1,0))
|
||||
|
||||
if act.type == 'GREASEPENCIL':
|
||||
if act.type == 'GPENCIL':
|
||||
#change vector according to alignement
|
||||
vec = get_gp_alignement_vector(context)
|
||||
|
||||
|
@ -270,7 +226,7 @@ class GPTB_OT_draw_cam(Operator):
|
|||
drawcam.hide_viewport = False
|
||||
maincam.hide_viewport = True
|
||||
|
||||
if created and drawcam.name == 'obj_cam': # Go in camera view
|
||||
if created and drawcam.name == 'obj_cam':#Go in camera view
|
||||
context.region_data.view_perspective = 'CAMERA'
|
||||
# ## make active
|
||||
# bpy.context.view_layer.objects.active = ob
|
||||
|
@ -278,16 +234,15 @@ class GPTB_OT_draw_cam(Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_set_view_as_cam(Operator):
|
||||
class GPTB_OT_set_view_as_cam(bpy.types.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.area.type == 'VIEW_3D' and \
|
||||
context.region_data.view_perspective != 'CAMERA'# need to be out of camera
|
||||
return 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):
|
||||
|
@ -320,9 +275,9 @@ class GPTB_OT_set_view_as_cam(Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_reset_cam_rot(Operator):
|
||||
class GPTB_OT_reset_cam_rot(bpy.types.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"}
|
||||
|
||||
|
@ -331,115 +286,52 @@ class GPTB_OT_reset_cam_rot(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 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':
|
||||
# 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
|
||||
else:
|
||||
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(Operator):
|
||||
class GPTB_OT_toggle_mute_animation(bpy.types.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)
|
||||
mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL
|
||||
skip_gp : bpy.props.BoolProperty(default=False)
|
||||
skip_obj : bpy.props.BoolProperty(default=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selection = event.shift
|
||||
return self.execute(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.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
|
||||
if self.skip_gp and o.type == 'GPENCIL':
|
||||
continue
|
||||
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
if self.skip_obj and o.type != 'GPENCIL':
|
||||
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
|
||||
|
@ -447,45 +339,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
|||
if not act:
|
||||
continue
|
||||
|
||||
print(f'\n---{o.name}:')
|
||||
self.set_action_mute(act)
|
||||
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
print(i, fcu.data_path, fcu.array_index)
|
||||
fcu.mute = self.mute
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
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):
|
||||
class GPTB_OT_list_disabled_anims(bpy.types.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"}
|
||||
|
||||
|
@ -506,41 +368,25 @@ class GPTB_OT_list_disabled_anims(Operator):
|
|||
pool = context.scene.objects
|
||||
|
||||
for o in pool:
|
||||
# if self.skip_gp and o.type == 'GREASEPENCIL':
|
||||
# if self.skip_gp and o.type == 'GPENCIL':
|
||||
# continue
|
||||
# if self.skip_obj and o.type != 'GREASEPENCIL':
|
||||
# if self.skip_obj and o.type != 'GPENCIL':
|
||||
# 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}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}')
|
||||
else:
|
||||
li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}')
|
||||
if li:
|
||||
utils.show_message_box(li)
|
||||
show_message_box(li)
|
||||
else:
|
||||
self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}")
|
||||
return {'FINISHED'}
|
||||
|
@ -548,7 +394,7 @@ class GPTB_OT_list_disabled_anims(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(Operator):
|
||||
class GPTB_OT_overlay_presets(bpy.types.Operator):
|
||||
bl_idname = "gp.overlay_presets"
|
||||
bl_label = "Overlay presets"
|
||||
bl_description = "Overlay save/load presets for showing only whats needed"
|
||||
|
@ -603,202 +449,15 @@ class GPTB_OT_overlay_presets(Operator):
|
|||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_clear_active_frame(Operator):
|
||||
bl_idname = "gp.clear_active_frame"
|
||||
bl_label = "Clear Active Frame"
|
||||
bl_description = "Delete all strokes in active frames"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
l = obj.data.layers.active
|
||||
if not l:
|
||||
self.report({'ERROR'}, 'No layers')
|
||||
return {'CANCELLED'}
|
||||
f = l.current_frame()
|
||||
if not f:
|
||||
self.report({'ERROR'}, 'No active frame')
|
||||
return {'CANCELLED'}
|
||||
|
||||
ct = len(f.drawing.strokes)
|
||||
if not ct:
|
||||
self.report({'ERROR'}, 'Active frame already empty')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for s in reversed(f.drawing.strokes):
|
||||
f.drawing.strokes.remove(s)
|
||||
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_check_canvas_alignement(Operator):
|
||||
bl_idname = "gp.check_canvas_alignement"
|
||||
bl_label = "Check Canvas Alignement"
|
||||
bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# if lock_axis is 'VIEW' then the draw axis is always aligned
|
||||
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
||||
|
||||
def execute(self, context):
|
||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
|
||||
self.report({'INFO'}, 'Drawing plane use "View" (always aligned)')
|
||||
return {'FINISHED'}
|
||||
|
||||
_angle, ret, message = utils.check_angle_from_view(obj=context.object, context=context)
|
||||
if not ret or not message:
|
||||
self.report({'ERROR'}, 'Could not get view angle infos')
|
||||
return {'CANCELLED'}
|
||||
|
||||
title = 'Aligned \o/' if ret == 'INFO' else "Not aligned !"
|
||||
|
||||
if context.region_data.view_perspective != 'CAMERA':
|
||||
title = title + ' ( not in camera view)'
|
||||
|
||||
utils.show_message_box(_message=message, _title=title, _icon=ret)
|
||||
# self.report({ret}, message)
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_step_select_frames(Operator):
|
||||
bl_idname = "gptb.step_select_frames"
|
||||
bl_label = "Step Select Frame"
|
||||
bl_description = "Select frames by a step frame value"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
start : bpy.props.IntProperty(name='Start Frame',
|
||||
description='Start frame of the step animation',
|
||||
default=100)
|
||||
|
||||
step : bpy.props.IntProperty(name='Step',
|
||||
description='Step of the frame, value of 2 select one frame on two',
|
||||
default=2,
|
||||
min=2)
|
||||
|
||||
strict : bpy.props.BoolProperty(name='Strict',
|
||||
description='Strictly select step frame from start to scene end range\
|
||||
\nElse reset step when a gap exsits already',
|
||||
default=False)
|
||||
|
||||
# TODO: add option to start at cursor (True default)
|
||||
|
||||
def invoke(self, context, execute):
|
||||
## list frame to keep
|
||||
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||
# return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, 'step')
|
||||
|
||||
col.prop(self, 'strict')
|
||||
if self.strict:
|
||||
col.prop(self, 'start')
|
||||
|
||||
## helper (need more work)
|
||||
# col.separator()
|
||||
# range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}"
|
||||
# col.label(text=f'Will keep {range_string}')
|
||||
if not self.strict:
|
||||
col.label(text=f'Each gap will be considered a new step start', icon='INFO')
|
||||
|
||||
def execute(self, context):
|
||||
numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)]
|
||||
self.to_select = numbers
|
||||
|
||||
## Negative switch : list frames to remove
|
||||
self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers]
|
||||
|
||||
gp = context.object.data
|
||||
|
||||
## Get frame summary (reset start after each existing gaps)
|
||||
key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames]))
|
||||
key_summary.sort()
|
||||
print('key summary: ', key_summary)
|
||||
|
||||
start = key_summary[0]
|
||||
if self.strict:
|
||||
to_select = self.to_select
|
||||
else:
|
||||
to_select = []
|
||||
prev = None
|
||||
for k in key_summary:
|
||||
print(k, prev)
|
||||
if prev is not None and k != prev + 1:
|
||||
## this is a gap ! new start
|
||||
prev = start = k
|
||||
# print('new start', start)
|
||||
continue
|
||||
|
||||
new_range = [i for i in range(start, key_summary[-1] + 1, self.step)]
|
||||
# print('new_range: ', new_range)
|
||||
if k not in new_range:
|
||||
to_select.append(k)
|
||||
|
||||
prev = k
|
||||
|
||||
## deselect all
|
||||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
f.select = False
|
||||
|
||||
print('To select:', to_select)
|
||||
gct = 0
|
||||
for i in to_select:
|
||||
ct = 0
|
||||
for l in gp.layers:
|
||||
frame = next((f for f in l.frames if f.frame_number == i), None)
|
||||
if not frame:
|
||||
continue
|
||||
|
||||
## Select instead of remove
|
||||
frame.select = True
|
||||
## Optionnally remove frames ?
|
||||
# l.frames.remove(frame)
|
||||
|
||||
ct += 1
|
||||
|
||||
# print(f'{i}: Selected {ct} frame(s)')
|
||||
gct += ct
|
||||
|
||||
self.report({'INFO'}, f'Selected {gct} frames')
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_open_addon_prefs(Operator):
|
||||
bl_idname = "gptb.open_addon_prefs"
|
||||
bl_label = "Open Addon Prefs"
|
||||
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
utils.open_addon_prefs()
|
||||
return {'FINISHED'}
|
||||
|
||||
classes = (
|
||||
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,164 +0,0 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_layer_list(self, context):
|
||||
'''return (identifier, name, description) of enum content'''
|
||||
if not context:
|
||||
return [('None', 'None','None')]
|
||||
if not context.object:
|
||||
return [('None', 'None','None')]
|
||||
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
# try:
|
||||
# except:
|
||||
# return [("", "", "")]
|
||||
# return [(i, basename(i), "") for i in blends]
|
||||
# return [(i.path, basename(i.path), "") for i in self.blends]
|
||||
|
||||
class GPTB_OT_duplicate_send_to_layer(Operator) :
|
||||
bl_idname = "gp.duplicate_send_to_layer"
|
||||
bl_label = 'Duplicate Send To Layer'
|
||||
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
|
||||
# important to have the updated enum here as bl_property
|
||||
bl_property = "layers_enum"
|
||||
|
||||
layers_enum : bpy.props.EnumProperty(
|
||||
name="Duplicate to layers",
|
||||
description="Duplicate selected keys in active layer and send them to chosen layer",
|
||||
items=get_layer_list,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.delete_source:
|
||||
return f"Move selected keys in active layer to chosen layer"
|
||||
else:
|
||||
return f"Copy selected keys in active layer and send to chosen layer"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'\
|
||||
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
|
||||
|
||||
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
|
||||
|
||||
def execute(self, context):
|
||||
target_layer = self.layers_enum
|
||||
if not target_layer:
|
||||
self.report({'WARNING'}, 'Target layer not specified')
|
||||
return {'CANCELLED'}
|
||||
|
||||
gpl = context.object.data.layers
|
||||
target_layer = gpl.get(target_layer)
|
||||
|
||||
act_layer = gpl.active
|
||||
selected_frames = [f for f in act_layer.frames if f.select]
|
||||
|
||||
act_frame_num = [f.frame_number for f in act_layer.frames if f.select]
|
||||
|
||||
to_replace = [f for f in target_layer.frames if f.frame_number in act_frame_num]
|
||||
|
||||
replaced = len(to_replace)
|
||||
|
||||
## Remove overlapping frames
|
||||
for f in reversed(to_replace):
|
||||
target_layer.frames.remove(f.frame_number)
|
||||
|
||||
## Copy original frames
|
||||
for f in selected_frames:
|
||||
utils.copy_frame_at(f, target_layer, f.frame_number)
|
||||
# target_layer.frames.copy(f) # GPv2
|
||||
sent = len(selected_frames)
|
||||
|
||||
## Delete original frames as an option
|
||||
if self.delete_source:
|
||||
for f in reversed(selected_frames):
|
||||
act_layer.frames.remove(f.frame_number)
|
||||
mess = f'{sent} keys moved'
|
||||
else:
|
||||
mess = f'{sent} keys copied'
|
||||
|
||||
if replaced:
|
||||
mess += f' ({replaced} replaced)'
|
||||
|
||||
self.report({'INFO'}, mess)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
gp = context.object.data
|
||||
if not len(gp.layers):
|
||||
self.report({'WARNING'}, 'No layers on current GP object')
|
||||
return {'CANCELLED'}
|
||||
|
||||
active = gp.layers.active
|
||||
|
||||
if not active:
|
||||
self.report({'WARNING'}, 'No active layer to take keys from')
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.selected_frames = [f for f in active.frames if f.select]
|
||||
|
||||
if not self.selected_frames:
|
||||
self.report({'WARNING'}, 'No selected keys in active layer')
|
||||
return {'CANCELLED'}
|
||||
|
||||
wm = context.window_manager
|
||||
wm.invoke_search_popup(self) # can't specify size... width=500, height=600
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
|
||||
addon_keymaps.append((km,kmi))
|
||||
|
||||
# km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter)
|
||||
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True)
|
||||
kmi.properties.delete_source = True
|
||||
addon_keymaps.append((km,kmi))
|
||||
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
|
||||
def menu_duplicate_and_send_to_layer(self, context):
|
||||
if context.space_data.ui_mode == 'GPENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN'
|
||||
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
|
||||
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
|
||||
|
||||
classes = (
|
||||
GPTB_OT_duplicate_send_to_layer,
|
||||
)
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
register_keymaps()
|
||||
bpy.types.DOPESHEET_MT_key.append(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
|
||||
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
|
||||
unregister_keymaps()
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -1,6 +1,5 @@
|
|||
import bpy
|
||||
from .utils import get_addon_prefs, is_locked, is_hidden
|
||||
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
|
||||
from .utils import get_addon_prefs
|
||||
|
||||
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||
bl_idname = "screen.gp_keyframe_jump"
|
||||
|
@ -10,34 +9,19 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
next : BoolProperty(
|
||||
name="Next GP keyframe", description="Go to next active GP keyframe",
|
||||
default=True, options={'HIDDEN', 'SKIP_SAVE'})
|
||||
next : bpy.props.BoolProperty(
|
||||
name="Next GP keyframe", description="Go to next active GP keyframe", default=True)
|
||||
|
||||
target : EnumProperty(
|
||||
name="Target layer", description="Choose wich layer to evaluate for keyframe change",
|
||||
default='ACTIVE', options={'HIDDEN', 'SKIP_SAVE'},
|
||||
target : bpy.props.EnumProperty(
|
||||
name="Target layer", description="Choose wich layer to evaluate for keyframe change", default='ACTIVE',# options={'ANIMATABLE'}, update=None, get=None, set=None,
|
||||
items=(
|
||||
('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0),
|
||||
('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1),
|
||||
('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2),
|
||||
))
|
||||
|
||||
keyframe_type : EnumProperty(
|
||||
name="Keyframe Filter", description="Jump to choosen keyframe type, else use the UI jump filter",
|
||||
default='NONE', options={'HIDDEN', 'SKIP_SAVE'},
|
||||
items=(
|
||||
('NONE', 'Use UI Filter', '', 0), # 'KEYFRAME'
|
||||
('ALL', 'All', '', 1),
|
||||
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 2),
|
||||
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 3),
|
||||
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
|
||||
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
|
||||
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
|
||||
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
|
||||
))
|
||||
#(key, label, descr, id[, icon])
|
||||
|
||||
def execute(self, context):
|
||||
if not context.object.data.layers.active:
|
||||
|
@ -45,21 +29,16 @@ 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 is_hidden(l)]
|
||||
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
|
||||
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 is_hidden(l)]
|
||||
gpl = [l for l in context.object.data.layers if not l.hide]
|
||||
|
||||
elif self.target == 'ACCESSIBLE':
|
||||
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
|
||||
|
||||
if self.keyframe_type != 'NONE':
|
||||
# use shortcut choice override
|
||||
kftype = self.keyframe_type
|
||||
else:
|
||||
kftype = context.scene.gptoolprops.keyframe_type
|
||||
|
||||
current = context.scene.frame_current
|
||||
p = n = None
|
||||
|
@ -68,10 +47,6 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
maxs = []
|
||||
for l in gpl:
|
||||
for f in l.frames:
|
||||
# keyframe type filter
|
||||
if kftype != 'ALL' and f.keyframe_type != kftype:
|
||||
continue
|
||||
|
||||
if f.frame_number < current:
|
||||
p = f.frame_number
|
||||
if f.frame_number > current:
|
||||
|
@ -89,13 +64,10 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
if maxs:
|
||||
n = min(maxs)
|
||||
|
||||
## Double the frame set to avoid refresh problem (had one in 2.91.2)
|
||||
if self.next and n is not None:
|
||||
context.scene.frame_set(n)
|
||||
context.scene.frame_current = n
|
||||
elif not self.next and p is not None:
|
||||
context.scene.frame_set(p)
|
||||
context.scene.frame_current = p
|
||||
else:
|
||||
self.report({'INFO'}, 'No keyframe in this direction')
|
||||
return {"CANCELLED"}
|
||||
|
@ -108,10 +80,10 @@ class KFJ_OT_rebinder(bpy.types.Operator):
|
|||
bl_label = "rebind keyframe jump shortcut"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
s_keycode: StringProperty()
|
||||
s_ctrl: StringProperty()
|
||||
s_shift: StringProperty()
|
||||
s_alt: StringProperty()
|
||||
s_keycode: bpy.props.StringProperty()
|
||||
s_ctrl: bpy.props.StringProperty()
|
||||
s_shift: bpy.props.StringProperty()
|
||||
s_alt: bpy.props.StringProperty()
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
@ -172,13 +144,12 @@ 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_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))
|
||||
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))
|
||||
|
||||
def unregister_keymaps():
|
||||
# print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg
|
||||
|
|
|
@ -1,786 +0,0 @@
|
|||
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)
|
|
@ -1,205 +0,0 @@
|
|||
|
||||
import bpy
|
||||
import os
|
||||
import re
|
||||
from .utils import get_addon_prefs
|
||||
from .functions import redraw_ui
|
||||
from .__init__ import set_namespace_env
|
||||
|
||||
|
||||
class GPTB_OT_reset_project_namespaces(bpy.types.Operator):
|
||||
bl_idname = "gptb.reset_project_namespaces"
|
||||
bl_label = "Reload Project Names"
|
||||
bl_description = "Reset projects namespaced from environnement variable"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
prefix_list = os.getenv('PREFIXES')
|
||||
suffix_list = os.getenv('SUFFIXES')
|
||||
if not prefix_list and not suffix_list:
|
||||
self.report({'WARNING'}, "No name list in env (variables: 'PREFIXES','SUFFIXES')")
|
||||
return {'CANCELLED'}
|
||||
|
||||
for propname in ('prefixes', 'suffixes'):
|
||||
pg = getattr(prefs, propname)
|
||||
uilist = pg.namespaces
|
||||
uilist.clear()
|
||||
|
||||
missing = []
|
||||
if prefix_list:
|
||||
set_namespace_env('PREFIXES', prefs.prefixes)
|
||||
else:
|
||||
missing.append('prefixes')
|
||||
|
||||
if suffix_list:
|
||||
set_namespace_env('SUFFIXES', prefs.suffixes)
|
||||
else:
|
||||
missing.append('suffixes')
|
||||
|
||||
if missing:
|
||||
self.report({'WARNING'}, f'No {" and ".join(missing)} presets to load from project env')
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_add_namespace_entry(bpy.types.Operator):
|
||||
bl_idname = "gptb.add_namespace_entry"
|
||||
bl_label = "Add Namespace Entry"
|
||||
bl_description = "Add item in list"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
idx : bpy.props.IntProperty()
|
||||
new : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
|
||||
propname : bpy.props.StringProperty(default='prefixes', options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.pg = getattr(get_addon_prefs(), self.propname)
|
||||
self.proptype = self.propname[:-2]
|
||||
## Basic:
|
||||
# self.pg.namespaces.add()
|
||||
# return {'FINISHED'}# can just add empty entry and leave...
|
||||
if self.new:
|
||||
self.pg.namespaces.add()
|
||||
self.idx = len(self.pg.namespaces) - 1
|
||||
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# layout.use_property_split = True
|
||||
item = self.pg.namespaces[self.idx]
|
||||
layout.label(text=f'Enter {self.proptype.title()}:', icon='INFO')
|
||||
layout.prop(item, 'tag', text=self.proptype.title())
|
||||
if item.tag and not re.match(r'^[A-Z]{2}$', item.tag):
|
||||
layout.label(text=f'{self.propname.title()} are preferably two capital letter (ex: CO)', icon='ERROR')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text='Provide a name (Optional):', icon='INFO')
|
||||
layout.prop(item, 'name')
|
||||
|
||||
def execute(self, context):
|
||||
item = self.pg.namespaces[self.idx]
|
||||
## Here can perform post add checks
|
||||
# (check for duplicate ?)
|
||||
# all_prefix = [n.tag for i, n in enumerate(self.pg.namespaces) if i != self.pg.idx]
|
||||
|
||||
if self.new:
|
||||
# in case of new addition, remove just added if nothing specified
|
||||
if not item.tag and not item.name:
|
||||
self.pg.namespaces.remove(self.idx)
|
||||
|
||||
redraw_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_remove_namespace_entry(bpy.types.Operator):
|
||||
bl_idname = "gptb.remove_namespace_entry"
|
||||
bl_label = "Remove Namespace Entry"
|
||||
bl_description = "Remove item in list"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
propname : bpy.props.StringProperty(default='prefixes', options={'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
self.pg = getattr(get_addon_prefs(), self.propname)
|
||||
entry_count = len(self.pg.namespaces)
|
||||
if not entry_count:
|
||||
return {'CANCELLED'}
|
||||
# check if index is out of range
|
||||
if not (0 <= self.pg.idx < entry_count):
|
||||
self.report({"ERROR"}, 'Must select an entry to remove it')
|
||||
return {'CANCELLED'}
|
||||
|
||||
item = self.pg.namespaces[self.pg.idx]
|
||||
if item.is_project:
|
||||
self.report({"ERROR"}, 'Cannot remove a prefix that is defined by project, hide it instead')
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.pg.namespaces.remove(self.pg.idx)
|
||||
self.pg.idx -= 1
|
||||
redraw_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
class GPTB_OT_move_item(bpy.types.Operator):
|
||||
bl_idname = "gptb.move_item"
|
||||
bl_label = "Move Item"
|
||||
bl_description = "Move item in list up or down"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
# direction : bpy.props.IntProperty(default=1)
|
||||
direction : bpy.props.EnumProperty(
|
||||
items=(
|
||||
('UP', 'Move Up', 'Move up'),
|
||||
('DOWN', 'Move down', 'Move down'),
|
||||
),
|
||||
default='UP',
|
||||
|
||||
)
|
||||
propname : bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
pg = getattr(get_addon_prefs(), self.propname)
|
||||
uilist = pg.namespaces
|
||||
index = pg.idx
|
||||
|
||||
neighbor = index + (-1 if self.direction == 'UP' else 1)
|
||||
uilist.move(neighbor, index)
|
||||
list_length = len(uilist) - 1 # (index starts at 0)
|
||||
new_index = index + (-1 if self.direction == 'UP' else 1)
|
||||
list_index = max(0, min(new_index, list_length))
|
||||
|
||||
setattr(pg, 'idx', list_index)
|
||||
redraw_ui()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
# self.use_filter_show = True # force open/close the search feature
|
||||
# prefs = get_addon_prefs()
|
||||
# split = layout.split(align=False, factor=0.3)
|
||||
row = layout.row()
|
||||
hide_ico = 'HIDE_ON' if item.hide else 'HIDE_OFF'
|
||||
source_ico = 'NETWORK_DRIVE' if item.is_project else 'USER' # BLANK1
|
||||
|
||||
row.label(text='', icon=source_ico)
|
||||
row.prop(item, 'hide', text='', icon=hide_ico, invert_checkbox=True)
|
||||
subrow = row.row(align=True)
|
||||
subrow.prop(item, 'tag', text='')
|
||||
subrow.prop(item, 'name', text='')
|
||||
subrow.enabled = not item.is_project
|
||||
|
||||
# row = layout.split(align=False)
|
||||
# row.label(text=item.prefix)
|
||||
# row.label(text=item.name)
|
||||
|
||||
# if self.show_desc:
|
||||
# row.label(text=item.description)
|
||||
# row.operator('sbam.open_online_repo', text='', icon='URL')
|
||||
|
||||
class GPTB_UL_namespace_list(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname)
|
||||
|
||||
## Need to duplicate UL as a separate class for suffixes\
|
||||
## otherwise displayed row in UI are synchronised
|
||||
class GPTB_UL_namespace_list_suffix(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
draw_namespace_item(self, context, layout, data, item, icon, active_data, active_propname)
|
||||
|
||||
|
||||
classes = (
|
||||
## layer name management
|
||||
GPTB_OT_reset_project_namespaces,
|
||||
GPTB_OT_add_namespace_entry,
|
||||
GPTB_OT_remove_namespace_entry,
|
||||
GPTB_OT_move_item,
|
||||
GPTB_UL_namespace_list,
|
||||
GPTB_UL_namespace_list_suffix,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
152
OP_layer_nav.py
152
OP_layer_nav.py
|
@ -1,152 +0,0 @@
|
|||
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)
|
|
@ -1,168 +0,0 @@
|
|||
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)
|
|
@ -1,183 +0,0 @@
|
|||
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)
|
|
@ -1,346 +0,0 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from time import time
|
||||
from .utils import (get_gp_draw_plane,
|
||||
location_to_region,
|
||||
region_to_location,
|
||||
is_locked,
|
||||
is_hidden)
|
||||
|
||||
|
||||
### passing by 2D projection
|
||||
def get_3d_coord_on_drawing_plane_from_2d(context, co):
|
||||
plane_co, plane_no = get_gp_draw_plane()
|
||||
rv3d = context.region_data
|
||||
view_mat = rv3d.view_matrix.inverted()
|
||||
if not plane_no:
|
||||
plane_no = Vector((0,0,1))
|
||||
plane_no.rotate(view_mat)
|
||||
depth_3d = view_mat @ Vector((0, 0, -1000))
|
||||
org = region_to_location(co, view_mat.to_translation())
|
||||
view_point = region_to_location(co, depth_3d)
|
||||
hit = geometry.intersect_line_plane(org, view_point, plane_co, plane_no)
|
||||
|
||||
if hit and plane_no:
|
||||
return context.object, hit, plane_no
|
||||
|
||||
return None, None, None
|
||||
|
||||
"""
|
||||
class GP_OT_pick_closest_material(Operator):
|
||||
bl_idname = "gp.pick_closest_material"
|
||||
bl_label = "Get Closest Stroke Material"
|
||||
bl_description = "Pick closest stroke material"
|
||||
bl_options = {"REGISTER"} # , "UNDO"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
def filter_stroke(self, context):
|
||||
# get stroke under mouse using kdtree
|
||||
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||
|
||||
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||
for i, pair in enumerate(point_pair):
|
||||
kd.insert(pair[0], i)
|
||||
kd.balance()
|
||||
|
||||
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing
|
||||
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||
|
||||
if not hit:
|
||||
return 'No hit on drawing plane', None
|
||||
|
||||
mouse_3d = hit
|
||||
mouse_local = self.inv_mat @ mouse_3d # local space
|
||||
co, index, _dist = kd.find(mouse_local) # local space
|
||||
# co, index, _dist = kd.find(mouse_3d) # world space
|
||||
# context.scene.cursor.location = co # world space
|
||||
s = point_pair[index][1]
|
||||
|
||||
## find point index in stroke
|
||||
self.idx = None
|
||||
for i, p in enumerate(s.points):
|
||||
if p.position == co:
|
||||
self.idx = i
|
||||
break
|
||||
|
||||
del point_pair
|
||||
return s, self.ob.matrix_world @ co
|
||||
|
||||
def invoke(self, context, event):
|
||||
# self.prefs = get_addon_prefs()
|
||||
self.ob = context.object
|
||||
self.gp = self.ob.data
|
||||
|
||||
self.stroke_list = []
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for l in self.gp.layers:
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
else:
|
||||
# [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||
for l in self.gp.layers:
|
||||
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||
continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
if self.fill_only:
|
||||
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
|
||||
|
||||
if not self.stroke_list:
|
||||
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
self.stroke, self.coord = self.filter_stroke(context)
|
||||
if isinstance(self.stroke, str):
|
||||
self.report({'ERROR'}, self.stroke)
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
del self.stroke_list
|
||||
|
||||
if self.idx is None:
|
||||
self.report({'WARNING'}, 'No coord found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position
|
||||
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
|
||||
## directly use world position ?
|
||||
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
|
||||
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||
self.plen = len(self.stroke.points)
|
||||
|
||||
# context.scene.cursor.location = self.coord #Dbg
|
||||
return self.execute(context)
|
||||
# context.window_manager.modal_handler_add(self)
|
||||
# return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
self.ob.active_material_index = self.stroke.material_index
|
||||
# self.report({'INFO'}, f'Mat: {self.ob.data.materials[self.stroke.material_index].name}')
|
||||
return {'FINISHED'}
|
||||
|
||||
# def modal(self, context, event):
|
||||
# if event.type == 'MOUSEMOVE':
|
||||
# mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
# delta = mouse - self.init_mouse
|
||||
|
||||
# if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
||||
# print(f'{self.stroke}, points num {len(self.stroke.points)}, material index:{self.stroke.material_index}')
|
||||
# return {'FINISHED'}
|
||||
|
||||
# if event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
# # for i, p in enumerate(self.stroke.points): # reset position
|
||||
# # self.stroke.points[i].position = self.init_pos[i]
|
||||
# context.area.tag_redraw()
|
||||
# return {'CANCELLED'}
|
||||
|
||||
# return {'RUNNING_MODAL'}
|
||||
"""
|
||||
|
||||
class GP_OT_pick_closest_material(Operator):
|
||||
bl_idname = "gp.pick_closest_material"
|
||||
bl_label = "Get Closest Stroke Material"
|
||||
bl_description = "Pick closest stroke material"
|
||||
bl_options = {"REGISTER"} # , "UNDO"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
stroke_filter : bpy.props.EnumProperty(default='FILL',
|
||||
items=(
|
||||
('FILL', 'Fill', 'Target only Fill materials', 0),
|
||||
('STROKE', 'Stroke', 'Target only Stroke materials', 1),
|
||||
('ALL', 'All', 'All material', 2),
|
||||
),
|
||||
options={'SKIP_SAVE'})
|
||||
|
||||
def filter_stroke(self, context):
|
||||
# get stroke under mouse using kdtree
|
||||
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||
|
||||
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||
for i, pair in enumerate(point_pair):
|
||||
kd.insert(pair[0], i)
|
||||
kd.balance()
|
||||
|
||||
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
|
||||
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||
|
||||
if not hit:
|
||||
return 'No hit on drawing plane', None
|
||||
|
||||
mouse_3d = hit
|
||||
mouse_local = self.inv_mat @ mouse_3d # local space
|
||||
co, index, _dist = kd.find(mouse_local) # local space
|
||||
# co, index, _dist = kd.find(mouse_3d) # world space
|
||||
# context.scene.cursor.location = co # world space
|
||||
s = point_pair[index][1]
|
||||
|
||||
## find point index in stroke
|
||||
self.idx = None
|
||||
for i, p in enumerate(s.points):
|
||||
if p.position == co:
|
||||
self.idx = i
|
||||
break
|
||||
|
||||
del point_pair
|
||||
return s, self.ob.matrix_world @ co
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.t0 = time()
|
||||
self.limit = self.t0 + 0.2 # 200 miliseconds
|
||||
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
self.idx = None
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
if time() > self.limit:
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.value == 'RELEASE': # if a key was release (any key in case shortcut was customised)
|
||||
if time() > self.limit:
|
||||
# dont know if condition is neeed
|
||||
return {'CANCELLED'}
|
||||
|
||||
return self.execute(context)
|
||||
# return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
# return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# self.prefs = get_addon_prefs()
|
||||
self.ob = context.object
|
||||
gp = self.ob.data
|
||||
|
||||
self.stroke_list = []
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for l in gp.layers:
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
else:
|
||||
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||
for l in gp.layers:
|
||||
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||
continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
if self.stroke_filter == 'FILL':
|
||||
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
|
||||
elif self.stroke_filter == 'STROKE':
|
||||
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_stroke]
|
||||
# else ALL (no filter)
|
||||
|
||||
if not self.stroke_list:
|
||||
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
stroke, self.coord = self.filter_stroke(context)
|
||||
if isinstance(stroke, str):
|
||||
self.report({'ERROR'}, stroke)
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
del self.stroke_list
|
||||
|
||||
if self.idx is None:
|
||||
self.report({'WARNING'}, 'No coord found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position
|
||||
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated
|
||||
# self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||
# self.plen = len(stroke.points)
|
||||
|
||||
self.ob.active_material_index = stroke.material_index
|
||||
## debug show trigger time
|
||||
# print(f'Trigger time {time() - self.t0:.3f}')
|
||||
self.report({'INFO'}, f'Mat: {self.ob.data.materials[stroke.material_index].name}')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW')
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
idname="gp.pick_closest_material",
|
||||
type="S", # type="LEFTMOUSE",
|
||||
value="PRESS",
|
||||
# key_modifier='S', # S like Sample
|
||||
)
|
||||
kmi.properties.stroke_filter = 'FILL'
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
idname="gp.pick_closest_material",
|
||||
type="S", # type="LEFTMOUSE",
|
||||
value="PRESS",
|
||||
alt = True,
|
||||
# key_modifier='S', # S like Sample
|
||||
)
|
||||
kmi.properties.stroke_filter = 'STROKE'
|
||||
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
|
||||
classes=(
|
||||
GP_OT_pick_closest_material,
|
||||
)
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
unregister_keymaps()
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
201
OP_palettes.py
201
OP_palettes.py
|
@ -5,6 +5,7 @@ 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):
|
||||
|
@ -42,7 +43,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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# Start Clean (delete unuesed sh*t)
|
||||
|
@ -82,7 +83,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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
filename_ext = '.json'
|
||||
|
||||
|
@ -110,7 +111,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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||
|
||||
|
@ -129,8 +130,6 @@ 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:
|
||||
|
@ -166,10 +165,12 @@ 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 != 'GREASEPENCIL':
|
||||
if ob.type != 'GPENCIL':
|
||||
print(f'{ob.name} not a GP object')
|
||||
continue
|
||||
|
||||
|
@ -224,7 +225,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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
|
@ -252,7 +253,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 == 'GREASEPENCIL'
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
|
@ -260,7 +261,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 == 'GREASEPENCIL' and o != ob]
|
||||
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
|
||||
|
||||
if not selection:
|
||||
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
|
||||
|
@ -281,192 +282,12 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
|||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
||||
bl_idname = "gp.clean_material_stack"
|
||||
bl_label = "Clean Material Stack"
|
||||
bl_description = "Clean materials duplication in active GP object stack"
|
||||
bl_options = {"REGISTER", "UNDO"} # , "INTERNAL"
|
||||
|
||||
|
||||
|
||||
use_clean_mats : bpy.props.BoolProperty(name="Remove Duplication",
|
||||
description="All duplicated material (with suffix .001, .002 ...) will be replaced by the material with clean name (if found in scene)" ,
|
||||
default=True)
|
||||
|
||||
skip_different_materials : bpy.props.BoolProperty(name="Skip Different Material",
|
||||
description="Will not touch duplication if color settings are different (and show infos about skipped materials)",
|
||||
default=True)
|
||||
|
||||
use_fuses_mats : bpy.props.BoolProperty(name="Fuse Materials Slots",
|
||||
description="Fuse materials slots when multiple uses same materials",
|
||||
default=True)
|
||||
|
||||
remove_empty_slots : bpy.props.BoolProperty(name="Remove Empty Slots",
|
||||
description="Remove slots that haven't any material attached ",
|
||||
default=True)
|
||||
|
||||
# skip_binded_empty_slots : bpy.props.BoolProperty(name="Skip Binded Empty slots",
|
||||
# description="Remove only empty slots that haven't any material attached",
|
||||
# default=False)
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ob = context.object
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
box = layout.box()
|
||||
box.prop(self, 'use_clean_mats')
|
||||
if self.use_clean_mats:
|
||||
box.prop(self, 'skip_different_materials')
|
||||
|
||||
# layout.separator()
|
||||
box = layout.box()
|
||||
box.prop(self, 'use_fuses_mats')
|
||||
box = layout.box()
|
||||
box.prop(self, 'remove_empty_slots')
|
||||
# if self.remove_empty_slots:
|
||||
# box.prop(self, 'skip_binded_empty_slots')
|
||||
|
||||
|
||||
def different_gp_mat(self, mata, matb):
|
||||
a = mata.grease_pencil
|
||||
b = matb.grease_pencil
|
||||
if a.color[:] != b.color[:]:
|
||||
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke color is different'
|
||||
if a.fill_color[:] != b.fill_color[:]:
|
||||
return f'! {self.ob.name}: {mata.name} and {matb.name} fill_color color is different'
|
||||
if a.show_stroke != b.show_stroke:
|
||||
return f'! {self.ob.name}: {mata.name} and {matb.name} stroke has different state'
|
||||
if a.show_fill != b.show_fill:
|
||||
return f'! {self.ob.name}: {mata.name} and {matb.name} fill has different state'
|
||||
|
||||
## Clean dups
|
||||
|
||||
def clean_mats_duplication(self, ob):
|
||||
import re
|
||||
diff_ct = 0
|
||||
todel = []
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not hasattr(ob, 'material_slots'):
|
||||
return
|
||||
for i, ms in enumerate(ob.material_slots):
|
||||
mat = ms.material
|
||||
if not mat:
|
||||
continue
|
||||
match = re.search(r'(.*)\.\d{3}$', mat.name)
|
||||
if not match:
|
||||
continue
|
||||
basemat = bpy.data.materials.get(match.group(1))
|
||||
if not basemat:
|
||||
continue
|
||||
diff = self.different_gp_mat(mat, basemat)
|
||||
if diff:
|
||||
print(diff)
|
||||
diff_ct += 1
|
||||
if self.skip_different_materials:
|
||||
continue
|
||||
|
||||
if mat not in todel:
|
||||
todel.append(mat)
|
||||
ms.material = basemat
|
||||
print(f'{ob.name} : slot {i} >> replaced {mat.name}')
|
||||
mat.use_fake_user = False
|
||||
|
||||
### delete (only when using on all objects loop, else can delete another objects mat...)
|
||||
## for m in reversed(todel):
|
||||
## bpy.data.materials.remove(m)
|
||||
|
||||
if diff_ct:
|
||||
return('INFO', f'{diff_ct} mat skipped >> same name but different color settings!')
|
||||
|
||||
## fuse
|
||||
|
||||
def fuse_object_mats(self, ob):
|
||||
for i in range(len(ob.material_slots))[::-1]:
|
||||
ms = ob.material_slots[i]
|
||||
mat = ms.material
|
||||
# if not mat:
|
||||
# # remove empty slots
|
||||
# if self.remove_empty_slots:
|
||||
# ob.active_material_index = i
|
||||
# bpy.ops.object.material_slot_remove()
|
||||
# continue
|
||||
|
||||
# update mat list
|
||||
mlist = [ms.material for ms in ob.material_slots if ms.material]
|
||||
if mlist.count(mat) > 1:
|
||||
# get first material in list
|
||||
new_mat_id = mlist.index(mat)
|
||||
|
||||
# iterate in all strokes and replace with new_mat_id
|
||||
for l in ob.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.drawing.strokes:
|
||||
if s.material_index == i:
|
||||
s.material_index = new_mat_id
|
||||
|
||||
# delete slot (or add to the remove_slot list
|
||||
ob.active_material_index = i
|
||||
bpy.ops.object.material_slot_remove()
|
||||
|
||||
def delete_empty_material_slots(self, ob):
|
||||
for i in range(len(ob.material_slots))[::-1]:
|
||||
ms = ob.material_slots[i]
|
||||
mat = ms.material
|
||||
if not mat:
|
||||
# is_binded=False
|
||||
# if self.skip_binded_empty_slots:
|
||||
# for l in ob.data.layers:
|
||||
# for f in l.frames:
|
||||
# for s in f.drawing.strokes:
|
||||
# if s.material_index == i:
|
||||
# is_binded = True
|
||||
# break
|
||||
# if is_binded:
|
||||
# continue
|
||||
|
||||
ob.active_material_index = i
|
||||
bpy.ops.object.material_slot_remove()
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
info = None
|
||||
|
||||
if not self.use_clean_mats and not self.use_fuses_mats and not self.remove_empty_slots:
|
||||
self.report({'ERROR'}, 'At least one operation should be selected')
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.use_clean_mats:
|
||||
info = self.clean_mats_duplication(ob)
|
||||
if self.use_fuses_mats:
|
||||
self.fuse_object_mats(ob)
|
||||
if self.remove_empty_slots:
|
||||
self.delete_empty_material_slots(ob)
|
||||
|
||||
if info:
|
||||
self.report({info[0]}, info[1])
|
||||
# else:
|
||||
# self.report({'WARNING'}, '')
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
classes = (
|
||||
GPTB_OT_load_palette,
|
||||
GPTB_OT_save_palette,
|
||||
GPTB_OT_load_default_palette,
|
||||
GPTB_OT_load_blend_palette,
|
||||
GPTB_OT_copy_active_to_selected_palette,
|
||||
GPTB_OT_clean_material_stack,
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
|
|
|
@ -1,581 +0,0 @@
|
|||
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,8 +40,6 @@ exclude = (
|
|||
'audio_bitrate',
|
||||
]
|
||||
"""
|
||||
|
||||
'''
|
||||
def render_with_restore():
|
||||
class RenderFileRestorer:
|
||||
rd = bpy.context.scene.render
|
||||
|
@ -82,66 +80,26 @@ 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():
|
||||
### can add propeties for personalisation as toolsetting props
|
||||
|
||||
rd.resolution_percentage = res_factor
|
||||
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage
|
||||
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
|
||||
rd.resolution_x = rd.resolution_x + 1
|
||||
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage
|
||||
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
|
||||
rd.resolution_y = rd.resolution_y + 1
|
||||
|
||||
rd.image_settings.file_format = 'FFMPEG'
|
||||
ff.format = 'MPEG4'
|
||||
ff.codec = 'H264'
|
||||
ff.constant_rate_factor = 'HIGH' # MEDIUM
|
||||
ff.constant_rate_factor = 'HIGH'# MEDIUM
|
||||
ff.ffmpeg_preset = 'REALTIME'
|
||||
ff.gopsize = 10
|
||||
ff.audio_codec = 'AAC'
|
||||
|
@ -158,18 +116,13 @@ 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"
|
||||
|
||||
## 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)
|
||||
fp = join(blend.parent, "images", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
|
||||
|
||||
#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 = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
|
||||
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
|
||||
|
||||
# bpy.ops.render.render_wrap(use_view=viewport)
|
||||
### render
|
||||
|
|
|
@ -21,19 +21,14 @@ exclude = (
|
|||
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
|
||||
)
|
||||
|
||||
def delete_file(filepath):
|
||||
fp = Path(filepath)
|
||||
if fp.exists() and fp.is_file():
|
||||
def delete_file(filepath) :
|
||||
try:
|
||||
print('removing', fp)
|
||||
fp.unlink(missing_ok=False)
|
||||
# os.remove(fp)
|
||||
if os.path.isfile(filepath) :
|
||||
print('removing', filepath)
|
||||
os.remove(filepath)
|
||||
return True
|
||||
except PermissionError:
|
||||
print(f'impossible to remove (permission error): {fp}')
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print(f'Impossible to remove (file not found error): {fp}')
|
||||
print(f'impossible to remove {filepath}')
|
||||
return False
|
||||
|
||||
# render function
|
||||
|
@ -63,7 +58,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 = int(frame_count / total_frame * 100)
|
||||
bpy.context.window_manager.pblast_completion = frame_count / total_frame * 100
|
||||
except AttributeError :
|
||||
#debug
|
||||
if debug : print("AttributeError avoided")
|
||||
|
@ -236,7 +231,9 @@ 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
|
||||
|
@ -269,38 +266,6 @@ 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):
|
||||
|
@ -309,7 +274,6 @@ 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
|
||||
|
@ -352,20 +316,16 @@ def playblast(context, viewport = False, stamping = True):
|
|||
# rd.is_movie_format# check if its movie mode
|
||||
|
||||
## set filepath
|
||||
# mode incremental or just use fulldate (cannot create conflict and filter OK but long name)
|
||||
date_format = "%Y-%m-%d_%H-%M-%S"
|
||||
## old
|
||||
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
|
||||
blend = Path(bpy.data.filepath)
|
||||
# fp = blend.parent / "playblast" / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4'
|
||||
date_format = "%Y-%m-%d_%H-%M-%S"
|
||||
fp = join(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 = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage
|
||||
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
|
||||
|
||||
|
||||
# get total number of frames
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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):
|
||||
'''
|
||||
|
@ -74,7 +76,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
# namespace_order
|
||||
namespaces=[]
|
||||
for l in gpl:
|
||||
ns= l.name.lower().split(separator, 1)[0]
|
||||
ns= l.info.lower().split(separator, 1)[0]
|
||||
if ns not in namespaces:
|
||||
namespaces.append(ns)
|
||||
|
||||
|
@ -88,14 +90,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.name.lower() not in ('background',):
|
||||
if l.info.lower() not in ('background',):
|
||||
print()
|
||||
print('>', l.name)
|
||||
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
|
||||
print('>', l.info)
|
||||
ns= l.info.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.name == individuels
|
||||
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
|
||||
|
||||
else:
|
||||
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
|
||||
|
@ -125,10 +127,3 @@ 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)
|
437
OP_realign.py
437
OP_realign.py
|
@ -1,437 +0,0 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
import numpy as np
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
from math import pi
|
||||
from time import time
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
from . import utils
|
||||
from .utils import is_hidden, is_locked
|
||||
|
||||
def get_scale_matrix(scale):
|
||||
# recreate a neutral mat scale
|
||||
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
||||
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
||||
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
||||
matscale = matscale_x @ matscale_y @ matscale_z
|
||||
return matscale
|
||||
|
||||
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject - ops method
|
||||
:all_stroke: affect hidden, locked layers
|
||||
'''
|
||||
|
||||
if restore_frame:
|
||||
oframe = bpy.context.scene.frame_current
|
||||
|
||||
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
|
||||
|
||||
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
|
||||
frame_list = list(set(frame_list))
|
||||
frame_list.sort()
|
||||
|
||||
scn = bpy.context.scene
|
||||
for i in frame_list:
|
||||
scn.frame_set(i) # refresh scene
|
||||
# scn.frame_current = i # no refresh
|
||||
|
||||
origin = scn.camera.matrix_world.to_translation()
|
||||
matrix_inv = obj.matrix_world.inverted()
|
||||
# origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
|
||||
# matrix = np.array(obj.matrix_world, dtype='float64')
|
||||
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
|
||||
#mat = src.matrix_world
|
||||
for layer in obj.data.layers:
|
||||
if not all_strokes:
|
||||
if not layer.select:
|
||||
continue
|
||||
if is_hidden(layer) or is_locked(layer):
|
||||
continue
|
||||
|
||||
frame = next((f for f in layer.frames if f.frame_number == i), None)
|
||||
if frame is None:
|
||||
print(layer.name, 'Not found')
|
||||
# FIXME: some strokes are ignored
|
||||
# print(frame'skip {layer.name}, no frame at {i}')
|
||||
continue
|
||||
|
||||
for s in frame.drawing.strokes:
|
||||
# print(layer.name, s.material_index)
|
||||
|
||||
## Batch matrix apply (Here is slower than list comprehension).
|
||||
# nb_points = len(s.points)
|
||||
# coords = np.empty(nb_points * 3, dtype='float64')
|
||||
# s.points.foreach_get('co', coords)
|
||||
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
|
||||
|
||||
## list comprehension method
|
||||
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
|
||||
|
||||
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
|
||||
|
||||
# Basic method (Slower than foreach_set and compatible with GPv3)
|
||||
## TODO: use low level api with curve offsets...
|
||||
for pt_index, point in enumerate(s.points):
|
||||
point.position = matrix_inv @ new_world_co_3d[pt_index]
|
||||
|
||||
## GPv2: ravel and use foreach_set
|
||||
## Ravel new coordinate on the fly
|
||||
## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
|
||||
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
|
||||
# s.points.foreach_set('co', new_local_coords)
|
||||
|
||||
if restore_frame:
|
||||
bpy.context.scene.frame_current = oframe
|
||||
|
||||
## Update the layer and redraw all viewports
|
||||
obj.data.layers.update()
|
||||
utils.refresh_areas()
|
||||
|
||||
def align_global(reproject=True, ref=None, all_strokes=True):
|
||||
|
||||
if not ref:
|
||||
ref = bpy.context.scene.camera
|
||||
|
||||
o = bpy.context.object
|
||||
# if o.matrix_basis != o.matrix_world and not o.parent:
|
||||
|
||||
ref = bpy.context.scene.camera
|
||||
ref_mat = ref.matrix_world
|
||||
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||
|
||||
if o.parent:
|
||||
mat = o.matrix_world
|
||||
else:
|
||||
mat = o.matrix_basis
|
||||
|
||||
o_loc, o_rot, o_scale = mat.decompose()
|
||||
|
||||
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||
|
||||
loc_mat = Matrix.Translation(o_loc)
|
||||
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||
scale_mat = get_scale_matrix(o_scale)
|
||||
|
||||
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||
|
||||
# world_coords = []
|
||||
for l in o.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
|
||||
## GPv2
|
||||
# s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
# # s.points.update() # seem to works # but adding/deleting a point is "safer"
|
||||
# ## force update
|
||||
# s.points.add(1)
|
||||
# s.points.pop()
|
||||
|
||||
for p in s.points:
|
||||
## GOOD :
|
||||
# world_co = mat @ p.position
|
||||
# p.position = new_mat.inverted() @ world_co
|
||||
|
||||
## GOOD :
|
||||
p.position = p.position @ mat.inverted() @ new_mat
|
||||
|
||||
if o.parent:
|
||||
o.matrix_world = new_mat
|
||||
else:
|
||||
o.matrix_basis = new_mat
|
||||
|
||||
if reproject:
|
||||
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
|
||||
|
||||
|
||||
def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
||||
|
||||
print('aligning all frames...')
|
||||
|
||||
o = bpy.context.object
|
||||
if not ref:
|
||||
ref = bpy.context.scene.camera
|
||||
|
||||
# get all rot
|
||||
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
||||
|
||||
## double list keys
|
||||
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||
|
||||
## normal iter
|
||||
# for fcu in o.animation_data.action.fcurves:
|
||||
# if fcu.data_path != chanel :
|
||||
# continue
|
||||
# for k in fcu.keyframe_points():
|
||||
# rot_keys.append(k.co.x)
|
||||
|
||||
rot_keys = list(set(rot_keys))
|
||||
|
||||
# TODO # TOTHINK
|
||||
# for now the rotation of the object is adjusted at every frame....
|
||||
# might be better to check camera rotation of the current frame only (stored as copy).
|
||||
# else the object rotate following the cameraview vector (not constant)...
|
||||
|
||||
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||
|
||||
for l in o.data.layers:
|
||||
for f in l.frames:
|
||||
# set the frame to dedicated
|
||||
bpy.context.scene.frame_set(f.frame_number)
|
||||
|
||||
ref_mat = ref.matrix_world
|
||||
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||
|
||||
if o.parent:
|
||||
mat = o.matrix_world
|
||||
else:
|
||||
mat = o.matrix_basis
|
||||
|
||||
o_loc, o_rot, o_scale = mat.decompose()
|
||||
loc_mat = Matrix.Translation(o_loc)
|
||||
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||
scale_mat = get_scale_matrix(o_scale)
|
||||
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
# print('coords: ', coords)
|
||||
# print([co for v in coords for co in v])
|
||||
s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
# s.points.update() # seem to works
|
||||
## force update
|
||||
s.points.add(1)
|
||||
s.points.pop()
|
||||
|
||||
for fnum in rot_keys:
|
||||
bpy.context.scene.frame_set(fnum)
|
||||
#/update calculation block
|
||||
ref_mat = ref.matrix_world
|
||||
ref_loc, ref_rot, ref_scale = ref_mat.decompose()
|
||||
|
||||
if o.parent:
|
||||
mat = o.matrix_world
|
||||
else:
|
||||
mat = o.matrix_basis
|
||||
|
||||
o_loc, o_rot, o_scale = mat.decompose()
|
||||
loc_mat = Matrix.Translation(o_loc)
|
||||
rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90
|
||||
scale_mat = get_scale_matrix(o_scale)
|
||||
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||
# update calculation block/
|
||||
|
||||
if o.parent:
|
||||
o.matrix_world = new_mat
|
||||
else:
|
||||
o.matrix_basis = new_mat
|
||||
|
||||
o.keyframe_insert(chanel, index=-1, frame=bpy.context.scene.frame_current, options={'INSERTKEY_AVAILABLE'})
|
||||
|
||||
|
||||
if reproject:
|
||||
batch_reproject(o, proj_type='FRONT', all_strokes=all_strokes)
|
||||
|
||||
return
|
||||
|
||||
|
||||
class GPTB_OT_realign(bpy.types.Operator):
|
||||
bl_idname = "gp.realign"
|
||||
bl_label = "Realign GP"
|
||||
bl_description = "Realign the grease pencil front axis with active camera"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
reproject : bpy.props.BoolProperty(
|
||||
name='Reproject', default=True,
|
||||
description='Reproject stroke on the new alignment')
|
||||
|
||||
all_strokes : bpy.props.BoolProperty(
|
||||
name='All Strokes', default=True,
|
||||
description='Hided and locked layer will also be reprojected')
|
||||
|
||||
set_draw_axis : bpy.props.BoolProperty(
|
||||
name='Set draw axis to Front', default=True,
|
||||
description='Set the gpencil draw plane axis to Front')
|
||||
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multiframe mode')
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.alert = ''
|
||||
o = context.object
|
||||
if o.animation_data and o.animation_data.action:
|
||||
act = o.animation_data.action
|
||||
for chan in ('rotation_euler', 'rotation_quaternion'):
|
||||
if act.fcurves.find(chan):
|
||||
self.alert = 'Animated Rotation (CONSTANT interpolation)'
|
||||
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
|
||||
if interpos:
|
||||
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
|
||||
break
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text='Realign the GP object : Front axis facing active camera')
|
||||
if self.alert:
|
||||
layout.label(text=self.alert, icon='ERROR')
|
||||
layout.label(text='(rotations key will be overwritten to face camera)')
|
||||
|
||||
# layout.separator()
|
||||
box = layout.box()
|
||||
box.prop(self, "reproject")
|
||||
if self.reproject:
|
||||
box.label(text='After Realigning, reproject each frames on front axis')
|
||||
if not context.region_data.view_perspective == 'CAMERA':
|
||||
box.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||
box.prop(self, "all_strokes")
|
||||
if not self.all_strokes:
|
||||
box.label(text='Only visible and unlocked layers will be reprojected', icon='INFO')
|
||||
|
||||
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
|
||||
if axis != 'AXIS_Y':
|
||||
orient = {
|
||||
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
|
||||
# 'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], #
|
||||
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'],
|
||||
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'],
|
||||
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
|
||||
}
|
||||
|
||||
box = layout.box()
|
||||
box.label(text=f'Current drawing plane : {orient[axis][0]}', icon=orient[axis][1])
|
||||
box.prop(self, "set_draw_axis")
|
||||
|
||||
|
||||
def exit(self, context, frame):
|
||||
context.scene.frame_current = frame
|
||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y' and self.set_draw_axis:
|
||||
context.scene.tool_settings.gpencil_sculpt.lock_axis = 'AXIS_Y'
|
||||
|
||||
def execute(self, context):
|
||||
t0 = time()
|
||||
oframe = context.scene.frame_current
|
||||
|
||||
o = bpy.context.object
|
||||
if o.animation_data and o.animation_data.action:
|
||||
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'):
|
||||
align_all_frames(reproject=self.reproject)
|
||||
print(f'\nAnim realign ({time()-t0:.2f}s)')
|
||||
self.exit(context, oframe)
|
||||
return {"FINISHED"}
|
||||
|
||||
align_global(reproject=self.reproject)
|
||||
print(f'\nGlobal Realign ({time()-t0:.2f}s)')
|
||||
self.exit(context, oframe)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
||||
bl_idname = "gp.batch_reproject_all_frames"
|
||||
bl_label = "Reproject All Frames"
|
||||
bl_description = "Reproject all frames of active object."
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
all_strokes : bpy.props.BoolProperty(
|
||||
name='All Strokes', default=True,
|
||||
description='Hided and locked layer will also be reprojected')
|
||||
|
||||
type : bpy.props.EnumProperty(name='Type',
|
||||
items=(('CURRENT', "Current", ""),
|
||||
('FRONT', "Front", ""),
|
||||
('SIDE', "Side", ""),
|
||||
('TOP', "Top", ""),
|
||||
('VIEW', "View", ""),
|
||||
('CURSOR', "Cursor", ""),
|
||||
# ('SURFACE', "Surface", ""),
|
||||
),
|
||||
default='CURRENT')
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multi-edit')
|
||||
return {"CANCELLED"}
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not context.region_data.view_perspective == 'CAMERA':
|
||||
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
|
||||
layout.label(text='Reprojection is made from camera', icon='ERROR')
|
||||
layout.prop(self, "all_strokes")
|
||||
layout.prop(self, "type", text='Project Axis')
|
||||
|
||||
## Hint show axis
|
||||
if self.type == 'CURRENT':
|
||||
## Show as prop
|
||||
# row = layout.row()
|
||||
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
|
||||
# row.enabled = False
|
||||
|
||||
orient = {
|
||||
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
|
||||
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
|
||||
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
|
||||
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
|
||||
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
|
||||
}
|
||||
box = layout.box()
|
||||
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
|
||||
box.label(text=orient[axis][0], icon=orient[axis][1])
|
||||
|
||||
def execute(self, context):
|
||||
t0 = time()
|
||||
orient = self.type
|
||||
if self.type == 'CURRENT':
|
||||
orient = None
|
||||
|
||||
batch_reproject(context.object, proj_type=orient, all_strokes=self.all_strokes, restore_frame=True)
|
||||
|
||||
self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' )
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
### -- MENU ENTRY --
|
||||
|
||||
def reproject_clean_menu(self, context):
|
||||
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
def reproject_context_menu(self, context):
|
||||
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
classes = (
|
||||
GPTB_OT_realign,
|
||||
GPTB_OT_batch_reproject_all_frames,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu)
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
|
@ -0,0 +1,482 @@
|
|||
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,6 +151,10 @@ def register():
|
|||
bpy.utils.register_class(GPTB_OT_sticky_cutter)
|
||||
# register_keymaps()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
# unregister_keymaps()
|
||||
|
|
327
README.md
327
README.md
|
@ -2,18 +2,11 @@
|
|||
|
||||
Blender addon - Various tool to help with grease pencil in animation productions.
|
||||
|
||||
### /!\ Main branch is currently broken, in migration to gpv3
|
||||
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
|
||||
|
||||
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||
<!-- ### [Demo Youtube]() -->
|
||||
|
||||
**[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
|
||||
|
||||
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||
|
||||
**[Readme Doc in French](README_FR.md)**
|
||||
|
||||
It is recommended to enable _Grease Pencil Tools_ native Blender addon. Brings essentials features:
|
||||
Canvas rotation, Box deform, Timeline scrubbing in viewport, Quick layer navigator.
|
||||
**[Readme Doc in French (Documentation en Français et plus détaillée)](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/blob/master/README_FR.md)**
|
||||
|
||||
---
|
||||
|
||||
|
@ -36,38 +29,9 @@ 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
|
||||
|
||||
An "on save" Handler that trigger relative remap of all path can be enabled in addon prefs (disabled by default).
|
||||
Add an "on save" Handler that trigger relative remap of all path.
|
||||
|
||||
### function
|
||||
|
||||
|
@ -107,7 +71,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**
|
||||
|
@ -120,44 +84,11 @@ 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
|
||||
|
||||
<!--
|
||||
|
||||
## Todo:
|
||||
|
||||
- Allow to render resolution from cam name
|
||||
|
@ -174,9 +105,251 @@ Panel in sidebar : 3D view > sidebar 'N' > Gpencil
|
|||
|
||||
- Move automatically view to match GP Front (depending on Gpencil view settings)
|
||||
|
||||
- move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures) -->
|
||||
- move GP keyframes selection and Object keyframe selection simultaneouly (Already Done by Tom Viguier at [Andarta](https://gitlab.com/andarta-pictures)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Consult [Changelog here](CHANGELOG.md)
|
||||
## Changelog:
|
||||
|
||||
1.0.3:
|
||||
|
||||
- feat: add "Append Materials To Selected" to material submenu. Append materials to other selected GP objects if there aren't there.
|
||||
|
||||
1.0.2:
|
||||
|
||||
- pref: Added option to disable always remap relative on save in addon-preference
|
||||
|
||||
1.0.1:
|
||||
|
||||
- fix: copy paste problems
|
||||
- Get points uv_properties (used for brushed points)
|
||||
- Trigger an update on each pasted strokes, recalculate badly drawn uv and fills (works in 2.93+)
|
||||
|
||||
1.0.0:
|
||||
|
||||
- Compatible with official grease pencil tools
|
||||
- removed box deform and rotate canvas that existed in other
|
||||
|
||||
0.9.3:
|
||||
|
||||
- feat: keyframe jump keys are now auto-binded
|
||||
- UI: added keyframe jump customisation in addon pref
|
||||
- code: split keyframe jump in a separate file with his new key updater
|
||||
|
||||
0.9.2:
|
||||
|
||||
- doc: Correct download link (important, bugged the addon install) + update
|
||||
- code: added tracker url
|
||||
- updater: remove updater temp file, reset minimum version, turn off verbose mode
|
||||
|
||||
0.9.1:
|
||||
|
||||
- Public release
|
||||
- prefs: added fps as part of project settings
|
||||
- check file use pref fps value (previously used harcoded 24fps value)
|
||||
- cleanup: Remove wip GMIC-bridge tools that need to be done separately (if needed)
|
||||
- update: apply changes in integrated copy-paste from the last version of standalone addon
|
||||
- doc: Added fully-detailed french readme
|
||||
|
||||
|
||||
0.8.0:
|
||||
|
||||
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
|
||||
- stripped associated properties from properties.py and passed as wm props.
|
||||
|
||||
0.7.2:
|
||||
|
||||
- fix: Palette importer bug
|
||||
|
||||
0.7.0:
|
||||
|
||||
- feat: auto create empty frame on color layer
|
||||
|
||||
0.6.3:
|
||||
|
||||
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
|
||||
|
||||
0.6.2:
|
||||
|
||||
- feat: colorisation, Option to change stop lines length
|
||||
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
|
||||
- Minor refactor for submodule register
|
||||
|
||||
0.6.1:
|
||||
|
||||
- feat: render objects grouped, one anim render with all ticked object using manual output name
|
||||
|
||||
0.6.0:
|
||||
|
||||
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
|
||||
|
||||
0.5.9:
|
||||
|
||||
- feat: render exporter
|
||||
- Render a selection of GP object isolated from the rest
|
||||
- added exclusions names for GP object listing
|
||||
- setup settings and output according to a name
|
||||
- open render folder
|
||||
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
|
||||
- check file: set scene resolution to settings in prefs (default 2048x1080)
|
||||
|
||||
0.5.8:
|
||||
|
||||
- feat: GP material append on active object from single blend file
|
||||
|
||||
0.5.7:
|
||||
|
||||
- Added warning message for cursor snapping
|
||||
|
||||
0.5.5 - 0.5.6:
|
||||
|
||||
- check file: added check for placement an projection mode for Gpencil.
|
||||
- add a slider to change edit_lines_opacity globally for all GP data at once
|
||||
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
|
||||
|
||||
0.5.4:
|
||||
|
||||
- feat: anim manager in his own GP_toolbox submenu:
|
||||
- button to list disabled anim (allow to quickly check state of the scene)
|
||||
- disable/enable all fcurve in for GP object or other object separately to paint
|
||||
- shift clic to target selection only
|
||||
- check file: added disabled fcurved counter alert with detail in console
|
||||
|
||||
0.5.3:
|
||||
|
||||
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
|
||||
- check file option: change select active tool (choice added in addon preferences)
|
||||
|
||||
0.5.2:
|
||||
|
||||
- Revert back obj_cam operator for following object (native lock view follow only translation)
|
||||
- Changed method for canvas rotation to more robust rotate axis.
|
||||
- Add operators on link checker to open containing folder/file of link
|
||||
- Refactor: file checkers in their own file
|
||||
|
||||
0.5.1:
|
||||
|
||||
- fix: error when empty material slot on GP object.
|
||||
- fix: cursor snap on GP canvas when GP is parented
|
||||
- change: Deleted obj cam (and related set view) operator
|
||||
- change: blacker note background for playblast (stamp_background)
|
||||
- feat: Always playblast from main camera (if in draw_cam)
|
||||
- feat: Handler added to Remap relative on save (pre)
|
||||
- ops: Check for broken links with porposition to find missing files
|
||||
- ops: Added basic hardcoded file checker
|
||||
- Lock main cam
|
||||
- set scene percentage at 100
|
||||
- set show slider and sync range
|
||||
- set fps to 24
|
||||
|
||||
0.4.6:
|
||||
|
||||
- feat: basic Palette manager with base material check and warning
|
||||
|
||||
0.4.5:
|
||||
|
||||
- open blender config folder from addon preference
|
||||
- fix: obj cam parent on selected object
|
||||
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
|
||||
- freeview : bug when rotating free viewfrom cardianl views
|
||||
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
|
||||
|
||||
|
||||
0.4.4:
|
||||
|
||||
- feat: added cursor follow handlers and UI toggle
|
||||
|
||||
0.4.3:
|
||||
|
||||
- change playblast out to 'images' and add playblast as name prefix
|
||||
|
||||
0.4.2:
|
||||
|
||||
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
|
||||
- fix: canvas rotate works with parented camera !
|
||||
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
|
||||
|
||||
0.4.1:
|
||||
|
||||
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
|
||||
|
||||
0.4.0:
|
||||
|
||||
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
|
||||
|
||||
0.3.8:
|
||||
|
||||
- UI: expose onion skin in interface
|
||||
- UI: expose autolock in interface
|
||||
- UI : putted tint layers in a submenu
|
||||
- code: refactor, pushed most of class register in their owner file
|
||||
- tool: tool to rename current or all grease pencil datablock with different name than container object
|
||||
|
||||
0.3.7:
|
||||
|
||||
- UI: new interface with tabs for addon preferences
|
||||
- UI: possible to disable color panel from preference (might be deleted if unusable)
|
||||
- docs: change readme changelog format and correct doc
|
||||
|
||||
0.3.6:
|
||||
|
||||
- UI: Stoplines : add a button for quickly set stoplines visibility.
|
||||
|
||||
0.3.5:
|
||||
|
||||
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
|
||||
- Fix: Enter key added to valid object-breakdown modal.
|
||||
|
||||
0.3.3:
|
||||
|
||||
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
|
||||
|
||||
0.3.3:
|
||||
|
||||
- version 1 beta of gmic colorize
|
||||
- variant of `screen.gp_keyframe_jump` through keymap seetings
|
||||
|
||||
0.3.0:
|
||||
|
||||
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
|
||||
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
|
||||
|
||||
0.2.3:
|
||||
|
||||
- add operator to `screen.gp_keyframe_jump`
|
||||
- add shortcut to rotate canvas
|
||||
- fix duplicate class
|
||||
|
||||
0.2.2:
|
||||
|
||||
- separated props resolution_percentage parameter
|
||||
- playblast options for launching folder and opening folder
|
||||
|
||||
0.2.1:
|
||||
|
||||
- playblast feature
|
||||
- Button to go zoom 100% or fit screen
|
||||
- display scene resolution with res indicator
|
||||
- Fix reference panel : works with video and display in a box layout.
|
||||
- close pseudo-color panel by default (plan to move it to Gpencil tab)
|
||||
|
||||
0.2.0:
|
||||
|
||||
- UI: Toggle camera background images from Toolbox panel
|
||||
- UI: quick access to passepartout
|
||||
- Feature: option to use namespace for pseudo color
|
||||
|
||||
0.1.5:
|
||||
|
||||
- added CGC-auto-updater
|
||||
|
||||
0.1.3:
|
||||
|
||||
- flip cam x
|
||||
- inital stage of overlay toggle (need pref/multiple pref)
|
||||
|
||||
0.1.2:
|
||||
|
||||
- subpanel of GP data (instead of direct append)
|
||||
- initial commit with GP pseudo color
|
120
README_FR.md
120
README_FR.md
|
@ -2,33 +2,14 @@
|
|||
|
||||
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
|
||||
|
||||
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
|
||||
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-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)**
|
||||
> 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é)
|
||||
|
||||
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
|
||||
|
||||
**[English Readme Doc](README.md)**
|
||||
|
||||
Il est recommandé d'activer l'addon natif _Grease pencil tools_ en parallèle qui ajoute des outils essentiels:
|
||||
Rotation du Canvas, Boîte de déformation, Scrub dans la timeline, Navigation de calque facilité...
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -40,7 +21,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
|
||||
- In Front - met en avant l'option `In Front` des propriété de l'objet
|
||||
- X-ray - Option `In Front` dans les 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.
|
||||
|
||||
|
@ -72,19 +53,16 @@ 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:
|
||||
- set show slider and sync range in opened dopesheet
|
||||
- set fps to 24 (need generalisation with addonpref choice)
|
||||
- set select cursor type (according to prefs ?)
|
||||
- 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'
|
||||
- Lock main cam
|
||||
- set scene res to def project res (specified in addon prefs)
|
||||
- set scene percentage at 100:
|
||||
- set show slider and sync range in opened dopesheet
|
||||
- set fps to 24 (need generalisation with addonpref choice)
|
||||
- set select cursor type (according to prefs ?)
|
||||
- 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 ;) )
|
||||
|
||||
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.
|
||||
|
@ -108,72 +86,64 @@ 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 la paropriétée `tints` des layers est utile aux projet car il modifie cette 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 on utilise les tints de layer car il en change la 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
|
||||
|
||||
**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
|
||||
**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.
|
||||
|
||||
|
||||
## 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 et copier un lien.
|
||||
**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.
|
||||
|
||||
<!-- **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))
|
||||
|
||||
## 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
|
||||
## raccourci supplémentaires
|
||||
|
||||
Sculpt mode:
|
||||
|
||||
- `1`, `2`, `3` (rangée au-dessus des lettres) Bascule les filtres de selection par Points/Strokes, comme en edit mode.
|
||||
- point/stroke filter shortcut sur `1`, `2`, `3` en toggle (similaire a l'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...
|
||||
|
||||
---
|
||||
|
||||
### Idées:
|
||||
### TODO:
|
||||
|
||||
- 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 les repos d'[Andarta](https://gitlab.com/andarta-pictures)
|
||||
- Déplacer les clés de dopesheet en même temps que les clés de GP (Déjà Créer par Tom Viguier sur [Andarta](https://gitlab.com/andarta-pictures)
|
||||
|
||||
---
|
||||
|
||||
[Liste des changements ici](CHANGELOG.md)
|
||||
- Meilleure table lumineuse (grosse réflexion et travail en perspective)
|
|
@ -1,68 +0,0 @@
|
|||
import bpy
|
||||
from bpy.types import WorkSpaceTool
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from time import time
|
||||
from .utils import get_addon_prefs
|
||||
|
||||
|
||||
class GPTB_WT_eraser(WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'PAINT_GREASE_PENCIL'
|
||||
|
||||
# The prefix of the idname should be your add-on name.
|
||||
bl_idname = "gp.eraser_tool"
|
||||
bl_label = "Eraser"
|
||||
bl_description = (
|
||||
"This is a tooltip\n"
|
||||
"with multiple lines"
|
||||
)
|
||||
bl_icon = "brush.paint_vertex.draw"
|
||||
bl_widget = None
|
||||
bl_keymap = (
|
||||
("gp.eraser", {"type": 'LEFTMOUSE', "value": 'PRESS'},
|
||||
{"properties": []}),
|
||||
("wm.radial_control", {"type": 'F', "value": 'PRESS'},
|
||||
{"properties": [("data_path_primary", 'scene.gptoolprops.eraser_radius')]}),
|
||||
)
|
||||
|
||||
bl_cursor = 'DOT'
|
||||
|
||||
'''
|
||||
def draw_cursor(context, tool, xy):
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
|
||||
radius = context.scene.gptoolprops.eraser_radius
|
||||
draw_circle_2d(xy, (0.75, 0.25, 0.35, 0.85), radius, 32)
|
||||
'''
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
layout.prop(context.scene.gptoolprops, "eraser_radius")
|
||||
|
||||
### --- REGISTER ---
|
||||
|
||||
## --- KEYMAP
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
|
||||
km = addon.keymaps.new(name="Grease Pencil Stroke Paint (Draw brush)", space_type="EMPTY", region_type='WINDOW')
|
||||
kmi = km.keymap_items.new("gp.eraser", type='LEFTMOUSE', value="PRESS", ctrl=True)
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
kmi.active = prefs.use_precise_eraser
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
def register():
|
||||
bpy.utils.register_tool(GPTB_WT_eraser, after={"builtin.cursor"})
|
||||
#bpy.context.window_manager.keyconfigs.default.keymaps['Grease Pencil Stroke Paint (Draw brush)'].keymap_items[3].idname = 'gp.eraser'
|
||||
|
||||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_tool(GPTB_WT_eraser)
|
||||
unregister_keymaps()
|
718
UI_tools.py
718
UI_tools.py
|
@ -1,18 +1,14 @@
|
|||
# from . import addon_updater_ops
|
||||
from .utils import (get_addon_prefs,
|
||||
anim_status,
|
||||
gp_modifier_status,
|
||||
)
|
||||
from . import addon_updater_ops
|
||||
from .utils import get_addon_prefs
|
||||
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(Panel):
|
||||
class GPTB_PT_dataprop_panel(bpy.types.Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
# bl_space_type = 'VIEW_3D'
|
||||
|
@ -20,7 +16,7 @@ class GPTB_PT_dataprop_panel(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_grease_pencil_layers"#subpanel of this ID
|
||||
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
|
@ -39,8 +35,8 @@ class GPTB_PT_dataprop_panel(Panel):
|
|||
|
||||
## UI in Gpencil sidebar menu
|
||||
|
||||
class GPTB_PT_sidebar_panel(Panel):
|
||||
bl_label = "GP Toolbox"
|
||||
class GPTB_PT_sidebar_panel(bpy.types.Panel):
|
||||
bl_label = "Toolbox"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
|
@ -48,28 +44,23 @@ class GPTB_PT_sidebar_panel(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:')
|
||||
col = layout.column()
|
||||
|
||||
## 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:
|
||||
# layout.label(text='! Flipped !')
|
||||
row = layout.row(align=True)
|
||||
|
||||
row.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
|
||||
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')
|
||||
|
@ -90,16 +81,10 @@ class GPTB_PT_sidebar_panel(Panel):
|
|||
if context.scene.camera:
|
||||
row = layout.row(align=True)# .split(factor=0.5)
|
||||
row.label(text='Passepartout')
|
||||
if context.scene.camera.name == 'draw_cam':
|
||||
row.prop(context.scene.gptoolprops, 'drawcam_passepartout', text='', icon ='OBJECT_HIDDEN')
|
||||
else:
|
||||
row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN')
|
||||
row.prop(context.scene.camera.data, '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.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')
|
||||
|
||||
## background images/videos
|
||||
|
@ -114,72 +99,84 @@ class GPTB_PT_sidebar_panel(Panel):
|
|||
for bg_img in context.scene.camera.data.background_images:
|
||||
if bg_img.source == 'IMAGE' and bg_img.image:
|
||||
row = box.row(align=True)
|
||||
row.prop(bg_img, 'show_background_image', text='', icon='IMAGE_RGB')
|
||||
row.prop(bg_img, 'alpha', text=bg_img.image.name) # options={'HIDDEN'}
|
||||
# row.label(icon='IMAGE_RGB')
|
||||
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
|
||||
# row.prop(bg_img, 'show_background_image', text='', icon=icon)
|
||||
|
||||
row.label(text=bg_img.image.name, icon='IMAGE_RGB')# FILE_IMAGE
|
||||
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
|
||||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
||||
if bg_img.source == 'MOVIE_CLIP' and bg_img.clip:
|
||||
row = box.row(align=True)
|
||||
row.prop(bg_img, 'show_background_image', text='', icon='FILE_MOVIE')
|
||||
row.prop(bg_img, 'alpha', text=bg_img.clip.name) # options={'HIDDEN'}
|
||||
# row.label(icon='FILE_MOVIE')
|
||||
# icon = 'HIDE_OFF' if bg_img.show_background_image else 'HIDE_ON'
|
||||
# row.prop(bg_img, 'show_background_image', text='', icon=icon)
|
||||
row.label(text=bg_img.clip.name, icon='FILE_MOVIE')
|
||||
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
|
||||
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
|
||||
|
||||
## playblast params
|
||||
layout.separator()
|
||||
layout.label(text = 'Playblast:')
|
||||
row = layout.row(align=False)#split(factor=0.6)
|
||||
row.label(text = f'{rd.resolution_x * context.scene.gptoolprops.resolution_percentage // 100} x {rd.resolution_y * context.scene.gptoolprops.resolution_percentage // 100}')
|
||||
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
|
||||
# row.prop(rd, 'resolution_percentage', text='')#real percent scene percentage
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
|
||||
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
|
||||
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
|
||||
|
||||
else:
|
||||
layout.label(text='No camera !', icon = 'ERROR')
|
||||
|
||||
## Straight line ops from official greasepencil_tools addon if enabled.
|
||||
# if any(x in context.preferences.addons.keys() for x in ('greasepencil_tools', 'greasepencil-addon')): # check enabled addons
|
||||
if hasattr(bpy.types, 'GP_OT_straight_stroke'): # check if operator exists : bpy.ops.gp.straight_stroke.idname()
|
||||
layout.operator(bpy.types.GP_OT_straight_stroke.bl_idname, icon ="CURVE_PATH")
|
||||
|
||||
## Options
|
||||
col = layout.column()
|
||||
col.label(text = 'Options:')
|
||||
layout.separator()
|
||||
layout.label(text = 'Options:')
|
||||
# row = layout.row(align=False)
|
||||
## maybe remove cursor_follow icon that look like
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
layout.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
layout.prop(context.space_data.overlay, 'use_gpencil_onion_skin')
|
||||
|
||||
## Kf Jump filter
|
||||
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 == 'GREASEPENCIL':
|
||||
# col.prop(context.object.data, 'use_autolock_layers') # not often used
|
||||
col.prop(context.object, 'show_in_front') # text='In Front'
|
||||
if context.object and context.object.type == 'GPENCIL':
|
||||
layout.prop(context.object.data, 'use_autolock_layers')
|
||||
layout.prop(context.object, 'show_in_front', text='X-ray')#default text "In Front"
|
||||
|
||||
## rename datablock temporary layout
|
||||
if context.object.name != context.object.data.name:
|
||||
box = col.box()
|
||||
box = layout.box()
|
||||
box.label(text='different name for object and data:', icon='INFO')
|
||||
row = box.row(align=False)
|
||||
row.operator('gp.rename_data_from_obj').rename_all = False
|
||||
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 = layout.box()
|
||||
box.label(text='Missing base material setup', icon='INFO')
|
||||
box.operator('gp.load_default_palette')
|
||||
|
||||
else:
|
||||
col.label(text='No GP object selected')
|
||||
layout.label(text='No GP object selected')
|
||||
|
||||
|
||||
## Gpv3: not more edit line (use Curve lines)
|
||||
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
||||
layout.prop(context.scene.gptoolprops, 'edit_lines_opacity')
|
||||
|
||||
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
|
||||
layout.operator('gp.create_empty_frames', icon = 'DECORATE_KEYFRAME')
|
||||
|
||||
## File checker
|
||||
row = layout.row(align=True)
|
||||
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
|
||||
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
|
||||
|
||||
# Mention update as notice
|
||||
# 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(Panel):
|
||||
bl_label = "Animation Manager"
|
||||
class GPTB_PT_anim_manager(bpy.types.Panel):
|
||||
bl_label = "Animation manager"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
|
@ -189,131 +186,48 @@ class GPTB_PT_anim_manager(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
|
||||
|
||||
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
|
||||
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
|
||||
ops.mute = False
|
||||
|
||||
ops = row.operator('gp.toggle_mute_animation', text='OFF', icon=off_icon)
|
||||
ops.mode = cat_type
|
||||
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.mute = True
|
||||
|
||||
## GP modifiers
|
||||
subcol = col.column()
|
||||
|
||||
row = subcol.row(align=True)
|
||||
row.label(text='Gp modifiers:')
|
||||
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
|
||||
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
|
||||
|
||||
## Step Select Frames
|
||||
col.operator('gptb.step_select_frames')
|
||||
|
||||
## Follow curve path
|
||||
col = col.column()
|
||||
row = col.row(align=True)
|
||||
|
||||
if context.object:
|
||||
if context.object.type == 'CURVE' and context.mode in ('OBJECT', 'EDIT_CURVE'):
|
||||
row.operator('object.object_from_curve', text='Back To Object', icon='LOOP_BACK')
|
||||
|
||||
elif (follow_const := context.object.constraints.get('Follow Path')) and follow_const.target:
|
||||
row.operator('object.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE')
|
||||
row.operator('object.remove_follow_path', text='', icon='X')
|
||||
col.label(text=f'{context.object.name} -> {follow_const.target.name}', icon='CON_FOLLOWPATH')
|
||||
if follow_const.use_fixed_location:
|
||||
col.prop(follow_const, 'offset_factor')
|
||||
else:
|
||||
col.prop(follow_const, 'offset')
|
||||
if context.object.location.length != 0: # context.object.location[:] != (0,0,0):
|
||||
col.operator('object.location_clear', text='Offseted Location! Reset', icon='ERROR')
|
||||
# ? Check if object location is animated ? (can be intentional...)
|
||||
|
||||
else:
|
||||
col.operator('object.create_follow_path_curve', text='Create Follow Curve', icon='CURVE_BEZCURVE')
|
||||
|
||||
|
||||
## This can go in an extra category...
|
||||
col = layout.column()
|
||||
col.use_property_split = False
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
if context.scene.gptoolprops.cursor_follow:
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
|
||||
|
||||
|
||||
class GPTB_PT_toolbox_playblast(Panel):
|
||||
bl_label = "Playblast"
|
||||
class GPTB_PT_tint_layers(bpy.types.Panel):
|
||||
bl_label = "Tint layers"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
rd = context.scene.render
|
||||
row = layout.row(align=False) # split(factor=0.6)
|
||||
percent = context.scene.gptoolprops.resolution_percentage
|
||||
row.label(text = f'{rd.resolution_x * percent // 100} x {rd.resolution_y * percent // 100}')
|
||||
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
|
||||
# row.prop(rd, 'resolution_percentage', text='') # real percent scene percentage
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
|
||||
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
|
||||
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
|
||||
|
||||
class GPTB_PT_tint_layers(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="")
|
||||
|
@ -324,82 +238,50 @@ class GPTB_PT_tint_layers(Panel):
|
|||
## pseudo color layers
|
||||
# layout.separator()
|
||||
col = layout.column(align = True)
|
||||
# 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')
|
||||
row = col.split(align=False, factor=0.63)
|
||||
row.prop(context.scene.gptoolprops, 'autotint_offset')
|
||||
row.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_checker(Panel):
|
||||
bl_label = "Checker"
|
||||
class GPTB_PT_render(bpy.types.Panel):
|
||||
bl_label = "Render"
|
||||
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
|
||||
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.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.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.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'
|
||||
|
||||
## 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')
|
||||
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS
|
||||
|
||||
blend = Path(bpy.data.filepath)
|
||||
out = blend.parents[1] / "compo" / "base"
|
||||
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
|
||||
|
||||
|
||||
class GPTB_PT_color(Panel):
|
||||
bl_label = "Color"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
## Create empty frame on layer
|
||||
|
||||
# Material panel as a pop-up (would work if palette dir is separated)
|
||||
# col.operator("wm.call_panel", text="Link Materials Palette", icon='COLOR').name = "GPTB_PT_palettes_linker_ui"
|
||||
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
|
||||
# col.operator("wm.call_panel", text="Link Material Palette", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
|
||||
|
||||
""" # unused : added in Animation Manager
|
||||
class GPTB_PT_extra(Panel):
|
||||
bl_label = "Extra"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Gpencil"
|
||||
bl_parent_id = "GPTB_PT_sidebar_panel"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
|
||||
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
|
||||
"""
|
||||
|
||||
"""
|
||||
## unused -- (integrated in sidebar_panel)
|
||||
class GPTB_PT_cam_ref_panel(Panel):
|
||||
class GPTB_PT_cam_ref_panel(bpy.types.Panel):
|
||||
bl_label = "Background imgs"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
|
@ -425,375 +307,36 @@ class GPTB_PT_cam_ref_panel(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_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', '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.separator()
|
||||
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
|
||||
layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
|
||||
|
||||
|
||||
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_color,
|
||||
GPTB_PT_tint_layers,
|
||||
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,
|
||||
|
||||
GPTB_PT_render,
|
||||
## GPTB_PT_cam_ref_panel,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -814,3 +357,36 @@ 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") """
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,22 +1,33 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# 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/>.
|
||||
|
||||
bl_info = {
|
||||
"name": "GP toolbox",
|
||||
"description": "Tool set for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (4, 0, 4),
|
||||
"blender": (4, 3, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"description": "Set of tools for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou",
|
||||
"version": (1, 0, 3),
|
||||
"blender": (2, 91, 0),
|
||||
"location": "sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
|
||||
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
|
||||
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
|
||||
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from sys import modules
|
||||
from .utils import get_addon_prefs, draw_kmi
|
||||
from . import addon_updater_ops
|
||||
|
||||
from .utils import *
|
||||
from .functions import *
|
||||
|
||||
## GMIC
|
||||
from .GP_guided_colorize import GP_colorize
|
||||
|
@ -30,47 +41,24 @@ 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_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,
|
||||
GP_PG_namespaces,
|
||||
)
|
||||
from .properties import GP_PG_ToolsSettings
|
||||
|
||||
from bpy.props import (FloatProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
IntProperty,
|
||||
PointerProperty
|
||||
)
|
||||
IntProperty)
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.app.handlers import persistent
|
||||
from pathlib import Path
|
||||
# from .eyedrop import EyeDropper
|
||||
|
@ -85,14 +73,11 @@ from pathlib import Path
|
|||
|
||||
@persistent
|
||||
def remap_relative(dummy):
|
||||
# try:
|
||||
all_path = [lib for lib in bpy.utils.blend_paths(local=True)]
|
||||
bpy.ops.file.make_paths_relative()
|
||||
for i, lib in enumerate(bpy.utils.blend_paths(local=True)):
|
||||
if all_path[i] != lib:
|
||||
print('Remapped:', all_path[i], '\n>> ', lib)
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
|
||||
def remap_on_save_update(self, context):
|
||||
pref = get_addon_prefs()
|
||||
|
@ -104,56 +89,74 @@ def remap_on_save_update(self, context):
|
|||
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
||||
bpy.app.handlers.save_pre.remove(remap_relative)
|
||||
|
||||
|
||||
## precise eraser
|
||||
# def update_use_precise_eraser(self, context):
|
||||
# km, kmi = TOOL_eraser_brush.addon_keymaps[0]
|
||||
# kmi.active = self.use_precise_eraser
|
||||
|
||||
class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
bl_idname = __name__
|
||||
|
||||
## 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"),
|
||||
('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"),
|
||||
('MAN_OPS', "Operator", "Operator to add Manually"),
|
||||
# ('TUTO', "Tutorial", "How to use the tool"),
|
||||
('UPDATE', "Update", "Check and apply updates"),
|
||||
# ('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(
|
||||
name="Relative Remap On Save",
|
||||
description="Always remap all external path to relative when saving\nNeed blender restart if changed",
|
||||
default=False,
|
||||
default=True,
|
||||
update=remap_on_save_update
|
||||
)
|
||||
|
||||
fps : IntProperty(
|
||||
name='Frame Rate',
|
||||
description="Fps of the project, Used to conform the file when you use Check file operator",
|
||||
default=24,
|
||||
default=25,
|
||||
min=1,
|
||||
max=10000
|
||||
)
|
||||
|
@ -172,77 +175,16 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
description="Path relative to blend to place render",
|
||||
default="//render", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
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,
|
||||
)
|
||||
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')
|
||||
|
||||
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
|
||||
|
||||
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",
|
||||
|
@ -256,6 +198,19 @@ 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",
|
||||
|
@ -280,7 +235,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
## KF jumper
|
||||
kfj_use_shortcut: BoolProperty(
|
||||
name = "Use Keyframe Jump Shortcut",
|
||||
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
|
||||
description = "Auto bind shotcut for keyframe jump (else you can bien manually using 'screen.gp_keyframe_jump' id_name)",
|
||||
default = True)
|
||||
|
||||
kfj_prev_keycode : StringProperty(
|
||||
|
@ -323,68 +278,12 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
description = "add ctrl",
|
||||
default = False)
|
||||
|
||||
|
||||
fixprops: bpy.props.PointerProperty(type = GP_PG_FixSettings)
|
||||
|
||||
## GP Layer navigator
|
||||
|
||||
nav_use_fade : BoolProperty(
|
||||
name='Fade Inactive Layers',
|
||||
description='Fade Inactive layers to determine active layer in a glimpse',
|
||||
default=True)
|
||||
|
||||
nav_fade_val : FloatProperty(
|
||||
name='Fade Value',
|
||||
description='Fade value for other layers when navigating (0=invisible)',
|
||||
default=0.1, min=0.0, max=0.95, step=1, precision=2)
|
||||
|
||||
nav_limit : FloatProperty(
|
||||
name='Fade Duration',
|
||||
description='Time of other layer faded when using layer navigation',
|
||||
default=1.4, min=0.1, max=5, step=2, precision=1, subtype='TIME', unit='TIME')
|
||||
|
||||
nav_use_fade_in : BoolProperty(
|
||||
name='Progressive Fade Back',
|
||||
description='Use a fade on other layer when navigating',
|
||||
default=True)
|
||||
|
||||
nav_fade_in_time : FloatProperty(
|
||||
name='Fade-In Time',
|
||||
description='Duration of the fade',
|
||||
default=0.5, min=0.1, max=5, step=2, precision=2, subtype='TIME', unit='TIME')
|
||||
|
||||
nav_interval : FloatProperty(
|
||||
name='Refresh Rate',
|
||||
description='Refresh rate for fade updating (upper value means stepped fade)',
|
||||
default=0.04, min=0.01, max=0.5, step=3, precision=2, subtype='TIME', unit='TIME')
|
||||
|
||||
## Temp cutter
|
||||
# temp_cutter_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
|
||||
|
@ -404,53 +303,13 @@ 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
|
||||
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')
|
||||
box.label(text='Palette library folder:')
|
||||
box.prop(self, 'palette_path')
|
||||
|
||||
## render output
|
||||
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)
|
||||
|
||||
box.prop(self, 'output_path')
|
||||
box.prop(self, 'use_relative_remap_on_save')
|
||||
|
||||
### TODO add render settings
|
||||
|
||||
|
@ -459,136 +318,60 @@ 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
|
||||
|
||||
## Keyframe jump now displayed in Shortcut Tab
|
||||
# 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'
|
||||
|
||||
# if self.kfj_next_keycode:
|
||||
# mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
||||
# text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
|
||||
# text = f'Jump Keyframe Next: {text} (Click to change)'
|
||||
# else:
|
||||
# text = prompt
|
||||
# ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
# ops.s_keycode = 'kfj_next_keycode'
|
||||
# ops.s_ctrl = 'kfj_next_ctrl'
|
||||
# ops.s_shift = 'kfj_next_shift'
|
||||
# ops.s_alt = 'kfj_next_alt'
|
||||
|
||||
# else:
|
||||
# box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
||||
# box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
||||
|
||||
box = layout.box()
|
||||
box.label(text='Tools options:')
|
||||
box.label(text='Keyframe Jump option:')
|
||||
|
||||
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')
|
||||
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:
|
||||
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')
|
||||
text = prompt
|
||||
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
ops.s_keycode = 'kfj_prev_keycode'
|
||||
ops.s_ctrl = 'kfj_prev_ctrl'
|
||||
ops.s_shift = 'kfj_prev_shift'
|
||||
ops.s_alt = 'kfj_prev_alt'
|
||||
|
||||
if self.pref_tabs == 'KEYS':
|
||||
# layout.label(text='Shortcuts :')
|
||||
if self.kfj_next_keycode:
|
||||
mods = '+'.join([m for m, b in [('Ctrl', self.kfj_next_ctrl), ('Shift', self.kfj_next_shift), ('Alt', self.kfj_next_alt)] if b])
|
||||
text = f'{mods}+{self.kfj_next_keycode}' if mods else self.kfj_next_keycode
|
||||
text = f'Jump Keyframe Next: {text} (Click to change)'
|
||||
else:
|
||||
text = prompt
|
||||
ops = box.operator('prefs.shortcut_rebinder', text=text, icon='FILE_REFRESH')
|
||||
ops.s_keycode = 'kfj_next_keycode'
|
||||
ops.s_ctrl = 'kfj_next_ctrl'
|
||||
ops.s_shift = 'kfj_next_shift'
|
||||
ops.s_alt = 'kfj_next_alt'
|
||||
|
||||
else:
|
||||
box.label(text="No Jump hotkey auto set. Following operators needs to be set manually", icon="ERROR")
|
||||
box.label(text="screen.gp_keyframe_jump - preferably in 'screen' category to jump from any editor")
|
||||
|
||||
## Active tool
|
||||
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
|
||||
box.label(text='Autofix check button options:')
|
||||
box.prop(self, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
||||
|
||||
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
|
||||
box.prop(self, "render_obj_exclusion", icon='FILTER')#
|
||||
|
||||
## 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
|
||||
## random color character separator
|
||||
box = layout.box()
|
||||
box.label(text='Random color options:')
|
||||
box.prop(self, 'separator')
|
||||
|
||||
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
|
||||
# layout.label(text='Notes:')
|
||||
layout.label(text='Following operators ID have to be set manually in keymap if needed :')
|
||||
layout.label(text='Following operators ID have to be set manually :')
|
||||
|
||||
## keyframe jump
|
||||
box = layout.box()
|
||||
|
@ -609,162 +392,13 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap'
|
||||
box.label(text='Or just create a new shortcut using cursor_snap')
|
||||
|
||||
## Clear keyframe
|
||||
box = layout.box()
|
||||
box.label(text='Clear active frame (delete all strokes without deleting the frame)')
|
||||
row = box.row()
|
||||
row.label(text='gp.clear_active_frame')
|
||||
row.operator('wm.copytext', icon='COPYDOWN').text = 'gp.clear_active_frame'
|
||||
|
||||
## user prefs
|
||||
box = layout.box()
|
||||
box.label(text='Note: You can access user pref file and startup file in config folder')
|
||||
box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG')
|
||||
|
||||
if self.pref_tabs == 'CHECKS':
|
||||
layout.label(text='Following checks will be made when clicking "Check File" button:')
|
||||
col = layout.column()
|
||||
col.use_property_split = True
|
||||
# col.prop(self.fixprops, 'check_only')
|
||||
col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO')
|
||||
# col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
|
||||
col.separator()
|
||||
col.prop(self.fixprops, 'lock_main_cam')
|
||||
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
|
||||
col.prop(self.fixprops, 'set_res_percentage')
|
||||
col.prop(self.fixprops, 'set_fps', text=f'Reset FPS (to {self.fps})')
|
||||
col.prop(self.fixprops, 'set_slider_n_sync')
|
||||
col.prop(self.fixprops, 'check_front_axis')
|
||||
col.prop(self.fixprops, 'check_placement')
|
||||
col.prop(self.fixprops, 'set_gp_use_lights_off')
|
||||
col.prop(self.fixprops, 'set_pivot_median_point')
|
||||
col.prop(self.fixprops, 'disable_guide')
|
||||
col.prop(self.fixprops, 'list_disabled_anim')
|
||||
col.prop(self.fixprops, 'list_obj_vis_conflict')
|
||||
col.prop(self.fixprops, 'list_gp_mod_vis_conflict')
|
||||
col.prop(self.fixprops, 'list_broken_mod_targets')
|
||||
col.prop(self.fixprops, 'autokey_add_n_replace')
|
||||
col.prop(self.fixprops, 'remove_redundant_strokes')
|
||||
#-# col.prop(self.fixprops, 'set_cursor_type')
|
||||
|
||||
# col = layout.column()
|
||||
# col.use_property_split = True
|
||||
col.prop(self.fixprops, "select_active_tool", icon='RESTRICT_SELECT_OFF')
|
||||
col.prop(self.fixprops, "file_path_type")
|
||||
col.prop(self.fixprops, "lock_object_mode")
|
||||
# row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)')
|
||||
|
||||
# if self.pref_tabs == 'UPDATE':
|
||||
# addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
|
||||
|
||||
### --- ENV_PROP ---
|
||||
def set_namespace_env(name_env, prop_group):
|
||||
tag_list = os.getenv(name_env)
|
||||
current_pfix = []
|
||||
project_pfix = []
|
||||
|
||||
if tag_list and tag_list.strip():
|
||||
## Force clear (clear also hide): prop_group.namespaces.clear()
|
||||
|
||||
## Get current tag list
|
||||
tag_list = tag_list.strip(', ').split(',')
|
||||
|
||||
current_pfix = [n.tag for n in prop_group.namespaces if n.tag]
|
||||
# for n in prop_group.namespaces:
|
||||
# print(n.tag, n.name)
|
||||
|
||||
for p in tag_list:
|
||||
tag = p.split(':')[0].strip()
|
||||
project_pfix.append(tag)
|
||||
name = '' if not ':' in p else p.split(':')[1].strip()
|
||||
item = None
|
||||
if tag not in current_pfix:
|
||||
item = prop_group.namespaces.add()
|
||||
item.tag = tag
|
||||
item.name = name
|
||||
# print('Loaded project tag:', tag, name)
|
||||
elif name:
|
||||
# get the tag and apply name
|
||||
item = next((n for n in prop_group.namespaces if n.tag == tag), None)
|
||||
if item: # and not item.name.strip()
|
||||
item.name = name
|
||||
# print('Loaded name:', name)
|
||||
|
||||
if item:
|
||||
item.is_project = True
|
||||
|
||||
else:
|
||||
tag_list = []
|
||||
|
||||
# "release" suffix that are not in project anymore
|
||||
for n in prop_group.namespaces:
|
||||
n.is_project = n.tag in project_pfix
|
||||
|
||||
def set_env_properties():
|
||||
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
fps = os.getenv('FPS')
|
||||
prefs.fps = int(fps) if fps else prefs.fps
|
||||
|
||||
render_width = os.getenv('RENDER_WIDTH')
|
||||
prefs.render_res_x = int(render_width) if render_width else prefs.render_res_x
|
||||
|
||||
render_height = os.getenv('RENDER_HEIGHT')
|
||||
prefs.render_res_y = int(render_height) if render_height else prefs.render_res_y
|
||||
|
||||
palettes = os.getenv('PALETTES')
|
||||
if prefs.use_env_palettes:
|
||||
prefs.palette_path = palettes if palettes else prefs.palette_path
|
||||
|
||||
brushes = os.getenv('BRUSHES')
|
||||
if prefs.use_env_brushes:
|
||||
prefs.brush_path = brushes if brushes else prefs.brush_path
|
||||
|
||||
# if prefs.use_env_namespace:
|
||||
|
||||
## Old method with direct string assignment (now a property group)
|
||||
# prefix_list = os.getenv('PREFIXES')
|
||||
# prefs.prefixes = prefix_list if prefix_list else prefs.prefixes
|
||||
# suffix_list = os.getenv('SUFFIXES')
|
||||
# prefs.suffixes = suffix_list if suffix_list else prefs.suffixes
|
||||
|
||||
set_namespace_env('PREFIXES', prefs.prefixes)
|
||||
set_namespace_env('SUFFIXES', prefs.suffixes)
|
||||
|
||||
separator = os.getenv('SEPARATOR')
|
||||
prefs.separator = separator if separator else prefs.separator
|
||||
|
||||
|
||||
class GPTB_set_env_settings(bpy.types.Operator):
|
||||
"""manually reset prefs from project environnement setttings"""
|
||||
bl_idname = "prefs.reset_gp_toolbox_env"
|
||||
bl_label = "Reset prefs from project environnement settings (if any)"
|
||||
|
||||
mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) # 'HIDDEN',
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if self.mode == 'ALL':
|
||||
set_env_properties()
|
||||
elif self.mode == 'PREFIXES':
|
||||
prefix_list = os.getenv('PREFIXES')
|
||||
if not prefix_list:
|
||||
self.report({'ERROR'}, 'No prefix preset to load from project environnement')
|
||||
return {'CANCELLED'}
|
||||
set_namespace_env('PREFIXES', prefs.prefixes)
|
||||
|
||||
elif self.mode == 'SUFFIXES':
|
||||
suffix_list = os.getenv('SUFFIXES')
|
||||
if not suffix_list:
|
||||
self.report({'ERROR'}, 'No suffix preset to load from project environnement')
|
||||
return {'CANCELLED'}
|
||||
set_namespace_env('SUFFIXES', prefs.suffixes)
|
||||
|
||||
return {'FINISHED'}
|
||||
if self.pref_tabs == 'UPDATE':
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
|
||||
### --- REGISTER ---
|
||||
|
@ -774,81 +408,63 @@ class GPTB_set_env_settings(bpy.types.Operator):
|
|||
|
||||
|
||||
classes = (
|
||||
GPTB_set_env_settings,
|
||||
GPTB_prefs,
|
||||
GPTB_prefs,
|
||||
GP_PG_ToolsSettings,
|
||||
GPT_OT_auto_tint_gp_layers,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
# register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
|
||||
def register():
|
||||
# Register property group first
|
||||
properties.register()
|
||||
addon_updater_ops.register(bl_info)
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
for mod in addon_modules:
|
||||
mod.register()
|
||||
|
||||
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_cursor_snap_canvas.register()
|
||||
OP_render.register()
|
||||
OP_copy_paste.register()
|
||||
UI_tools.register()
|
||||
keymaps.register()
|
||||
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings)
|
||||
|
||||
set_env_properties()
|
||||
# add handler (if option is on)
|
||||
|
||||
## add handler (if option is on)
|
||||
prefs = get_addon_prefs()
|
||||
if prefs.use_relative_remap_on_save:
|
||||
if get_addon_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)
|
||||
|
||||
for mod in reversed(addon_modules):
|
||||
mod.unregister()
|
||||
|
||||
keymaps.unregister()
|
||||
addon_updater_ops.unregister()
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
properties.unregister()
|
||||
|
||||
UI_tools.unregister()
|
||||
OP_copy_paste.unregister()
|
||||
OP_render.unregister()
|
||||
OP_cursor_snap_canvas.unregister()
|
||||
OP_palettes.unregister()
|
||||
OP_file_checker.unregister()
|
||||
OP_helpers.unregister()
|
||||
OP_keyframe_jump.unregister()
|
||||
OP_breakdowner.unregister()
|
||||
OP_temp_cutter.unregister()
|
||||
GP_colorize.unregister()## GP_guided_colorize.
|
||||
OP_playblast_bg.unregister()
|
||||
OP_playblast.unregister()
|
||||
del bpy.types.Scene.gptoolprops
|
||||
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
75
functions.py
75
functions.py
|
@ -6,10 +6,27 @@ 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 get_gp_draw_plane, link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
|
||||
from .utils import link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
|
||||
|
||||
|
||||
def to_bl_image(array, img):
|
||||
def get_view_origin_position():
|
||||
#method 1
|
||||
from bpy_extras import view3d_utils
|
||||
region = bpy.context.region
|
||||
rv3d = bpy.context.region_data
|
||||
view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||
print("view_loc1", view_loc)#Dbg
|
||||
|
||||
#method 2
|
||||
r3d = bpy.context.space_data.region_3d
|
||||
view_loc2 = r3d.view_matrix.inverted().translation
|
||||
print("view_loc2", view_loc2)#Dbg
|
||||
if view_loc != view_loc2: print('there might be an errror when finding view coordinate')
|
||||
|
||||
return view_loc
|
||||
|
||||
|
||||
def to_bl_image(array,img):
|
||||
# Write the result to Blender preview
|
||||
width = len(array[0])
|
||||
height = len(array)
|
||||
|
@ -39,7 +56,7 @@ def to_bl_image(array, img):
|
|||
image.pixels = output_pixels
|
||||
|
||||
|
||||
def bm_angle_split(bm, angle) :
|
||||
def bm_angle_split(bm,angle) :
|
||||
bm.verts.ensure_lookup_table()
|
||||
loop = link_vert(bm.verts[0],[bm.verts[0]])
|
||||
splitted = []
|
||||
|
@ -66,7 +83,7 @@ def bm_angle_split(bm, angle) :
|
|||
|
||||
return loops
|
||||
|
||||
def bm_uniform_density(bm, cam, max_spacing):
|
||||
def bm_uniform_density(bm,cam,max_spacing):
|
||||
from bpy_extras.object_utils import world_to_camera_view as cam_space
|
||||
scene = bpy.context.scene
|
||||
ratio = scene.render.resolution_y/scene.render.resolution_x
|
||||
|
@ -85,7 +102,7 @@ def bm_uniform_density(bm, cam, max_spacing):
|
|||
return bm
|
||||
|
||||
|
||||
def gp_stroke_angle_split (frame, strokes, angle):
|
||||
def gp_stroke_angle_split (frame,strokes,angle):
|
||||
strokes_info = gp_stroke_to_bmesh(strokes)
|
||||
|
||||
new_strokes = []
|
||||
|
@ -99,8 +116,7 @@ def gp_stroke_angle_split (frame, strokes, angle):
|
|||
|
||||
splitted_loops = bm_angle_split(bm,angle)
|
||||
|
||||
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||
frame.drawing.strokes.remove(stroke_info['stroke'])
|
||||
frame.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)
|
||||
|
@ -109,7 +125,7 @@ def gp_stroke_angle_split (frame, strokes, angle):
|
|||
return new_strokes
|
||||
|
||||
|
||||
def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
|
||||
def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
|
||||
strokes_info = gp_stroke_to_bmesh(strokes)
|
||||
|
||||
new_strokes = []
|
||||
|
@ -124,7 +140,6 @@ 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()
|
||||
|
||||
|
@ -137,7 +152,7 @@ def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
|
|||
return new_strokes
|
||||
|
||||
|
||||
def along_stroke(stroke, attr, length, min, max) :
|
||||
def along_stroke(stroke,attr,length,min,max) :
|
||||
strokelen = len(stroke.points)
|
||||
for index,point in enumerate(stroke.points) :
|
||||
value = getattr(point,attr)
|
||||
|
@ -149,9 +164,9 @@ def along_stroke(stroke, attr, length, min, max) :
|
|||
remap = remapping((strokelen-index)/length,0,1,min,max)
|
||||
setattr(point,attr,value*remap)
|
||||
|
||||
def randomise_points(mat, points, attr, strength) :
|
||||
def randomise_points(mat,points,attr,strength) :
|
||||
for point in points :
|
||||
if attr == 'co' :
|
||||
if attr is 'co' :
|
||||
random_x = (rand()-0.5)
|
||||
random_y = (rand()-0.5)
|
||||
|
||||
|
@ -167,7 +182,8 @@ def randomise_points(mat, points, attr, strength) :
|
|||
setattr(point,attr,value+random*strength)
|
||||
|
||||
|
||||
def zoom_to_object(cam, resolution, box, margin=0.01) :
|
||||
|
||||
def zoom_to_object(cam,resolution,box,margin=0.01) :
|
||||
min_x= box[0]
|
||||
max_x= box[1]
|
||||
min_y= box[2]
|
||||
|
@ -192,7 +208,7 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
|
|||
bpy.context.scene.objects.link(zoom_cam)
|
||||
|
||||
|
||||
resolution = (int(resolution[0]*factor), int(resolution[1]*factor))
|
||||
resolution = (int(resolution[0]*factor),int(resolution[1]*factor))
|
||||
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
@ -217,8 +233,29 @@ 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 = []) :
|
||||
def get_object_info(mesh_groups,order_list = []) :
|
||||
scene = bpy.context.scene
|
||||
cam = scene.camera
|
||||
#scale = scene.render.resolution_percentage / 100.0
|
||||
|
@ -341,10 +378,4 @@ def get_object_info(mesh_groups, order_list = []) :
|
|||
scene.render.resolution_y = res_y
|
||||
|
||||
|
||||
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()
|
||||
return mesh_info,convert_table
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
import bpy
|
||||
import gpu
|
||||
# import blf
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
def extrapolate_points_by_length(a, b, length):
|
||||
'''
|
||||
Return a third point C from by continuing in AB direction
|
||||
Length define BC distance. both vector2 and vector3
|
||||
'''
|
||||
# return b + ((b - a).normalized() * length)# one shot
|
||||
ab = b - a
|
||||
if not ab:
|
||||
return None
|
||||
return b + (ab.normalized() * length)
|
||||
|
||||
def view3d_camera_border_2d(context, cam):
|
||||
# based on https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
|
||||
# cam = context.scene.camera
|
||||
frame = cam.data.view_frame(scene=context.scene)
|
||||
# to world-space
|
||||
frame = [cam.matrix_world @ v for v in frame]
|
||||
# to pixelspace
|
||||
region, rv3d = context.region, context.space_data.region_3d
|
||||
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
|
||||
return frame_px
|
||||
|
||||
def vertices_to_line_loop(v_list, closed=True) -> list:
|
||||
'''Take a sequence of vertices
|
||||
return a position lists of segments to create a line loop passing in all points
|
||||
the result is usable with gpu_shader 'LINES'
|
||||
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
|
||||
'''
|
||||
loop = []
|
||||
for i in range(len(v_list) - 1):
|
||||
loop += [v_list[i], v_list[i + 1]]
|
||||
if closed:
|
||||
# Add segment between last and first to close loop
|
||||
loop += [v_list[-1], v_list[0]]
|
||||
return loop
|
||||
|
||||
def draw_cam_frame_callback_2d():
|
||||
context = bpy.context
|
||||
if context.region_data.view_perspective != 'CAMERA':
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.name != 'draw_cam':
|
||||
return
|
||||
|
||||
main_cam = context.scene.camera.parent
|
||||
if not main_cam:
|
||||
return
|
||||
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
frame_point = view3d_camera_border_2d(
|
||||
context, context.scene.camera.parent)
|
||||
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
|
||||
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
|
||||
|
||||
if context.scene.gptoolprops.drawcam_passepartout:
|
||||
### PASSEPARTOUT
|
||||
|
||||
# frame positions
|
||||
#
|
||||
# lupext lup rup rupext
|
||||
# D-----A
|
||||
# | |
|
||||
# C-----B
|
||||
# ldnext ldn rdn lnext
|
||||
|
||||
a = frame_point[0]
|
||||
b = frame_point[1]
|
||||
c = frame_point[2]
|
||||
d = frame_point[3]
|
||||
|
||||
ext = 10000
|
||||
rup = extrapolate_points_by_length(b,a, ext)
|
||||
rdn = extrapolate_points_by_length(a,b, ext)
|
||||
rupext = rup + ((a-d).normalized() * ext)
|
||||
rdnext = rdn + ((a-d).normalized() * ext)
|
||||
lup = extrapolate_points_by_length(c,d, ext)
|
||||
ldn = extrapolate_points_by_length(d,c, ext)
|
||||
lupext = lup + ((c-b).normalized() * ext)
|
||||
ldnext = ldn + ((c-b).normalized() * ext)
|
||||
|
||||
|
||||
# ppp_color = (1.0, 1.0, 0.0, 1)
|
||||
ppp_color = (0.0, 0.0, 0.0, context.scene.camera.data.passepartout_alpha)
|
||||
|
||||
rect = [rup, rdn, rupext, rdnext,
|
||||
lup, ldn, lupext, ldnext,
|
||||
a, b, c, d]
|
||||
|
||||
## convert to 2d
|
||||
# region, rv3d = context.region, context.space_data.region_3d
|
||||
# rect = [location_3d_to_region_2d(region, rv3d, v) for v in rect]
|
||||
|
||||
# lupext=6 lup=4 rup=0 rupext=2
|
||||
# d=11, a=8
|
||||
# c=10, b=9
|
||||
# ldnext=7 ldn=5 rdn=1 dnpext=3
|
||||
|
||||
indices = [(0,1,2), (1,2,3),
|
||||
(4,5,6), (5,6,7),
|
||||
(8,11,4), (8,4,0),
|
||||
(10,9,5), (9,5,1),
|
||||
]
|
||||
|
||||
# ### passpartout_points
|
||||
|
||||
passepartout = batch_for_shader(
|
||||
shader_2d, 'TRIS', {"pos": rect}, indices=indices)
|
||||
# shader_2d, 'LINE_LOOP', {"pos": rect})
|
||||
|
||||
shader_2d.bind()
|
||||
shader_2d.uniform_float("color", ppp_color)
|
||||
passepartout.draw(shader_2d)
|
||||
|
||||
|
||||
### Camera framing trace over
|
||||
|
||||
gpu.state.line_width_set(1.0)
|
||||
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||
|
||||
"""
|
||||
## need to accurately detect viewport background color (difficult)
|
||||
### COUNTER CURRENT FRAMING
|
||||
if False:
|
||||
camf = view3d_camera_border_2d(
|
||||
context, context.scene.camera)
|
||||
ca = (camf[0].x + 1, camf[0].y + 1)
|
||||
cb = (camf[1].x + 1, camf[1].y - 1)
|
||||
cc = (camf[2].x - 1, camf[2].y - 1)
|
||||
cd = (camf[3].x - 1, camf[3].y + 1)
|
||||
screen_framing = batch_for_shader(
|
||||
shader_2d, 'LINE_LOOP', {"pos": [ca, cb, cc, cd]})
|
||||
shader_2d.bind()
|
||||
shader_2d.uniform_float("color", (0.25, 0.25, 0.25, 1.0))
|
||||
# shader_2d.uniform_float("color", (0.9, 0.9, 0.9, 1.0))
|
||||
screen_framing.draw(shader_2d)
|
||||
"""
|
||||
### FRAMING
|
||||
|
||||
# frame_color = (0.06, 0.4, 0.040, 0.5)
|
||||
frame_color = (0.0, 0.0, 0.25, 1.0)
|
||||
|
||||
screen_framing = batch_for_shader(
|
||||
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
|
||||
|
||||
shader_2d.bind()
|
||||
shader_2d.uniform_float("color", frame_color)
|
||||
screen_framing.draw(shader_2d)
|
||||
|
||||
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
draw_handle = None
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
global draw_handle
|
||||
draw_handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||
draw_cam_frame_callback_2d, (), "WINDOW", "POST_PIXEL")
|
||||
|
||||
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
global draw_handle
|
||||
if draw_handle:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(draw_handle, 'WINDOW')
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -8,8 +8,6 @@ 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')
|
||||
|
@ -24,12 +22,6 @@ 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)
|
||||
|
|
|
@ -1,165 +1,23 @@
|
|||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
StringProperty,
|
||||
FloatProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
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 o in context.scene.objects:
|
||||
# if o.type != 'GPENCIL':
|
||||
# continue
|
||||
# o.data.edit_line_color[3]=self.edit_lines_opacity
|
||||
for gp in bpy.data.grease_pencils:
|
||||
if not gp.is_annotation:
|
||||
gp.edit_line_color[3]=self.edit_lines_opacity
|
||||
|
||||
|
||||
def update_layer_name(self, context):
|
||||
if not self.layer_name:
|
||||
# never replace by nothing (since there should be prefix/suffix)
|
||||
return
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not context.object.data.layers.active:
|
||||
return
|
||||
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
|
||||
# context.object.data.layers.active.name = self.layer_name
|
||||
|
||||
|
||||
class GP_PG_FixSettings(PropertyGroup):
|
||||
|
||||
|
||||
lock_main_cam : BoolProperty(
|
||||
name="Lock Main Cam",
|
||||
description="Lock the main camera (works only if 'layout' is not in name)",
|
||||
default=False, options={'HIDDEN'})
|
||||
|
||||
set_scene_res : BoolProperty(
|
||||
name="Reset Scene Resolution",
|
||||
description="Set the scene resolution to current prefs project settings",
|
||||
default=False, options={'HIDDEN'})
|
||||
|
||||
set_res_percentage : BoolProperty(
|
||||
name="Reset Resolution Percentage To 100%",
|
||||
description="",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
set_fps : BoolProperty(
|
||||
name="Reset Fps",
|
||||
description="Set the framerate of the scene to current prefs project settings",
|
||||
default=False, options={'HIDDEN'})
|
||||
|
||||
set_slider_n_sync : BoolProperty(
|
||||
name="Dopesheets Show Slider And Sync Range",
|
||||
description="Toggle on the use of show slider and sync range",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
set_gp_use_lights_off : BoolProperty(
|
||||
name="Set Off Use lights On All Gpencil Objects",
|
||||
description="Uncheck Use lights on all grease pencil objects\nAt object level, not layer level (Object properties > Visibility > GP uselight)",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
check_front_axis : BoolProperty(
|
||||
name="Check If Draw Axis is Front (X-Z)",
|
||||
description="Alert if the current grease pencil draw axis is not front (X-Z)",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
check_placement : BoolProperty(
|
||||
name="Check Stroke Placement",
|
||||
description="Alert if the current grease pencil stroke placement is not Origin",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
set_pivot_median_point : BoolProperty(
|
||||
name="Set Pivot To Median Point",
|
||||
description="Change the pivot point to the most used median point",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
disable_guide : BoolProperty(
|
||||
name="Disable Drawing Guide",
|
||||
description="Disable constrained guide in draw mode",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_disabled_anim : BoolProperty(
|
||||
name="List Disabled Animation",
|
||||
description="Alert if there are disabled animations",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_obj_vis_conflict : BoolProperty(
|
||||
name="List Object Visibility Conflicts",
|
||||
description="Alert if some objects have different hide viewport and hide render values",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_gp_mod_vis_conflict : BoolProperty(
|
||||
name="List GP Object Modifiers Visibility Conflicts",
|
||||
description="Alert if some GP modifier have different show viewport and show render values",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
list_broken_mod_targets : BoolProperty(
|
||||
name="List GP Object modifiers broken target",
|
||||
description="Alert if some GP modifier have a target layer not existing in layer stack",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
autokey_add_n_replace : BoolProperty(
|
||||
name="Autokey Mode Add and Replace",
|
||||
description="Change autokey mode back to 'Add & Replace'",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
remove_redundant_strokes : BoolProperty(
|
||||
name="Remove Redundant Strokes",
|
||||
description="Remove GP strokes duplication. When points are exactly identical within the same frame",
|
||||
default=True, options={'HIDDEN'})
|
||||
|
||||
# set_cursor_type : BoolProperty(
|
||||
# name="Set Select Cursor Mode",
|
||||
# description="Set the type of the selection cursor (according to addon prefs)",
|
||||
# default=True, options={'HIDDEN'})
|
||||
|
||||
## default active tool to use
|
||||
select_active_tool : EnumProperty(
|
||||
name="Set Default Selection Tool", description="Active tool to set when launching check fix scene",
|
||||
default='builtin.select_lasso',
|
||||
items=(
|
||||
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
|
||||
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
|
||||
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
|
||||
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
|
||||
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
|
||||
))
|
||||
|
||||
## check file path mapping type
|
||||
file_path_type : EnumProperty(
|
||||
name="Check File Path Mapping", description="Check if all path in scene are relative, absolute or disable the check",
|
||||
default='RELATIVE',
|
||||
items=(
|
||||
('none', 'No Check', 'Do not check for path type', 0),
|
||||
('RELATIVE', 'Relative', 'Check if all path are relative', 1),
|
||||
('ABSOLUTE', 'Absolute', 'Check if all path are absolute', 2),
|
||||
))
|
||||
|
||||
## check set lock object mode
|
||||
lock_object_mode : EnumProperty(
|
||||
name="Set Lock Object Mode", description="Set 'Lock object mode' parameter check'",
|
||||
default='none',
|
||||
items=(
|
||||
('none', 'Do nothing', 'Do not set', 0),
|
||||
('LOCK', 'Lock object mode', 'Toggle lock object mode On', 1),
|
||||
('UNLOCK', 'Unlock object mode', 'Toggle lock object mode Off', 2),
|
||||
))
|
||||
|
||||
class GP_PG_ToolsSettings(PropertyGroup):
|
||||
eraser_radius : IntProperty(
|
||||
name="Eraser Radius", description="Radius of eraser brush",
|
||||
default=20, min=0, max=500, subtype='PIXEL')
|
||||
|
||||
drawcam_passepartout : BoolProperty(
|
||||
name="Show cam passepartout",
|
||||
description="Show a darkened overlay outside image area in Camera view",
|
||||
default=True,
|
||||
options={'HIDDEN'})
|
||||
|
||||
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
|
||||
autotint_offset : IntProperty(
|
||||
name="Tint hue offset", description="offset the tint by this value for better color",
|
||||
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1,
|
||||
|
@ -178,38 +36,14 @@ class GP_PG_ToolsSettings(PropertyGroup):
|
|||
name='Cursor Follow', description="3D cursor follow active object animation when activated",
|
||||
default=False, update=cursor_follow_update)
|
||||
|
||||
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)
|
||||
edit_lines_opacity : FloatProperty(
|
||||
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils", default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)#, get=None, set=None
|
||||
|
||||
## render
|
||||
name_for_current_render : StringProperty(
|
||||
name="Render_name", description="Name use for render current",
|
||||
default="")
|
||||
|
||||
keyframe_type : EnumProperty(
|
||||
name="Keyframe Filter", description="Only jump to defined keyframe type",
|
||||
default='ALL', options={'HIDDEN', 'SKIP_SAVE'},
|
||||
items=(
|
||||
('ALL', 'All', '', 0), # 'KEYFRAME'
|
||||
('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 1),
|
||||
('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 2),
|
||||
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 3),
|
||||
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 4),
|
||||
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
|
||||
))
|
||||
|
||||
layer_name : StringProperty(
|
||||
name="Layer Name",
|
||||
description="The layer name, should describe the content of the layer",
|
||||
default="",
|
||||
update=update_layer_name)# update=None, get=None, set=None
|
||||
|
||||
"""
|
||||
reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)
|
||||
|
@ -231,41 +65,3 @@ class GP_PG_ToolsSettings(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)
|
237
view3d_utils.py
237
view3d_utils.py
|
@ -1,237 +0,0 @@
|
|||
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