From 257fb557acf90aa0a7975d3ebcafafd344d841e3 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Fri, 24 Sep 2021 18:36:58 +0200 Subject: [PATCH] add render as pdf 0.4.0 - feat: render as pdf - ui: advanced options (prefs, disabled by default) - feat: check numbers of muted outputs - code: added addon prefs --- CHANGELOG.md | 7 ++ OP_render_pdf.py | 272 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 11 +- __init__.py | 8 +- prefs.py | 42 ++++++++ ui.py | 38 +++++-- 6 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 OP_render_pdf.py create mode 100644 prefs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f970d..b24fb34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ Activate / deactivate layer opaticty according to prefix Activate / deactivate all masks using MA layers --> +0.4.0 + +- feat: render as pdf +- ui: advanced options (prefs, disabled by default) +- feat: check numbers of muted outputs +- code: added addon prefs + 0.3.7 - fix: set render scene res at 100% at creation diff --git a/OP_render_pdf.py b/OP_render_pdf.py new file mode 100644 index 0000000..c4f33a1 --- /dev/null +++ b/OP_render_pdf.py @@ -0,0 +1,272 @@ +import bpy +from . import fn +from pathlib import Path +from itertools import groupby +from pprint import pprint as pp +from time import time, strftime + +def export_all_selected_frame_as_svg(): + '''Export All frames (only where there is a frame) of selected layer as svg''' + ### Export operator parameters description + # use_fill (boolean, (optional)) – Fill, Export strokes with fill enabled + + # selected_object_type (enum in ['ACTIVE', 'SELECTED', 'VISIBLE'], (optional)) – + + # Object, Which objects to include in the export + # ACTIVE Active, Include only the active object. + # SELECTED Selected, Include selected objects. + # VISIBLE Visible, Include all visible objects. + # + # stroke_sample (float in [0, 100], (optional)) – Sampling, Precision of stroke sampling. Low values mean a more precise result, and zero disables sampling + # use_normalized_thickness (boolean, (optional)) – Normalize, Export strokes with constant thickness + # use_clip_camera (boolean, (optional)) – Clip Camera, Clip drawings to camera size when export in camera view + + + ## Write an ouput name (folder and image will use this name) + ## if left empty, will use name of active object with 'svg_' prefix + name = '' + only_frames = 1 # put 0 to export whole frame range + ## ---- + + o = bpy.context.object + assert o.type == 'GPENCIL', 'Active object should be GP' + + if only_frames: + frames = [] + for ob in bpy.context.selected_objects: + if ob.type != 'GPENCIL': + continue + frames += [f.frame_number for l in ob.data.layers if not l.hide for f in l.frames if len(f.strokes)] + + if frames: + frames = sorted(list(set(frames))) + else: + frames = [f for f in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1)] + + print(len(frames), 'frames to export') + pass_name = name if name else f'svg_{o.name}' + + + blend = Path(bpy.data.filepath) + + for fnum in frames: + out = f'{pass_name}_{fnum:04d}.svg' + print(out) + folder = blend.parent / 'render' / pass_name + folder.mkdir(parents=True, exist_ok=True) + + fp = folder / out + if fp.exists(): + print(f' already exists: {fp}') + continue + + bpy.context.scene.frame_current = fnum + bpy.ops.wm.gpencil_export_svg(filepath=str(fp), + check_existing=True, + use_fill=True, selected_object_type='SELECTED', # ACTIVE, VISIBLE + stroke_sample=0.0, + use_normalized_thickness=False, + use_clip_camera=True) # False by defaut + + print('Done') + + +def pdf_render(fp): + scn = bpy.context.scene + fp.parent.mkdir(parents=True, exist_ok=True) # mode=0o777 + for fnum in range(scn.frame_start, scn.frame_end + 1): + # print('fnum: ', fnum) + scn.frame_current = fnum + # bpy.ops.wm.gpencil_export_svg(filepath=str(fp), + # check_existing=True, + # use_fill=True, selected_object_type='SELECTED', # ACTIVE, VISIBLE + # stroke_sample=0.0, + # use_normalized_thickness=False, + # use_clip_camera=True) # False by defaut + + # bpy.ops.wm.gpencil_export_pdf(filepath=f'{bpy.path.abspath(str(fp)).rstrip("/")}{fnum:04d}.pdf', + bpy.ops.wm.gpencil_export_pdf(filepath=f'{fp}{fnum:04d}.pdf', + check_existing=False, # True by default + use_fill=True, + selected_object_type='ACTIVE', # VISIBLE, SELECTED + stroke_sample=0, + use_normalized_thickness=False, + frame_mode='ACTIVE') + +class GPEXP_OT_export_as_pdf(bpy.types.Operator): + bl_idname = "gp.export_as_pdf" + bl_label = "export_as_pdf" + bl_description = "Export current layers as pdf" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + # rd_scn = bpy.data.scenes.get('Render') + # if not rd_scn: + # self.report({'ERROR'}, 'Viewlayers needs to be generated first!') + # return {'CANCELLED'} + + + ### store + ## dict all visible objects as key with value : sub dict {layer : hide_bool} + + # obj_vis = [[o, o.hide_viewport, o.hide_render] for o in context.scene.objects if o.type == 'GPENCIL' and not (o.hide_get() or o.hide_viewport)] + t0 = time() + + store = {o: {l: l.hide for l in o.data.layers} for o in context.scene.objects if o.type == 'GPENCIL' and not (o.hide_get() or o.hide_viewport)} + # pp(store) + act = context.object if context.object else None + selection = [o for o in context.selected_objects] + + messages = [] + + ## adaptative resampling on all concerned objects + for ob in store.keys(): + mod = ob.grease_pencil_modifiers.get('resample') + if not mod: + mod = ob.grease_pencil_modifiers.new('resample', 'GP_SIMPLIFY') + mod.mode = 'ADAPTIVE' + mod.factor = 0.001 + + # for ob in context.scene.objects: + for ob in store.keys(): + if ob.type != 'GPENCIL': + continue + + mess = f'--- {ob.name}:' + print(mess) + messages.append(mess) + + ## swap hide other GP object (or just swap select) + # for so in store.keys(): + # so.hide_viewport = True + # ob.hide_viewport = False + + context.view_layer.objects.active = ob # render active only mode + # for o in context.scene.objects: + # o.hide_viewport = True + # ob.hide_viewport = False + + ## manage layers + gpl = ob.data.layers + vl_dicts = {vl_name: list(layer_grp) for vl_name, layer_grp in groupby(gpl, lambda x: x.viewlayer_render)} + for vl_name, layer_list in vl_dicts.items(): + vl = context.scene.view_layers.get(vl_name) + if not vl: + mess = f'/!\ {vl_name} viewlayer not exists : skipped {[l.info for l in layer_list]}' + print(mess) + messages.append(mess) + continue + if vl_name in {'exclude', 'View Layer'}: + continue + + if not vl.use: + mess = f'{vl_name} viewlayer disabled' + print(mess) + messages.append(mess) + continue + + + # Case of initially masked layer ! + hide_ct = 0 + total = len(layer_list) + for l in layer_list: + if store[ob][l]: # get original hide bool + hide_ct += 1 + + if hide_ct == total: + mess = f'/!\ Skip {vl_name}: {hide_ct}/{total} are hided' + print(mess) + messages.append(mess) + continue + elif hide_ct > 0: + mess = f'Warning: {vl_name}: {hide_ct}/{total} are hided' + print(mess) + messages.append(mess) + + # check connections in compositor + ng_name = f'NG_{ob.name}' + ng = context.scene.node_tree.nodes.get(ng_name) + if not ng: + mess = f'Skip {vl_name}: Not found nodegroup {ng_name}' + print(mess) + messages.append(mess) + continue + + ng_socket = ng.outputs.get(vl_name) + + if not ng_socket: + mess = f'Skip {vl_name}: Not found in nodegroup {ng_name} sockets' + print(mess) + messages.append(mess) + continue + + if not len(ng_socket.links): + mess = f' socket is disconnected in {ng_name} nodegroup' + print(mess) + messages.append(mess) + continue + + fo_node = ng_socket.links[0].to_node + fo_socket = ng_socket.links[0].to_socket + + if fo_node.type != 'OUTPUT_FILE': + mess = f'Skip {vl_name}: node is not an output_file {fo_node.name}' + print(mess) + messages.append(mess) + continue + + # fo_socket.name isn't right + + idx = [i for i in fo_node.inputs].index(fo_socket) + subpath = fo_node.file_slots[idx].path + fp = Path(fo_node.base_path.rstrip('/')) / subpath + fp = Path(bpy.path.abspath(str(fp)).rstrip("/")) + + print(f'render {total} layers at: {fp.parent}') #Dbg + + # hide all layer that are: not associated with VL (not in layer_list) or hided initially (store[ob][l]) + for l in gpl: + l.hide = l not in layer_list or store[ob][l] + + for l in gpl: + if not l.hide: + print(f'-> {l.info}') #Dbg + + pdf_render(fp) + print() + + ### restore + for ob, layer_dic in store.items(): + # ob.hide_viewport = False # no need + for l, h in layer_dic.items(): + l.hide = h + + for o in selection: + o.select_set(True) + if act: + context.view_layer.objects.active = act + + # for oviz in obj_vis: + # oviz[0].hide_viewport = oviz[1] + + + self.report({'INFO'}, f'Done ({time()-t0:.1f}s)') + fn.show_message_box(_message=messages, _title='PDF render report') + return {"FINISHED"} + + +classes=( +GPEXP_OT_export_as_pdf, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/README.md b/README.md index 06617ee..a19ffb9 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,12 @@ ## Todo +- on renaming, correct also names in GP modifiers !!! +- opt: multi-merge : also merge merged NG automatically disabling AA without group (or externalise AA node ?) +- opt : How to disable main output + + +## Done - set exlude VL on non-used layers - set real AA nodegroup - -- multi-merge : also merge merged NG automatically disabling AA without group (or externalise AA node ?) -- opt : How to disable main output -- disable all masks \ No newline at end of file +- disable all masks (with filter) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 6fad1fa..398a2cb 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "GP Render", "description": "Organise export of gp layers through compositor output", "author": "Samuel Bernou", - "version": (0, 3, 7), + "version": (0, 4, 0), "blender": (2, 93, 0), "location": "View3D", "warning": "", @@ -18,6 +18,8 @@ from . import OP_connect_toggle from . import OP_manage_outputs from . import OP_scene_switch # from . import OP_check_layer_status +from . import OP_render_pdf +from . import prefs from . import OP_setup_layers from . import ui @@ -27,6 +29,7 @@ def register(): if bpy.app.background: return + prefs.register() OP_add_layer.register() OP_clear.register() OP_clean.register() @@ -35,6 +38,7 @@ def register(): OP_manage_outputs.register() OP_scene_switch.register() # OP_check_layer_status.register() + OP_render_pdf.register() OP_setup_layers.register() ui.register() # bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = PROJ_PGT_settings) @@ -46,6 +50,7 @@ def unregister(): ui.unregister() OP_setup_layers.unregister() # OP_check_layer_status.unregister() + OP_render_pdf.unregister() OP_scene_switch.unregister() OP_manage_outputs.unregister() OP_merge_layers.unregister() @@ -53,6 +58,7 @@ def unregister(): OP_clean.unregister() OP_clear.unregister() OP_add_layer.unregister() + prefs.unregister() # del bpy.types.Scene.pgroup_name diff --git a/prefs.py b/prefs.py new file mode 100644 index 0000000..d15cf5d --- /dev/null +++ b/prefs.py @@ -0,0 +1,42 @@ +import bpy + +class gp_render_prefs(bpy.types.AddonPreferences): + bl_idname = __name__.split('.')[0] + + # resample_otf : bpy.props.BoolProperty( + # name='Resample on the fly', + # description="Allow smoother stroke when using pinch\nnote that stroke using textured materials will not be resampled", + # default=True) + + advanced : bpy.props.BoolProperty( + name='Advanced Options', # Reproject On Guessed Plane + description="Display advanced options", + default=False) + + def draw(self, context): + layout = self.layout + layout.prop(self, "advanced") + +def get_addon_prefs(): + ''' + function to read current addon preferences properties + access with : get_addon_prefs().super_special_option + ''' + import os + addon_name = os.path.splitext(__name__)[0] + preferences = bpy.context.preferences + addon_prefs = preferences.addons[addon_name].preferences + return (addon_prefs) + + +classes=( +gp_render_prefs, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/ui.py b/ui.py index 0ec52e9..3d98e6e 100644 --- a/ui.py +++ b/ui.py @@ -1,5 +1,6 @@ import bpy from bpy.types import Panel +from .prefs import get_addon_prefs # from .preferences import get_addon_prefs # Node view panel @@ -11,6 +12,8 @@ class GPEXP_PT_gp_node_ui(Panel): bl_label = "Gpencil Render Manager" def draw(self, context): + prefs = get_addon_prefs() + advanced = prefs.advanced layout = self.layout layout.operator('gp.render_scene_switch', icon='SCENE_DATA', text='Switch Scene') @@ -38,12 +41,15 @@ class GPEXP_PT_gp_node_ui(Panel): # layout.label(text=f'{exclude_count} Excluded View Layers !') layout.operator('gp.enable_all_viewlayers', text=f'Reactivate {exclude_count} Excluded View Layers') + if not scn.use_nodes or not scn.node_tree: return + + disabled_output = [n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.mute] + if disabled_output: + layout.label(text=f'{len(disabled_output)} Output Muted', icon='INFO') layout.separator() - # TODO : add advanced bool checkbox to hide some options from the user - layout.label(text='View layers:') ct = len([n for n in context.scene.node_tree.nodes if n.type == 'R_LAYERS' and n.select]) @@ -52,12 +58,12 @@ class GPEXP_PT_gp_node_ui(Panel): col.operator('gp.activate_only_selected_layers', text=f'Activate Only {ct} Layer Nodes') col.enabled = ct > 0 - col = layout.column(align=True) - - txt = f'Merge {ct} Layer Nodes' - col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text=txt).disconnect = True - col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text='Merge (keep connect)').disconnect = False - col.enabled = ct > 1 + if advanced: + col = layout.column(align=True) + txt = f'Merge {ct} Layer Nodes' + col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text=txt).disconnect = True + col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text='Merge (keep connect)').disconnect = False + col.enabled = ct > 1 layout.separator() col = layout.column() @@ -83,7 +89,7 @@ class GPEXP_PT_gp_node_ui(Panel): col=layout.column() col.label(text='Clean and updates:') - + col.separator() @@ -100,16 +106,19 @@ class GPEXP_PT_gp_node_ui(Panel): # subcol.operator('gp.normalize_outnames', icon='SYNTAX_OFF', text=f'Normalize Paths {ct} Selected Ouptut') # not ready # col.operator('gp.number_outputs', icon='LINENUMBERS_ON', text='Renumber all outputs').mode = 'ALL' - subcol.operator('gp.set_output_node_format', icon='OUTPUT', text='Copy Active Output Format') + if advanced: + subcol.operator('gp.set_output_node_format', icon='OUTPUT', text='Copy Active Output Format') layout.separator() col=layout.column() col.label(text='Delete Options:') - col.operator('gp.clear_render_tree', icon='X', text='Clear Framed Nodes') + if advanced: + col.operator('gp.clear_render_tree', icon='X', text='Clear Framed Nodes') col.operator('gp.clear_render_tree', icon='X', text='Clear & Delete Render Scene').mode = "COMPLETE" + layout.prop(prefs, 'advanced', text='Show Advanced Options') # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'ALL' # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'SELECTED' @@ -170,6 +179,13 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): # row = layout.row() layout.prop(bpy.context.preferences.edit, 'use_anim_channel_group_colors') + + layout.separator() + + row = layout.row() + row.operator('gp.export_as_pdf', icon='RENDER_STILL', text='Render All to PDF Sequences') + if bpy.app.version < (3,0,0): + row.label(text='Not Blender 3.0.0') class GPEXP_MT_multi_user_doc(bpy.types.Menu):