Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCansecoGPC <machaquiro@yahoo.es>2019-12-09 16:42:04 +0300
committerCansecoGPC <machaquiro@yahoo.es>2019-12-09 16:42:04 +0300
commit75af6e5dcf84cc2d2693374a01ecbad0f874701b (patch)
tree86d5ad098857a591e9f997881d762e839b33e98a /amaranth
parent395ca8a4be7a66c72a5556c51f958644601a846b (diff)
Amaranth: Add back from addons contrib
Diffstat (limited to 'amaranth')
-rw-r--r--amaranth/__init__.py119
-rw-r--r--amaranth/animation/__init__.py0
-rw-r--r--amaranth/animation/frame_current.py45
-rw-r--r--amaranth/animation/jump_frames.py209
-rw-r--r--amaranth/animation/motion_paths.py144
-rw-r--r--amaranth/animation/timeline_extra_info.py67
-rw-r--r--amaranth/misc/__init__.py0
-rw-r--r--amaranth/misc/color_management.py84
-rw-r--r--amaranth/misc/dupli_group_id.py197
-rw-r--r--amaranth/misc/sequencer_extra_info.py67
-rw-r--r--amaranth/misc/toggle_wire.py134
-rw-r--r--amaranth/modeling/__init__.py0
-rw-r--r--amaranth/modeling/symmetry_tools.py189
-rw-r--r--amaranth/node_editor/__init__.py0
-rw-r--r--amaranth/node_editor/display_image.py105
-rw-r--r--amaranth/node_editor/id_panel.py154
-rw-r--r--amaranth/node_editor/node_shader_extra.py40
-rw-r--r--amaranth/node_editor/node_stats.py45
-rw-r--r--amaranth/node_editor/normal_node.py83
-rw-r--r--amaranth/node_editor/simplify_nodes.py147
-rw-r--r--amaranth/node_editor/switch_material.py68
-rw-r--r--amaranth/node_editor/templates/__init__.py81
-rw-r--r--amaranth/node_editor/templates/vectorblur.py69
-rw-r--r--amaranth/node_editor/templates/vignette.py100
-rw-r--r--amaranth/prefs.py134
-rw-r--r--amaranth/render/__init__.py0
-rw-r--r--amaranth/render/border_camera.py64
-rw-r--r--amaranth/render/final_resolution.py53
-rw-r--r--amaranth/render/meshlight_add.py194
-rw-r--r--amaranth/render/meshlight_select.py63
-rw-r--r--amaranth/render/passepartout.py45
-rw-r--r--amaranth/render/render_output_z.py53
-rw-r--r--amaranth/render/samples_scene.py254
-rw-r--r--amaranth/scene/__init__.py0
-rw-r--r--amaranth/scene/current_blend.py80
-rwxr-xr-xamaranth/scene/debug.py1408
-rw-r--r--amaranth/scene/goto_library.py90
-rw-r--r--amaranth/scene/material_remove_unassigned.py108
-rw-r--r--amaranth/scene/refresh.py76
-rw-r--r--amaranth/scene/save_reload.py78
-rw-r--r--amaranth/scene/stats.py62
-rw-r--r--amaranth/utils.py64
42 files changed, 4973 insertions, 0 deletions
diff --git a/amaranth/__init__.py b/amaranth/__init__.py
new file mode 100644
index 00000000..3a4dd49f
--- /dev/null
+++ b/amaranth/__init__.py
@@ -0,0 +1,119 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Amaranth
+
+Using Blender every day, you get to change little things on it to speedup
+your workflow. The problem is when you have to switch computers with
+somebody else's Blender, it sucks.
+That's the main reason behind Amaranth. I ported all sort of little changes
+I find useful into this addon.
+
+What is it about? Anything, whatever I think it can speedup workflow,
+I'll try to add it. Enjoy <3
+"""
+
+import sys
+
+# import amaranth's modules
+
+# NOTE: avoid local imports whenever possible!
+# Thanks to Christopher Crouzet for let me know about this.
+# http://stackoverflow.com/questions/13392038/python-making-a-class-variable-static-even-when-a-module-is-imported-in-differe
+
+from amaranth import prefs
+
+from amaranth.modeling import symmetry_tools
+
+from amaranth.scene import (
+ refresh,
+ save_reload,
+ current_blend,
+ stats,
+ goto_library,
+ debug,
+ material_remove_unassigned,
+ )
+
+from amaranth.node_editor import (
+ id_panel,
+ display_image,
+ templates,
+ simplify_nodes,
+ node_stats,
+ normal_node,
+ switch_material,
+ node_shader_extra,
+ )
+
+from amaranth.render import (
+ border_camera,
+ meshlight_add,
+ meshlight_select,
+ passepartout,
+ final_resolution,
+ samples_scene,
+ render_output_z,
+ )
+
+from amaranth.animation import (
+ timeline_extra_info,
+ frame_current,
+ motion_paths,
+ jump_frames,
+ )
+
+from amaranth.misc import (
+ color_management,
+ dupli_group_id,
+ toggle_wire,
+ sequencer_extra_info,
+ )
+
+
+# register the addon + modules found in globals()
+bl_info = {
+ "name": "Amaranth Toolset",
+ "author": "Pablo Vazquez, Bassam Kurdali, Sergey Sharybin, Lukas Tönne, Cesar Saez, CansecoGPC",
+ "version": (1, 0, 8),
+ "blender": (2, 81, 0),
+ "location": "Everywhere!",
+ "description": "A collection of tools and settings to improve productivity",
+ "warning": "",
+ "wiki_url": "https://pablovazquez.art/amaranth",
+ "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
+ "category": "Interface",
+}
+
+
+def _call_globals(attr_name):
+ for m in globals().values():
+ if hasattr(m, attr_name):
+ getattr(m, attr_name)()
+
+
+def _flush_modules(pkg_name):
+ pkg_name = pkg_name.lower()
+ for k in tuple(sys.modules.keys()):
+ if k.lower().startswith(pkg_name):
+ del sys.modules[k]
+
+
+def register():
+ _call_globals("register")
+
+
+def unregister():
+ _call_globals("unregister")
+ _flush_modules("amaranth") # reload amaranth
diff --git a/amaranth/animation/__init__.py b/amaranth/animation/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/animation/__init__.py
diff --git a/amaranth/animation/frame_current.py b/amaranth/animation/frame_current.py
new file mode 100644
index 00000000..6bea2009
--- /dev/null
+++ b/amaranth/animation/frame_current.py
@@ -0,0 +1,45 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Current Frame Slider
+
+Currently the only way to change the current frame is to have a Timeline
+editor open, but sometimes you don't have one, or you're fullscreen.
+This option adds the Current Frame slider to the Specials menu. Find it
+hitting the W menu in Object mode, you can slide or click in the middle
+of the button to set the frame manually.
+"""
+
+import bpy
+
+
+def button_frame_current(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ scene = context.scene
+ if context.preferences.addons["amaranth"].preferences.use_frame_current:
+ self.layout.separator()
+ self.layout.prop(scene, "frame_current", text="Set Current Frame")
+
+
+def register():
+ bpy.types.VIEW3D_MT_object_context_menu.append(button_frame_current)
+ bpy.types.VIEW3D_MT_pose_context_menu.append(button_frame_current)
+
+
+def unregister():
+ bpy.types.VIEW3D_MT_object_context_menu.remove(button_frame_current)
+ bpy.types.VIEW3D_MT_pose_context_menu.remove(button_frame_current)
diff --git a/amaranth/animation/jump_frames.py b/amaranth/animation/jump_frames.py
new file mode 100644
index 00000000..fb12cb35
--- /dev/null
+++ b/amaranth/animation/jump_frames.py
@@ -0,0 +1,209 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Jump X Frames on Shift Up/Down
+
+When you hit Shift Up/Down, you'll jump 10 frames forward/backwards.
+Sometimes is nice to tweak that value.
+
+In the User Preferences, Editing tab, you'll find a "Frames to Jump"
+slider where you can adjust how many frames you'd like to move
+forwards/backwards.
+
+Make sure you save your user settings if you want to use this value from
+now on.
+
+Find it on the User Preferences, Editing.
+"""
+
+import bpy
+from bpy.types import Operator
+from bpy.props import BoolProperty
+
+KEYMAPS = list()
+
+
+# FUNCTION: Check if object has keyframes for a specific frame
+def is_keyframe(ob, frame):
+ if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None:
+ for fcu in ob.animation_data.action.fcurves:
+ if frame in (p.co.x for p in fcu.keyframe_points):
+ return True
+ return False
+
+
+# monkey path is_keyframe function
+bpy.types.Object.is_keyframe = is_keyframe
+
+
+# FEATURE: Jump to frame in-between next and previous keyframe
+class AMTH_SCREEN_OT_keyframe_jump_inbetween(Operator):
+ """Jump to half in-between keyframes"""
+ bl_idname = "screen.amth_keyframe_jump_inbetween"
+ bl_label = "Jump to Keyframe In-between"
+
+ backwards: BoolProperty()
+
+ def execute(self, context):
+ back = self.backwards
+
+ scene = context.scene
+ ob = bpy.context.object
+ frame_start = scene.frame_start
+ frame_end = scene.frame_end
+
+ if not context.scene.get("amth_keyframes_jump"):
+ context.scene["amth_keyframes_jump"] = list()
+
+ keyframes_list = context.scene["amth_keyframes_jump"]
+
+ for f in range(frame_start, frame_end):
+ if ob.is_keyframe(f):
+ keyframes_list = list(keyframes_list)
+ keyframes_list.append(f)
+
+ if keyframes_list:
+ keyframes_list_half = []
+
+ for i, item in enumerate(keyframes_list):
+ try:
+ next_item = keyframes_list[i + 1]
+ keyframes_list_half.append(int((item + next_item) / 2))
+ except:
+ pass
+
+ if len(keyframes_list_half) > 1:
+ if back:
+ v = (scene.frame_current == keyframes_list_half[::-1][-1],
+ scene.frame_current < keyframes_list_half[::-1][-1])
+ if any(v):
+ self.report({"INFO"}, "No keyframes behind")
+ else:
+ for i in keyframes_list_half[::-1]:
+ if scene.frame_current > i:
+ scene.frame_current = i
+ break
+ else:
+ v = (scene.frame_current == keyframes_list_half[-1],
+ scene.frame_current > keyframes_list_half[-1])
+ if any(v):
+ self.report({"INFO"}, "No keyframes ahead")
+ else:
+ for i in keyframes_list_half:
+ if scene.frame_current < i:
+ scene.frame_current = i
+ break
+ else:
+ self.report({"INFO"}, "Object has only 1 keyframe")
+ else:
+ self.report({"INFO"}, "Object has no keyframes")
+
+ return {"FINISHED"}
+
+
+# FEATURE: Jump forward/backward every N frames
+class AMTH_SCREEN_OT_frame_jump(Operator):
+ """Jump a number of frames forward/backwards"""
+ bl_idname = "screen.amaranth_frame_jump"
+ bl_label = "Jump Frames"
+
+ forward: BoolProperty(default=True)
+
+ def execute(self, context):
+ scene = context.scene
+
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return {"CANCELLED"}
+
+ preferences = context.preferences.addons["amaranth"].preferences
+
+ if preferences.use_framerate:
+ framedelta = scene.render.fps
+ else:
+ framedelta = preferences.frames_jump
+ if self.forward:
+ scene.frame_current = scene.frame_current + framedelta
+ else:
+ scene.frame_current = scene.frame_current - framedelta
+
+ return {"FINISHED"}
+
+
+def ui_userpreferences_edit(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ preferences = context.preferences.addons["amaranth"].preferences
+
+ col = self.layout.column()
+ split = col.split(factor=0.21)
+ split.prop(preferences, "frames_jump",
+ text="Frames to Jump")
+
+
+def label(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ layout = self.layout
+
+ if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info:
+ row = layout.row(align=True)
+
+ row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname,
+ icon="PREV_KEYFRAME", text="").backwards = True
+ row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname,
+ icon="NEXT_KEYFRAME", text="").backwards = False
+
+
+def register():
+ bpy.utils.register_class(AMTH_SCREEN_OT_frame_jump)
+ bpy.utils.register_class(AMTH_SCREEN_OT_keyframe_jump_inbetween)
+ bpy.types.USERPREF_PT_animation_timeline.append(ui_userpreferences_edit)
+ bpy.types.USERPREF_PT_animation_timeline.append(label)
+
+ # register keyboard shortcuts
+ wm = bpy.context.window_manager
+ kc = wm.keyconfigs.addon
+
+ km = kc.keymaps.new(name="Frames")
+ kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'UP_ARROW', 'PRESS', shift=True, ctrl=True)
+ kmi.properties.backwards = False
+ KEYMAPS.append((km, kmi))
+
+ kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'DOWN_ARROW', 'PRESS', shift=True, ctrl=True)
+ kmi.properties.backwards = True
+ KEYMAPS.append((km, kmi))
+
+ kmi = km.keymap_items.new(
+ "screen.amaranth_frame_jump", "UP_ARROW", "PRESS", shift=True)
+ kmi.properties.forward = True
+ KEYMAPS.append((km, kmi))
+
+ kmi = km.keymap_items.new(
+ "screen.amaranth_frame_jump", "DOWN_ARROW", "PRESS", shift=True)
+ kmi.properties.forward = False
+ KEYMAPS.append((km, kmi))
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_SCREEN_OT_frame_jump)
+ bpy.utils.unregister_class(AMTH_SCREEN_OT_keyframe_jump_inbetween)
+ bpy.types.USERPREF_PT_animation_timeline.remove(ui_userpreferences_edit)
+ for km, kmi in KEYMAPS:
+ km.keymap_items.remove(kmi)
+ KEYMAPS.clear()
diff --git a/amaranth/animation/motion_paths.py b/amaranth/animation/motion_paths.py
new file mode 100644
index 00000000..7988b452
--- /dev/null
+++ b/amaranth/animation/motion_paths.py
@@ -0,0 +1,144 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Bone Motion Paths:
+
+Match Frame Range + Clear All Paths
+
+* Clear All Paths:
+Silly operator to loop through all bones and clear their paths, useful
+when having hidden bones (othrewise you have to go through each one of
+them and clear manually)
+
+*Match Current Frame Range:
+Set the current frame range as motion path range.
+
+Both requests by Hjalti from Project Pampa
+Thanks to Bassam Kurdali for helping finding out the weirdness behind
+Motion Paths bpy.
+
+Developed during Caminandes Open Movie Project
+"""
+
+import bpy
+
+
+class AMTH_POSE_OT_paths_clear_all(bpy.types.Operator):
+
+ """Clear motion paths from all bones"""
+ bl_idname = "pose.paths_clear_all"
+ bl_label = "Clear All Motion Paths"
+ bl_options = {"UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.mode == "POSE"
+
+ def execute(self, context):
+ # silly but works
+ for b in context.object.data.bones:
+ b.select = True
+ bpy.ops.pose.paths_clear()
+ b.select = False
+ return {"FINISHED"}
+
+
+class AMTH_POSE_OT_paths_frame_match(bpy.types.Operator):
+
+ """Match Start/End frame of scene to motion path range"""
+ bl_idname = "pose.paths_frame_match"
+ bl_label = "Match Frame Range"
+ bl_options = {"UNDO"}
+
+ def execute(self, context):
+ avs = context.object.pose.animation_visualization
+ scene = context.scene
+
+ if avs.motion_path.type == "RANGE":
+ if scene.use_preview_range:
+ avs.motion_path.frame_start = scene.frame_preview_start
+ avs.motion_path.frame_end = scene.frame_preview_end
+ else:
+ avs.motion_path.frame_start = scene.frame_start
+ avs.motion_path.frame_end = scene.frame_end
+
+ else:
+ if scene.use_preview_range:
+ avs.motion_path.frame_before = scene.frame_preview_start
+ avs.motion_path.frame_after = scene.frame_preview_end
+ else:
+ avs.motion_path.frame_before = scene.frame_start
+ avs.motion_path.frame_after = scene.frame_end
+
+ return {"FINISHED"}
+
+
+def pose_motion_paths_ui(self, context):
+
+ layout = self.layout
+ scene = context.scene
+ avs = context.object.pose.animation_visualization
+ if context.active_pose_bone:
+ mpath = context.active_pose_bone.motion_path
+ layout.separator()
+ layout.label(text="Motion Paths Extras:")
+
+ split = layout.split()
+
+ col = split.column(align=True)
+
+ if context.selected_pose_bones:
+ if mpath:
+ sub = col.row(align=True)
+ sub.operator(
+ "pose.paths_update", text="Update Path", icon="BONE_DATA")
+ sub.operator("pose.paths_clear", text="", icon="X")
+ else:
+ col.operator(
+ "pose.paths_calculate",
+ text="Calculate Path",
+ icon="BONE_DATA")
+ else:
+ col.label(text="Select Bones First", icon="ERROR")
+
+ col = split.column(align=True)
+ col.operator(
+ AMTH_POSE_OT_paths_frame_match.bl_idname,
+ text="Set Preview Frame Range" if scene.use_preview_range else "Set Frame Range",
+ icon="PREVIEW_RANGE" if scene.use_preview_range else "TIME")
+
+ col = layout.column()
+ row = col.row(align=True)
+
+ if avs.motion_path.type == "RANGE":
+ row.prop(avs.motion_path, "frame_start", text="Start")
+ row.prop(avs.motion_path, "frame_end", text="End")
+ else:
+ row.prop(avs.motion_path, "frame_before", text="Before")
+ row.prop(avs.motion_path, "frame_after", text="After")
+
+ layout.separator()
+ layout.operator(AMTH_POSE_OT_paths_clear_all.bl_idname, icon="X")
+
+
+def register():
+ bpy.utils.register_class(AMTH_POSE_OT_paths_clear_all)
+ bpy.utils.register_class(AMTH_POSE_OT_paths_frame_match)
+ bpy.types.DATA_PT_display.append(pose_motion_paths_ui)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_POSE_OT_paths_clear_all)
+ bpy.utils.unregister_class(AMTH_POSE_OT_paths_frame_match)
+ bpy.types.DATA_PT_display.remove(pose_motion_paths_ui)
diff --git a/amaranth/animation/timeline_extra_info.py b/amaranth/animation/timeline_extra_info.py
new file mode 100644
index 00000000..0e875b43
--- /dev/null
+++ b/amaranth/animation/timeline_extra_info.py
@@ -0,0 +1,67 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Timeline Extra Info
+
+Display amount of frames left until Frame End, very handy especially when
+rendering an animation or OpenGL preview.
+Display current/end time on SMPTE. Find it on the Timeline header.
+"""
+
+import bpy
+
+
+def label_timeline_extra_info(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ layout = self.layout
+ scene = context.scene
+
+ if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info:
+ row = layout.row(align=True)
+
+ # Check for preview range
+ frame_start = scene.frame_preview_start if scene.use_preview_range else scene.frame_start
+ frame_end = scene.frame_preview_end if scene.use_preview_range else scene.frame_end
+
+ row.label(
+ text="%s / %s" %
+ (bpy.utils.smpte_from_frame(
+ scene.frame_current -
+ frame_start),
+ bpy.utils.smpte_from_frame(
+ frame_end -
+ frame_start)))
+
+ if (scene.frame_current > frame_end):
+ row.label(text="%s Frames Ahead" %
+ ((frame_end - scene.frame_current) * -1))
+ elif (scene.frame_current == frame_start):
+ row.label(text="Start Frame (%s left)" %
+ (frame_end - scene.frame_current))
+ elif (scene.frame_current == frame_end):
+ row.label(text="%s End Frame" % scene.frame_current)
+ else:
+ row.label(text="%s Frames Left" %
+ (frame_end - scene.frame_current))
+
+
+def register():
+ bpy.types.STATUSBAR_HT_header.append(label_timeline_extra_info)
+
+
+def unregister():
+ bpy.types.STATUSBAR_HT_header.remove(label_timeline_extra_info)
diff --git a/amaranth/misc/__init__.py b/amaranth/misc/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/misc/__init__.py
diff --git a/amaranth/misc/color_management.py b/amaranth/misc/color_management.py
new file mode 100644
index 00000000..72ad4bb2
--- /dev/null
+++ b/amaranth/misc/color_management.py
@@ -0,0 +1,84 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Color Management Presets
+
+Save your Color Management options as presets, for easy re-use.
+
+It will pretty much every option in the Color Management panel, such as
+the look, color settings, and so on. Except the curve points (have to
+figure out how to do that nicely), good news is that in Blender 2.69+ you
+can now copy/paste curves.
+"""
+
+import bpy
+from bl_operators.presets import AddPresetBase
+
+
+class AMTH_SCENE_MT_color_management_presets(bpy.types.Menu):
+
+ """List of Color Management presets"""
+ bl_label = "Color Management Presets"
+ preset_subdir = "color"
+ preset_operator = "script.execute_preset"
+ draw = bpy.types.Menu.draw_preset
+
+
+class AMTH_AddPresetColorManagement(AddPresetBase, bpy.types.Operator):
+
+ """Add or remove a Color Management preset"""
+ bl_idname = "scene.color_management_preset_add"
+ bl_label = "Add Color Management Preset"
+ preset_menu = "AMTH_SCENE_MT_color_management_presets"
+
+ preset_defines = [
+ "scene = bpy.context.scene",
+ ]
+
+ preset_values = [
+ "scene.view_settings.view_transform",
+ "scene.display_settings.display_device",
+ "scene.view_settings.exposure",
+ "scene.view_settings.gamma",
+ "scene.view_settings.look",
+ "scene.view_settings.use_curve_mapping",
+ "scene.sequencer_colorspace_settings.name",
+ ]
+
+ preset_subdir = "color"
+
+
+def ui_color_management_presets(self, context):
+
+ layout = self.layout
+
+ row = layout.row(align=True)
+ row.menu("AMTH_SCENE_MT_color_management_presets",
+ text=bpy.types.AMTH_SCENE_MT_color_management_presets.bl_label)
+ row.operator("scene.color_management_preset_add", text="", icon="ZOOM_IN")
+ row.operator("scene.color_management_preset_add",
+ text="", icon="ZOOM_OUT").remove_active = True
+ layout.separator()
+
+
+def register():
+ bpy.utils.register_class(AMTH_AddPresetColorManagement)
+ bpy.utils.register_class(AMTH_SCENE_MT_color_management_presets)
+ bpy.types.RENDER_PT_color_management.prepend(ui_color_management_presets)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_AddPresetColorManagement)
+ bpy.utils.unregister_class(AMTH_SCENE_MT_color_management_presets)
+ bpy.types.RENDER_PT_color_management.remove(ui_color_management_presets)
diff --git a/amaranth/misc/dupli_group_id.py b/amaranth/misc/dupli_group_id.py
new file mode 100644
index 00000000..05fa7129
--- /dev/null
+++ b/amaranth/misc/dupli_group_id.py
@@ -0,0 +1,197 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Object ID for Dupli Groups
+Say you have a linked character or asset, you can now set an Object ID for the
+entire instance (the objects in the group), and use it with the Object Index
+pass later in compositing. Something that I always wanted and it wasn't
+possible!
+
+In order for the Object ID to be loaded afterwards on computers without
+Amaranth installed, it will automatically create a text file (called
+AmaranthStartup.py) and save it inside the .blend, this will autorun on
+startup and set the OB IDs. Remember to have auto-run python scripts on your
+startup preferences.
+
+Set a Pass Index and press "Apply Object ID to Duplis" on the Relations panel,
+Object Properties.
+"""
+
+
+import bpy
+from amaranth.scene.debug import AMTH_SCENE_OT_blender_instance_open
+
+
+# Some settings are bound to be saved on a startup py file
+# TODO: refactor this, amth_text should not be declared as a global variable,
+# otherwise becomes confusing when you call it in the classes below.
+def amaranth_text_startup(context):
+
+ amth_text_name = "AmaranthStartup.py"
+ amth_text_exists = False
+
+ global amth_text
+
+ try:
+ if bpy.data.texts:
+ for tx in bpy.data.texts:
+ if tx.name == amth_text_name:
+ amth_text_exists = True
+ amth_text = bpy.data.texts[amth_text_name]
+ break
+ else:
+ amth_text_exists = False
+
+ if not amth_text_exists:
+ bpy.ops.text.new()
+ amth_text = bpy.data.texts[((len(bpy.data.texts) * -1) + 1)]
+ amth_text.name = amth_text_name
+ amth_text.write("# Amaranth Startup Script\nimport bpy\n")
+ amth_text.use_module = True
+
+ return amth_text_exists
+ except AttributeError:
+ return None
+
+
+# FEATURE: Dupli Group Path
+def ui_dupli_group_library_path(self, context):
+
+ ob = context.object
+
+ row = self.layout.row()
+ row.alignment = "LEFT"
+
+ if ob and ob.instance_collection and ob.instance_collection.library:
+ lib = ob.instance_collection.library.filepath
+
+ row.operator(AMTH_SCENE_OT_blender_instance_open.bl_idname,
+ text="Library: %s" % lib,
+ emboss=False,
+ icon="LINK_BLEND").filepath = lib
+# // FEATURE: Dupli Group Path
+
+
+# FEATURE: Object ID for objects inside DupliGroups
+class AMTH_OBJECT_OT_id_dupligroup(bpy.types.Operator):
+
+ """Set the Object ID for objects in the dupli group"""
+ bl_idname = "object.amaranth_object_id_duplis"
+ bl_label = "Apply Object ID to Duplis"
+
+ clear = False
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object.instance_collection
+
+ def execute(self, context):
+ self.__class__.clear = False
+ ob = context.active_object
+ amaranth_text_startup(context)
+ script_exists = False
+ script_intro = "# OB ID: %s" % ob.name
+ obdata = 'bpy.data.objects[" % s"]' % ob.name
+ # TODO: cleanup script var using format or template strings
+ script = "%s" % (
+ "\nif %(obdata)s and %(obdata)s.instance_collection and %(obdata)s.pass_index != 0: %(obname)s \n"
+ " for dob in %(obdata)s.instance_collection.objects: %(obname)s \n"
+ " dob.pass_index = %(obdata)s.pass_index %(obname)s \n" %
+ {"obdata": obdata, "obname": script_intro})
+
+ for txt in bpy.data.texts:
+ if txt.name == amth_text.name:
+ for li in txt.lines:
+ if script_intro == li.body:
+ script_exists = True
+ continue
+
+ if not script_exists:
+ amth_text.write("\n")
+ amth_text.write(script_intro)
+ amth_text.write(script)
+
+ if ob and ob.instance_collection:
+ if ob.pass_index != 0:
+ for dob in ob.instance_collection.objects:
+ dob.pass_index = ob.pass_index
+
+ self.report({"INFO"},
+ "%s ID: %s to all objects in this Dupli Group" % (
+ "Applied" if not script_exists else "Updated",
+ ob.pass_index))
+
+ return {"FINISHED"}
+
+
+class AMTH_OBJECT_OT_id_dupligroup_clear(bpy.types.Operator):
+
+ """Clear the Object ID from objects in dupli group"""
+ bl_idname = "object.amaranth_object_id_duplis_clear"
+ bl_label = "Clear Object ID from Duplis"
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object.instance_collection
+
+ def execute(self, context):
+ context.active_object.pass_index = 0
+ AMTH_OBJECT_OT_id_dupligroup.clear = True
+ amth_text_exists = amaranth_text_startup(context)
+ match_first = "# OB ID: %s" % context.active_object.name
+
+ if amth_text_exists:
+ for txt in bpy.data.texts:
+ if txt.name == amth_text.name:
+ for li in txt.lines:
+ if match_first in li.body:
+ li.body = ""
+ continue
+
+ self.report({"INFO"}, "Object IDs back to normal")
+ return {"FINISHED"}
+
+
+def ui_object_id_duplis(self, context):
+
+ if context.active_object.instance_collection:
+ split = self.layout.split()
+ row = split.row(align=True)
+ row.enabled = context.active_object.pass_index != 0
+ row.operator(
+ AMTH_OBJECT_OT_id_dupligroup.bl_idname)
+ row.operator(
+ AMTH_OBJECT_OT_id_dupligroup_clear.bl_idname,
+ icon="X", text="")
+ split.separator()
+
+ if AMTH_OBJECT_OT_id_dupligroup.clear:
+ self.layout.label(text="Next time you save/reload this file, "
+ "object IDs will be back to normal",
+ icon="INFO")
+# // FEATURE: Object ID for objects inside DupliGroups
+
+
+def register():
+ bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup)
+ bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup_clear)
+ bpy.types.OBJECT_PT_instancing.append(ui_dupli_group_library_path)
+ bpy.types.OBJECT_PT_relations.append(ui_object_id_duplis)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup)
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup_clear)
+ bpy.types.OBJECT_PT_instancing.remove(ui_dupli_group_library_path)
+ bpy.types.OBJECT_PT_relations.remove(ui_object_id_duplis)
diff --git a/amaranth/misc/sequencer_extra_info.py b/amaranth/misc/sequencer_extra_info.py
new file mode 100644
index 00000000..2d5d6b79
--- /dev/null
+++ b/amaranth/misc/sequencer_extra_info.py
@@ -0,0 +1,67 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Sequencer: Display Image File Name
+
+When seeking through an image sequence, display the active strips' file name
+for the current frame, and it's [playhead].
+
+Find it on the VSE header.
+"""
+import bpy
+
+
+# FEATURE: Sequencer Extra Info
+def act_strip(context):
+ try:
+ return context.scene.sequence_editor.active_strip
+ except AttributeError:
+ return None
+
+
+def ui_sequencer_extra_info(self, context):
+ layout = self.layout
+ strip = act_strip(context)
+ if strip:
+ seq_type = strip.type
+ if seq_type and seq_type == 'IMAGE':
+ elem = strip.strip_elem_from_frame(context.scene.frame_current)
+ if elem:
+ layout.label(
+ text="%s %s" %
+ (elem.filename, "[%s]" %
+ (context.scene.frame_current - strip.frame_start)))
+
+# // FEATURE: Sequencer Extra Info
+
+
+def register():
+ bpy.types.SEQUENCER_HT_header.append(ui_sequencer_extra_info)
+
+
+def unregister():
+ bpy.types.SEQUENCER_HT_header.remove(ui_sequencer_extra_info)
diff --git a/amaranth/misc/toggle_wire.py b/amaranth/misc/toggle_wire.py
new file mode 100644
index 00000000..3d43660d
--- /dev/null
+++ b/amaranth/misc/toggle_wire.py
@@ -0,0 +1,134 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import bpy
+
+
+# FEATURE: Toggle Wire Display
+class AMTH_OBJECT_OT_wire_toggle(bpy.types.Operator):
+
+ """Turn on/off wire display on mesh objects"""
+ bl_idname = "object.amth_wire_toggle"
+ bl_label = "Display Wireframe"
+ bl_options = {"REGISTER", "UNDO"}
+
+ clear: bpy.props.BoolProperty(
+ default=False, name="Clear Wireframe",
+ description="Clear Wireframe Display")
+
+ def execute(self, context):
+
+ scene = context.scene
+ is_all_scenes = scene.amth_wire_toggle_scene_all
+ is_selected = scene.amth_wire_toggle_is_selected
+ is_all_edges = scene.amth_wire_toggle_edges_all
+ is_optimal = scene.amth_wire_toggle_optimal
+ clear = self.clear
+
+ if is_all_scenes:
+ which = bpy.data.objects
+ elif is_selected:
+ if not context.selected_objects:
+ self.report({"INFO"}, "No selected objects")
+ which = context.selected_objects
+ else:
+ which = scene.objects
+
+ if which:
+ for ob in which:
+ if ob and ob.type in {
+ "MESH", "EMPTY", "CURVE",
+ "META", "SURFACE", "FONT"}:
+
+ ob.show_wire = False if clear else True
+ ob.show_all_edges = is_all_edges
+
+ for mo in ob.modifiers:
+ if mo and mo.type == "SUBSURF":
+ mo.show_only_control_edges = is_optimal
+
+ return {"FINISHED"}
+
+
+def ui_object_wire_toggle(self, context):
+
+ scene = context.scene
+
+ self.layout.separator()
+ col = self.layout.column(align=True)
+ col.label(text="Wireframes:")
+ row = col.row(align=True)
+ row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname,
+ icon="MOD_WIREFRAME", text="Display").clear = False
+ row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname,
+ icon="X", text="Clear").clear = True
+ col.separator()
+ row = col.row(align=True)
+ row.prop(scene, "amth_wire_toggle_edges_all")
+ row.prop(scene, "amth_wire_toggle_optimal")
+ row = col.row(align=True)
+ sub = row.row(align=True)
+ sub.active = not scene.amth_wire_toggle_scene_all
+ sub.prop(scene, "amth_wire_toggle_is_selected")
+ sub = row.row(align=True)
+ sub.active = not scene.amth_wire_toggle_is_selected
+ sub.prop(scene, "amth_wire_toggle_scene_all")
+
+
+def init_properties():
+ scene = bpy.types.Scene
+ scene.amth_wire_toggle_scene_all = bpy.props.BoolProperty(
+ default=False,
+ name="All Scenes",
+ description="Toggle wire on objects in all scenes")
+ scene.amth_wire_toggle_is_selected = bpy.props.BoolProperty(
+ default=False,
+ name="Only Selected Objects",
+ description="Only toggle wire on selected objects")
+ scene.amth_wire_toggle_edges_all = bpy.props.BoolProperty(
+ default=True,
+ name="Draw All Edges",
+ description="Draw all the edges even on coplanar faces")
+ scene.amth_wire_toggle_optimal = bpy.props.BoolProperty(
+ default=False,
+ name="Subsurf Optimal Display",
+ description="Skip drawing/rendering of interior subdivided edges "
+ "on meshes with Subdivision Surface modifier")
+
+
+def clear_properties():
+ props = (
+ 'amth_wire_toggle_is_selected',
+ 'amth_wire_toggle_scene_all',
+ "amth_wire_toggle_edges_all",
+ "amth_wire_toggle_optimal"
+ )
+ wm = bpy.context.window_manager
+ for p in props:
+ if p in wm:
+ del wm[p]
+
+# //FEATURE: Toggle Wire Display
+
+
+def register():
+ init_properties()
+ bpy.utils.register_class(AMTH_OBJECT_OT_wire_toggle)
+ bpy.types.VIEW3D_PT_view3d_properties.append(ui_object_wire_toggle)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_wire_toggle)
+ bpy.types.VIEW3D_PT_view3d_properties.remove(ui_object_wire_toggle)
+ clear_properties()
diff --git a/amaranth/modeling/__init__.py b/amaranth/modeling/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/modeling/__init__.py
diff --git a/amaranth/modeling/symmetry_tools.py b/amaranth/modeling/symmetry_tools.py
new file mode 100644
index 00000000..da342abc
--- /dev/null
+++ b/amaranth/modeling/symmetry_tools.py
@@ -0,0 +1,189 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Symmetry Tools: Find Asymmetric + Make Symmetric (by Sergey Sharybin)
+
+Our character wasn’t completely symmetric in some parts where it was
+supposed to, this could be by moving vertices by mistake or just reasons.
+To fix this in a fast way, Sergey coded this two super useful tools:
+
+* Find Asymmetric:
+Selects vertices that don’t have the same position on the opposite side.
+
+* Make Symmetric:
+Move selected vertices to match the position of those on the other side.
+
+This tools may not apply on every single model out there, but I tried it
+in many different characters and it worked. So probably better use it on
+those models that were already symmetric at some point, modeled with a
+mirror modifier or so.
+Search (spacebar) for "Find Asymmetric", and "Make Symmetric""Settings".
+
+> Developed during Caminandes Open Movie Project
+"""
+
+import bpy
+import bmesh
+from mathutils import Vector
+
+
+class AMTH_MESH_OT_find_asymmetric(bpy.types.Operator):
+
+ """
+ Find asymmetric vertices
+ """
+
+ bl_idname = "mesh.find_asymmetric"
+ bl_label = "Find Asymmetric"
+ bl_options = {"UNDO", "REGISTER"}
+
+ @classmethod
+ def poll(cls, context):
+ object = context.object
+ if object:
+ return object.mode == "EDIT" and object.type == "MESH"
+ return False
+
+ def execute(self, context):
+ threshold = 1e-6
+
+ object = context.object
+ bm = bmesh.from_edit_mesh(object.data)
+
+ # Deselect all the vertices
+ for v in bm.verts:
+ v.select = False
+
+ for v1 in bm.verts:
+ if abs(v1.co[0]) < threshold:
+ continue
+
+ mirror_found = False
+ for v2 in bm.verts:
+ if v1 == v2:
+ continue
+ if v1.co[0] * v2.co[0] > 0.0:
+ continue
+
+ mirror_coord = Vector(v2.co)
+ mirror_coord[0] *= -1
+ if (mirror_coord - v1.co).length_squared < threshold:
+ mirror_found = True
+ break
+ if not mirror_found:
+ v1.select = True
+
+ bm.select_flush_mode()
+
+ bmesh.update_edit_mesh(object.data)
+
+ return {"FINISHED"}
+
+
+class AMTH_MESH_OT_make_symmetric(bpy.types.Operator):
+
+ """
+ Make symmetric
+ """
+
+ bl_idname = "mesh.make_symmetric"
+ bl_label = "Make Symmetric"
+ bl_options = {"UNDO", "REGISTER"}
+
+ @classmethod
+ def poll(cls, context):
+ object = context.object
+ if object:
+ return object.mode == "EDIT" and object.type == "MESH"
+ return False
+
+ def execute(self, context):
+ threshold = 1e-6
+
+ object = context.object
+ bm = bmesh.from_edit_mesh(object.data)
+
+ for v1 in bm.verts:
+ if v1.co[0] < threshold:
+ continue
+ if not v1.select:
+ continue
+
+ closest_vert = None
+ closest_distance = -1
+ for v2 in bm.verts:
+ if v1 == v2:
+ continue
+ if v2.co[0] > threshold:
+ continue
+ if not v2.select:
+ continue
+
+ mirror_coord = Vector(v2.co)
+ mirror_coord[0] *= -1
+ distance = (mirror_coord - v1.co).length_squared
+ if closest_vert is None or distance < closest_distance:
+ closest_distance = distance
+ closest_vert = v2
+
+ if closest_vert:
+ closest_vert.select = False
+ closest_vert.co = Vector(v1.co)
+ closest_vert.co[0] *= -1
+ v1.select = False
+
+ for v1 in bm.verts:
+ if v1.select:
+ closest_vert = None
+ closest_distance = -1
+ for v2 in bm.verts:
+ if v1 != v2:
+ mirror_coord = Vector(v2.co)
+ mirror_coord[0] *= -1
+ distance = (mirror_coord - v1.co).length_squared
+ if closest_vert is None or distance < closest_distance:
+ closest_distance = distance
+ closest_vert = v2
+ if closest_vert:
+ v1.select = False
+ v1.co = Vector(closest_vert.co)
+ v1.co[0] *= -1
+
+ bm.select_flush_mode()
+ bmesh.update_edit_mesh(object.data)
+
+ return {"FINISHED"}
+
+
+def ui_symmetry_tools(self, context):
+ if bpy.context.mode == 'EDIT_MESH':
+ self.layout.separator()
+ self.layout.operator(
+ AMTH_MESH_OT_find_asymmetric.bl_idname,
+ icon="ALIGN_CENTER", text="Find Asymmetric")
+ self.layout.operator(
+ AMTH_MESH_OT_make_symmetric.bl_idname,
+ icon="ALIGN_JUSTIFY", text="Make Symmetric")
+
+
+def register():
+ bpy.utils.register_class(AMTH_MESH_OT_find_asymmetric)
+ bpy.utils.register_class(AMTH_MESH_OT_make_symmetric)
+ bpy.types.VIEW3D_MT_edit_mesh.append(ui_symmetry_tools)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_MESH_OT_find_asymmetric)
+ bpy.utils.unregister_class(AMTH_MESH_OT_make_symmetric)
+ bpy.types.VIEW3D_MT_edit_mesh.remove(ui_symmetry_tools)
diff --git a/amaranth/node_editor/__init__.py b/amaranth/node_editor/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/node_editor/__init__.py
diff --git a/amaranth/node_editor/display_image.py b/amaranth/node_editor/display_image.py
new file mode 100644
index 00000000..0dcefca8
--- /dev/null
+++ b/amaranth/node_editor/display_image.py
@@ -0,0 +1,105 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Display Active Image Node on Image Editor
+
+When selecting an Image node, it will show it on the Image editor (if
+there is any available). If you don't like this behavior, you can
+disable it from the Amaranth Toolset panel on the Scene properties.
+Coded by the awesome Sergey Sharybin. This feature only works on Blender
+2.68 and newer. Select an Image Node in the Compositor or Cycles nodes
+editor, there must be at least one image editor available.
+"""
+
+import bpy
+
+
+KEYMAPS = list()
+
+image_nodes = ("CompositorNodeRLayers",
+ "CompositorNodeImage",
+ "CompositorNodeViewer",
+ "CompositorNodeComposite",
+ "ShaderNodeTexImage",
+ "ShaderNodeTexEnvironment")
+
+
+class AMTH_NODE_OT_show_active_node_image(bpy.types.Operator):
+ """Show active image node image in the image editor"""
+ bl_idname = "node.show_active_node_image"
+ bl_label = "Show Active Node Node"
+ bl_options = {"UNDO"}
+
+ def execute(self, context):
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ mlocx = event.mouse_region_x
+ mlocy = event.mouse_region_y
+ select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
+
+ if 'FINISHED' in select_node: # Only run if we're clicking on a node
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return {"CANCELLED"}
+
+ preferences = context.preferences.addons["amaranth"].preferences
+ if preferences.use_image_node_display:
+ if context.active_node:
+ active_node = context.active_node
+
+ if active_node.bl_idname in image_nodes:
+ # Use largest image editor
+ area = None
+ area_size = 0
+ for a in context.screen.areas:
+ if a.type == "IMAGE_EDITOR":
+ size = a.width * a.height
+ if size > area_size:
+ area_size = size
+ area = a
+ if area:
+ for space in area.spaces:
+ if space.type == "IMAGE_EDITOR":
+ if active_node.bl_idname == "CompositorNodeViewer":
+ space.image = bpy.data.images[
+ "Viewer Node"]
+ elif active_node.bl_idname in ["CompositorNodeComposite", "CompositorNodeRLayers"]:
+ space.image = bpy.data.images[
+ "Render Result"]
+ elif active_node.image:
+ space.image = active_node.image
+ break
+ else:
+ return {'CANCELLED'}
+
+ return {"FINISHED"}
+ else:
+ return {"PASS_THROUGH"}
+
+
+def register():
+ bpy.utils.register_class(AMTH_NODE_OT_show_active_node_image)
+ kc = bpy.context.window_manager.keyconfigs.addon
+ km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR")
+ kmi = km.keymap_items.new("node.show_active_node_image",
+ "LEFTMOUSE", "DOUBLE_CLICK")
+ KEYMAPS.append((km, kmi))
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_NODE_OT_show_active_node_image)
+ for km, kmi in KEYMAPS:
+ km.keymap_items.remove(kmi)
+ KEYMAPS.clear()
diff --git a/amaranth/node_editor/id_panel.py b/amaranth/node_editor/id_panel.py
new file mode 100644
index 00000000..09f031da
--- /dev/null
+++ b/amaranth/node_editor/id_panel.py
@@ -0,0 +1,154 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""
+Object / Material Indices Panel
+
+When working with ID Masks in the Nodes Editor, is hard to follow track
+of which objects/materials have which ID.
+This adds a panel on the sidebar when an ID Mask node is selected.
+The active object is highlighted between [square brackets] On the Nodes
+Editor's sidebar, when an ID Mask node is selected.
+"""
+
+import bpy
+
+
+class AMTH_NODE_PT_indices(bpy.types.Panel):
+ bl_space_type = "NODE_EDITOR"
+ bl_region_type = "UI"
+ bl_label = "Object / Material Indices"
+ bl_options = {"DEFAULT_CLOSED"}
+
+ @classmethod
+ def poll(cls, context):
+ node = context.active_node
+ return node and node.type == "ID_MASK"
+
+ def draw(self, context):
+ layout = self.layout
+
+ objects = bpy.data.objects
+ materials = bpy.data.materials
+ node = context.active_node
+
+ show_ob_id = False
+ show_ma_id = False
+ matching_ids = False
+
+ if context.active_object:
+ ob_act = context.active_object
+ else:
+ ob_act = False
+
+ for ob in objects:
+ if ob and ob.pass_index > 0:
+ show_ob_id = True
+ for ma in materials:
+ if ma and ma.pass_index > 0:
+ show_ma_id = True
+ row = layout.row(align=True)
+ row.prop(node, "index", text="Mask Index")
+ row.prop(node, "use_matching_indices", text="Only Matching IDs")
+
+ layout.separator()
+
+ if not show_ob_id and not show_ma_id:
+ layout.label(
+ text="No objects or materials indices so far.", icon="INFO")
+
+ if show_ob_id:
+ split = layout.split()
+ col = split.column()
+ col.label(text="Object Name")
+ split.label(text="ID Number")
+ row = layout.row()
+ for ob in objects:
+ icon = "OUTLINER_DATA_" + ob.type
+ if ob.library:
+ icon = "LIBRARY_DATA_DIRECT"
+ elif ob.is_library_indirect:
+ icon = "LIBRARY_DATA_INDIRECT"
+
+ if ob and node.use_matching_indices \
+ and ob.pass_index == node.index \
+ and ob.pass_index != 0:
+ matching_ids = True
+ row.label(
+ text="[{}]".format(ob.name)
+ if ob_act and ob.name == ob_act.name else ob.name,
+ icon=icon)
+ row.label(text="%s" % ob.pass_index)
+ row = layout.row()
+
+ elif ob and not node.use_matching_indices \
+ and ob.pass_index > 0:
+
+ matching_ids = True
+ row.label(
+ text="[{}]".format(ob.name)
+ if ob_act and ob.name == ob_act.name else ob.name,
+ icon=icon)
+ row.label(text="%s" % ob.pass_index)
+ row = layout.row()
+
+ if node.use_matching_indices and not matching_ids:
+ row.label(text="No objects with ID %s" %
+ node.index, icon="INFO")
+
+ layout.separator()
+
+ if show_ma_id:
+ split = layout.split()
+ col = split.column()
+ col.label(text="Material Name")
+ split.label(text="ID Number")
+ row = layout.row()
+
+ for ma in materials:
+ icon = "BLANK1"
+ if ma.use_nodes:
+ icon = "NODETREE"
+ elif ma.library:
+ icon = "LIBRARY_DATA_DIRECT"
+ if ma.is_library_indirect:
+ icon = "LIBRARY_DATA_INDIRECT"
+
+ if ma and node.use_matching_indices \
+ and ma.pass_index == node.index \
+ and ma.pass_index != 0:
+ matching_ids = True
+ row.label(text="%s" % ma.name, icon=icon)
+ row.label(text="%s" % ma.pass_index)
+ row = layout.row()
+
+ elif ma and not node.use_matching_indices \
+ and ma.pass_index > 0:
+
+ matching_ids = True
+ row.label(text="%s" % ma.name, icon=icon)
+ row.label(text="%s" % ma.pass_index)
+ row = layout.row()
+
+ if node.use_matching_indices and not matching_ids:
+ row.label(text="No materials with ID %s" %
+ node.index, icon="INFO")
+
+
+def register():
+ bpy.utils.register_class(AMTH_NODE_PT_indices)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_NODE_PT_indices)
diff --git a/amaranth/node_editor/node_shader_extra.py b/amaranth/node_editor/node_shader_extra.py
new file mode 100644
index 00000000..7fbaf225
--- /dev/null
+++ b/amaranth/node_editor/node_shader_extra.py
@@ -0,0 +1,40 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+import bpy
+
+
+# FEATURE: Shader Nodes Extra Info
+def node_shader_extra(self, context):
+ if context.space_data.tree_type == 'ShaderNodeTree':
+ ob = context.active_object
+ snode = context.space_data
+ layout = self.layout
+
+ if ob and snode.shader_type == 'OBJECT':
+ if ob.type == 'LAMP':
+ layout.label(text="%s" % ob.name,
+ icon="LAMP_%s" % ob.data.type)
+ else:
+ layout.label(text="%s" % ob.name,
+ icon="OUTLINER_DATA_%s" % ob.type)
+
+# // FEATURE: Shader Nodes Extra Info
+
+
+def register():
+ bpy.types.NODE_HT_header.append(node_shader_extra)
+
+
+def unregister():
+ bpy.types.NODE_HT_header.remove(node_shader_extra)
diff --git a/amaranth/node_editor/node_stats.py b/amaranth/node_editor/node_stats.py
new file mode 100644
index 00000000..796e10dc
--- /dev/null
+++ b/amaranth/node_editor/node_stats.py
@@ -0,0 +1,45 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Nodes Stats
+
+Display the number of selected and total nodes on the compositor. On the
+Compositing Nodes Editor.
+"""
+
+import bpy
+
+
+def node_stats(self, context):
+ if context.scene.node_tree:
+ tree_type = context.space_data.tree_type
+ nodes = context.scene.node_tree.nodes
+ nodes_total = len(nodes.keys())
+ nodes_selected = 0
+ for n in nodes:
+ if n.select:
+ nodes_selected = nodes_selected + 1
+
+ if tree_type == 'CompositorNodeTree':
+ layout = self.layout
+ row = layout.row(align=True)
+ row.label(text="Nodes: %s/%s" % (nodes_selected, str(nodes_total)))
+
+
+def register():
+ bpy.types.NODE_HT_header.append(node_stats)
+
+
+def unregister():
+ bpy.types.NODE_HT_header.remove(node_stats)
diff --git a/amaranth/node_editor/normal_node.py b/amaranth/node_editor/normal_node.py
new file mode 100644
index 00000000..fe77856a
--- /dev/null
+++ b/amaranth/node_editor/normal_node.py
@@ -0,0 +1,83 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Nodes: XYZ Sliders for Normal Node
+
+Tweak the Normal node more accurately by using these sliders. Not the most
+intuitive way to tweak, but it helps.
+
+ProTip: Hit Shift+Drag for moving in very small steps.
+
+Coded by Lukas Töenne. Thanks!
+Find it on the Properties panel, when selecting a Normal node.
+"""
+
+
+import bpy
+from mathutils import Vector
+
+
+# FEATURE: Normal Node Values, by Lukas Tönne
+def init():
+ prop_normal_vector = bpy.props.FloatVectorProperty(
+ name="Normal", size=3, subtype='XYZ',
+ min=-1.0, max=1.0, soft_min=-1.0, soft_max=1.0,
+ get=normal_vector_get, set=normal_vector_set
+ )
+ bpy.types.ShaderNodeNormal.normal_vector = prop_normal_vector
+ bpy.types.CompositorNodeNormal.normal_vector = prop_normal_vector
+
+
+def clear():
+ del bpy.types.ShaderNodeNormal.normal_vector
+ del bpy.types.CompositorNodeNormal.normal_vector
+
+
+def normal_vector_get(self):
+ return self.outputs['Normal'].default_value
+
+
+def normal_vector_set(self, values):
+ # default_value allows un-normalized values,
+ # do this here to prevent awkward results
+ values = Vector(values).normalized()
+ self.outputs['Normal'].default_value = values
+
+
+def act_node(context):
+ try:
+ return context.active_node
+ except AttributeError:
+ return None
+
+
+def ui_node_normal_values(self, context):
+
+ node = act_node(context)
+
+ if act_node:
+ if node and node.type == 'NORMAL':
+ self.layout.prop(node, "normal_vector", text="")
+
+# // FEATURE: Normal Node Values, by Lukas Tönne
+
+
+def register():
+ init()
+ bpy.types.NODE_PT_active_node_properties.append(ui_node_normal_values)
+
+
+def unregister():
+ bpy.types.NODE_PT_active_node_properties.remove(ui_node_normal_values)
+ clear()
diff --git a/amaranth/node_editor/simplify_nodes.py b/amaranth/node_editor/simplify_nodes.py
new file mode 100644
index 00000000..71aec24c
--- /dev/null
+++ b/amaranth/node_editor/simplify_nodes.py
@@ -0,0 +1,147 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Nodes Simplify Panel [WIP Feature]
+
+Disable/Enable certain nodes at a time. Useful to quickly "simplify"
+compositing.
+This feature is a work in progress, the main issue now is when switching
+many different kinds one after the other.
+
+On the Nodes Editor Properties N panel.
+"""
+
+import bpy
+
+
+def init():
+ nodes_compo_types = (
+ ("ALL", "All Types", "", 0),
+ ("BLUR", "Blur", "", 1),
+ ("BOKEHBLUR", "Bokeh Blur", "", 2),
+ ("VECBLUR", "Vector Blur", "", 3),
+ ("DEFOCUS", "Defocus", "", 4),
+ ("R_LAYERS", "Render Layer", "", 5),
+ )
+ node = bpy.types.Node
+ nodes_compo = bpy.types.CompositorNodeTree
+ nodes_compo.types = bpy.props.EnumProperty(
+ items=nodes_compo_types, name="Types")
+ nodes_compo.toggle_mute = bpy.props.BoolProperty(default=False)
+ node.status = bpy.props.BoolProperty(default=False)
+
+
+def clear():
+ wm = bpy.context.window_manager
+ for p in ("types", "toggle_mute", "status"):
+ if wm.get(p):
+ del wm[p]
+
+
+class AMTH_NODE_PT_simplify(bpy.types.Panel):
+
+ bl_space_type = "NODE_EDITOR"
+ bl_region_type = "UI"
+ bl_label = "Simplify"
+ bl_options = {"DEFAULT_CLOSED"}
+
+ @classmethod
+ def poll(cls, context):
+ space = context.space_data
+ return space.type == "NODE_EDITOR" \
+ and space.node_tree is not None \
+ and space.tree_type == "CompositorNodeTree"
+
+ def draw(self, context):
+ layout = self.layout
+ node_tree = context.scene.node_tree
+
+ if node_tree is not None:
+ layout.prop(node_tree, "types")
+ layout.operator(AMTH_NODE_OT_toggle_mute.bl_idname,
+ text="Turn On" if node_tree.toggle_mute else "Turn Off",
+ icon="RESTRICT_VIEW_OFF" if node_tree.toggle_mute else "RESTRICT_VIEW_ON")
+
+ if node_tree.types == "VECBLUR":
+ layout.label(text="This will also toggle the Vector pass {}".format(
+ "on" if node_tree.toggle_mute else "off"), icon="INFO")
+
+
+class AMTH_NODE_OT_toggle_mute(bpy.types.Operator):
+
+ bl_idname = "node.toggle_mute"
+ bl_label = "Toggle Mute"
+
+ def execute(self, context):
+ scene = context.scene
+ node_tree = scene.node_tree
+ node_type = node_tree.types
+ rlayers = scene.render
+
+ if "amaranth_pass_vector" not in scene.keys():
+ scene["amaranth_pass_vector"] = []
+
+ # can"t extend() the list, so make a dummy one
+ pass_vector = scene["amaranth_pass_vector"]
+
+ if not pass_vector:
+ pass_vector = []
+
+ if node_tree.toggle_mute:
+ for node in node_tree.nodes:
+ if node_type == "ALL":
+ node.mute = node.status
+ if node.type == node_type:
+ node.mute = node.status
+ if node_type == "VECBLUR":
+ for layer in rlayers.layers:
+ if layer.name in pass_vector:
+ layer.use_pass_vector = True
+ pass_vector.remove(layer.name)
+
+ node_tree.toggle_mute = False
+
+ else:
+ for node in node_tree.nodes:
+ if node_type == "ALL":
+ node.mute = True
+ if node.type == node_type:
+ node.status = node.mute
+ node.mute = True
+ if node_type == "VECBLUR":
+ for layer in rlayers.layers:
+ if layer.use_pass_vector:
+ pass_vector.append(layer.name)
+ layer.use_pass_vector = False
+ pass
+
+ node_tree.toggle_mute = True
+
+ # Write back to the custom prop
+ pass_vector = sorted(set(pass_vector))
+ scene["amaranth_pass_vector"] = pass_vector
+
+ return {"FINISHED"}
+
+
+def register():
+ init()
+ bpy.utils.register_class(AMTH_NODE_PT_simplify)
+ bpy.utils.register_class(AMTH_NODE_OT_toggle_mute)
+
+
+def unregister():
+ clear()
+ bpy.utils.unregister_class(AMTH_NODE_PT_simplify)
+ bpy.utils.unregister_class(AMTH_NODE_OT_toggle_mute)
diff --git a/amaranth/node_editor/switch_material.py b/amaranth/node_editor/switch_material.py
new file mode 100644
index 00000000..d559965b
--- /dev/null
+++ b/amaranth/node_editor/switch_material.py
@@ -0,0 +1,68 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Material Selector
+
+Quickly switch materials in the active mesh without going to the Properties editor
+
+Based on 'Afeitadora's work on Elysiun
+http://www.elysiun.com/forum/showthread.php?290097-Dynamic-Object-Dropdown-List&p=2361851#post2361851
+
+"""
+
+import bpy
+
+def ui_node_editor_material_select(self, context):
+
+ act_ob = context.active_object
+
+ if act_ob and context.active_object.type in {'MESH', 'CURVE', 'SURFACE', 'META'} and \
+ context.space_data.tree_type == 'ShaderNodeTree' and \
+ context.space_data.shader_type == 'OBJECT':
+
+ if act_ob.active_material:
+ mat_name = act_ob.active_material.name
+ else:
+ mat_name = "No Material"
+
+ self.layout.operator_menu_enum("material.menu_select",
+ "material_select",
+ text=mat_name,
+ icon="MATERIAL")
+
+class AMNodeEditorMaterialSelect(bpy.types.Operator):
+ bl_idname = "material.menu_select"
+ bl_label = "Select Material"
+ bl_description = "Switch to another material in this mesh"
+
+ def avail_materials(self,context):
+ items = [(str(i),x.name,x.name, "MATERIAL", i) for i,x in enumerate(bpy.context.active_object.material_slots)]
+ return items
+ material_select: bpy.props.EnumProperty(items = avail_materials, name = "Available Materials")
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object
+
+ def execute(self,context):
+ bpy.context.active_object.active_material_index = int(self.material_select)
+ return {'FINISHED'}
+
+def register():
+ bpy.utils.register_class(AMNodeEditorMaterialSelect)
+ bpy.types.NODE_HT_header.append(ui_node_editor_material_select)
+
+def unregister():
+ bpy.utils.unregister_class(AMNodeEditorMaterialSelect)
+ bpy.types.NODE_HT_header.remove(ui_node_editor_material_select)
diff --git a/amaranth/node_editor/templates/__init__.py b/amaranth/node_editor/templates/__init__.py
new file mode 100644
index 00000000..90edac48
--- /dev/null
+++ b/amaranth/node_editor/templates/__init__.py
@@ -0,0 +1,81 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Node Templates - Vignette, Vector Blur
+
+Add a set of nodes with one click, in this version I added a "Vignette"
+as first example.
+
+There is no official way to make a vignette, this is just my approach at
+it. Templates: On the Compositor's header, "Template" pulldown. Or hit W.
+Vignette: Adjust the size and position of the vignette with the Ellipse
+Mask's X/Y and width, height values.
+"""
+
+import bpy
+from amaranth.node_editor.templates.vectorblur import AMTH_NODE_OT_AddTemplateVectorBlur
+from amaranth.node_editor.templates.vignette import AMTH_NODE_OT_AddTemplateVignette
+
+
+KEYMAPS = list()
+
+
+# Node Templates Menu
+class AMTH_NODE_MT_amaranth_templates(bpy.types.Menu):
+ bl_idname = 'AMTH_NODE_MT_amaranth_templates'
+ bl_space_type = 'NODE_EDITOR'
+ bl_label = "Templates"
+ bl_description = "List of Amaranth Templates"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(
+ AMTH_NODE_OT_AddTemplateVectorBlur.bl_idname,
+ text="Vector Blur",
+ icon='FORCE_HARMONIC')
+ layout.operator(
+ AMTH_NODE_OT_AddTemplateVignette.bl_idname,
+ text="Vignette",
+ icon='COLOR')
+
+
+def node_templates_pulldown(self, context):
+ if context.space_data.tree_type == 'CompositorNodeTree':
+ layout = self.layout
+ row = layout.row(align=True)
+ row.scale_x = 1.3
+ row.menu("AMTH_NODE_MT_amaranth_templates",
+ icon="NODETREE")
+
+
+def register():
+ bpy.utils.register_class(AMTH_NODE_MT_amaranth_templates)
+ bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVignette)
+ bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVectorBlur)
+ bpy.types.NODE_HT_header.append(node_templates_pulldown)
+ kc = bpy.context.window_manager.keyconfigs.addon
+ km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR")
+ kmi = km.keymap_items.new("wm.call_menu", "W", "PRESS")
+ kmi.properties.name = "AMTH_NODE_MT_amaranth_templates"
+ KEYMAPS.append((km, kmi))
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_NODE_MT_amaranth_templates)
+ bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVignette)
+ bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVectorBlur)
+ bpy.types.NODE_HT_header.remove(node_templates_pulldown)
+ for km, kmi in KEYMAPS:
+ km.keymap_items.remove(kmi)
+ KEYMAPS.clear()
diff --git a/amaranth/node_editor/templates/vectorblur.py b/amaranth/node_editor/templates/vectorblur.py
new file mode 100644
index 00000000..2bc01e48
--- /dev/null
+++ b/amaranth/node_editor/templates/vectorblur.py
@@ -0,0 +1,69 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import bpy
+from mathutils import Vector
+
+
+class AMTH_NODE_OT_AddTemplateVectorBlur(bpy.types.Operator):
+ bl_idname = "node.template_add_vectorblur"
+ bl_label = "Add Vector Blur"
+ bl_description = "Add a vector blur filter"
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ space = context.space_data
+ tree = context.scene.node_tree
+ return space.type == "NODE_EDITOR" \
+ and space.node_tree is not None \
+ and space.tree_type == "CompositorNodeTree" \
+ and tree \
+ and tree.nodes.active \
+ and tree.nodes.active.type == "R_LAYERS"
+
+ def _setupNodes(self, context):
+ scene = context.scene
+ space = context.space_data
+ tree = scene.node_tree
+
+ bpy.ops.node.select_all(action="DESELECT")
+
+ act_node = tree.nodes.active
+ #rlayer = act_node.scene.render.layers[act_node.layer]
+
+ #if not rlayer.use_pass_vector:
+ #rlayer.use_pass_vector = True
+
+ vblur = tree.nodes.new(type="CompositorNodeVecBlur")
+ vblur.use_curved = True
+ vblur.factor = 0.5
+
+ tree.links.new(act_node.outputs["Image"], vblur.inputs["Image"])
+ tree.links.new(act_node.outputs["Depth"], vblur.inputs["Z"])
+ tree.links.new(act_node.outputs["Vector"], vblur.inputs["Speed"])
+
+ if tree.nodes.active:
+ vblur.location = tree.nodes.active.location
+ vblur.location += Vector((250.0, 0.0))
+ else:
+ vblur.location += Vector(
+ (space.cursor_location[0], space.cursor_location[1]))
+
+ vblur.select = True
+
+ def execute(self, context):
+ self._setupNodes(context)
+
+ return {"FINISHED"}
diff --git a/amaranth/node_editor/templates/vignette.py b/amaranth/node_editor/templates/vignette.py
new file mode 100644
index 00000000..5e8ce0a4
--- /dev/null
+++ b/amaranth/node_editor/templates/vignette.py
@@ -0,0 +1,100 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import bpy
+from mathutils import Vector
+
+
+class AMTH_NODE_OT_AddTemplateVignette(bpy.types.Operator):
+ bl_idname = "node.template_add_vignette"
+ bl_label = "Add Vignette"
+ bl_description = "Add a vignette effect"
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ space = context.space_data
+ return space.type == "NODE_EDITOR" \
+ and space.node_tree is not None \
+ and space.tree_type == "CompositorNodeTree"
+
+ # used as reference the setup scene script from master nazgul
+ def _setupNodes(self, context):
+ scene = context.scene
+ space = context.space_data
+ tree = scene.node_tree
+ has_act = True if tree.nodes.active else False
+
+ bpy.ops.node.select_all(action="DESELECT")
+
+ ellipse = tree.nodes.new(type="CompositorNodeEllipseMask")
+ ellipse.width = 0.8
+ ellipse.height = 0.4
+ blur = tree.nodes.new(type="CompositorNodeBlur")
+ blur.use_relative = True
+ blur.factor_x = 30
+ blur.factor_y = 50
+ ramp = tree.nodes.new(type="CompositorNodeValToRGB")
+ ramp.color_ramp.interpolation = "B_SPLINE"
+ ramp.color_ramp.elements[1].color = (0.6, 0.6, 0.6, 1)
+
+ overlay = tree.nodes.new(type="CompositorNodeMixRGB")
+ overlay.blend_type = "OVERLAY"
+ overlay.inputs[0].default_value = 0.8
+ overlay.inputs[1].default_value = (0.5, 0.5, 0.5, 1)
+
+ tree.links.new(ellipse.outputs["Mask"], blur.inputs["Image"])
+ tree.links.new(blur.outputs["Image"], ramp.inputs[0])
+ tree.links.new(ramp.outputs["Image"], overlay.inputs[2])
+ if has_act:
+ tree.links.new(tree.nodes.active.outputs[0], overlay.inputs[1])
+
+ if has_act:
+ overlay.location = tree.nodes.active.location
+ overlay.location += Vector((350.0, 0.0))
+ else:
+ overlay.location += Vector(
+ (space.cursor_location[0], space.cursor_location[1]))
+
+ ellipse.location = overlay.location
+ ellipse.location += Vector((-715.0, -400))
+ ellipse.inputs[0].hide = True
+ ellipse.inputs[1].hide = True
+
+ blur.location = ellipse.location
+ blur.location += Vector((300.0, 0.0))
+ blur.inputs["Size"].hide = True
+
+ ramp.location = blur.location
+ ramp.location += Vector((175.0, 0))
+ ramp.outputs["Alpha"].hide = True
+
+ for node in (ellipse, blur, ramp, overlay):
+ node.select = True
+ node.show_preview = False
+
+ bpy.ops.node.join()
+
+ frame = ellipse.parent
+ frame.label = "Vignette"
+ frame.use_custom_color = True
+ frame.color = (0.1, 0.1, 0.1)
+
+ overlay.parent = None
+ overlay.label = "Vignette Overlay"
+
+ def execute(self, context):
+ self._setupNodes(context)
+
+ return {"FINISHED"}
diff --git a/amaranth/prefs.py b/amaranth/prefs.py
new file mode 100644
index 00000000..3c27c12d
--- /dev/null
+++ b/amaranth/prefs.py
@@ -0,0 +1,134 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import bpy
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+ )
+
+
+class AmaranthToolsetPreferences(bpy.types.AddonPreferences):
+ bl_idname = "amaranth"
+ use_frame_current: BoolProperty(
+ name="Current Frame Slider",
+ description="Set the current frame from the Specials menu in the 3D View",
+ default=True,
+ )
+ use_file_save_reload: BoolProperty(
+ name="Save & Reload File",
+ description="File menu > Save & Reload, or Ctrl + Shift + W",
+ default=True,
+ )
+ use_scene_refresh: BoolProperty(
+ name="Refresh Scene",
+ description="Specials Menu [W]",
+ default=True,
+ )
+ use_timeline_extra_info: BoolProperty(
+ name="Timeline Extra Info",
+ description="Timeline Header",
+ default=True,
+ )
+ use_image_node_display: BoolProperty(
+ name="Active Image Node in Editor",
+ description="Display active node image in image editor",
+ default=True,
+ )
+ use_scene_stats: BoolProperty(
+ name="Extra Scene Statistics",
+ description="Display extra scene statistics in the status bar (may be slow in heavy scenes)",
+ default=False,
+ )
+ frames_jump: IntProperty(
+ name="Frames",
+ description="Number of frames to jump forward/backward",
+ default=10,
+ min=1
+ )
+ use_framerate: BoolProperty(
+ name="Framerate Jump",
+ description="Jump the amount of frames forward/backward that you have set as your framerate",
+ default=False,
+ )
+ use_layers_for_render: BoolProperty(
+ name="Current Layers for Render",
+ description="Save the layers that should be enabled for render",
+ default=True,
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.label(
+ text="Here you can enable or disable specific tools, "
+ "in case they interfere with others or are just plain annoying")
+
+ split = layout.split(factor=0.25)
+
+ col = split.column()
+ sub = col.column(align=True)
+ sub.label(text="3D View", icon="VIEW3D")
+ sub.prop(self, "use_frame_current")
+ sub.prop(self, "use_scene_refresh")
+
+ sub.separator()
+
+ sub.label(text="General", icon="SCENE_DATA")
+ sub.prop(self, "use_file_save_reload")
+ sub.prop(self, "use_timeline_extra_info")
+ sub.prop(self, "use_scene_stats")
+ sub.prop(self, "use_layers_for_render")
+ sub.prop(self, "use_framerate")
+
+ sub.separator()
+
+ sub.label(text="Nodes Editor", icon="NODETREE")
+ sub.prop(self, "use_image_node_display")
+
+ col = split.column()
+ sub = col.column(align=True)
+ sub.label(text="")
+ sub.label(
+ text="Set the current frame from the Specials menu in the 3D View [W]")
+ sub.label(
+ text="Refresh the current Scene. Hotkey: F5 or in Specials menu [W]")
+
+ sub.separator()
+ sub.label(text="") # General icon
+ sub.label(
+ text="Quickly save and reload the current file (no warning!). "
+ "File menu or Ctrl+Shift+W")
+ sub.label(
+ text="SMPTE Timecode and frames left/ahead on Timeline's header")
+ sub.label(
+ text="Display extra stats for Scenes, Cameras, Meshlights (Cycles). Can be slow in heavy scenes")
+ sub.label(
+ text="Save the set of layers that should be activated for a final render")
+ sub.label(
+ text="Jump the amount of frames forward/backward that you've set as your framerate")
+
+ sub.separator()
+ sub.label(text="") # Nodes
+ sub.label(
+ text="When double-clicking an Image node, display it on the Image editor "
+ "(if any)")
+
+
+def register():
+ bpy.utils.register_class(AmaranthToolsetPreferences)
+
+
+def unregister():
+ bpy.utils.unregister_class(AmaranthToolsetPreferences)
diff --git a/amaranth/render/__init__.py b/amaranth/render/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/render/__init__.py
diff --git a/amaranth/render/border_camera.py b/amaranth/render/border_camera.py
new file mode 100644
index 00000000..3d286080
--- /dev/null
+++ b/amaranth/render/border_camera.py
@@ -0,0 +1,64 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Set Camera Bounds as Render Border
+
+When in camera view, we can now set the border-render to be the same size
+of the camera, so we don't render outside the view. Makes faster render
+preview. Under Specials menu W, when in Camera view.
+"""
+
+import bpy
+
+
+class AMTH_VIEW3D_OT_render_border_camera(bpy.types.Operator):
+
+ """Set camera bounds as render border"""
+ bl_idname = "view3d.render_border_camera"
+ bl_label = "Camera as Render Border"
+
+ @classmethod
+ def poll(cls, context):
+ return context.space_data.region_3d.view_perspective == "CAMERA"
+
+ def execute(self, context):
+ render = context.scene.render
+ render.use_border = True
+ render.border_min_x = 0
+ render.border_min_y = 0
+ render.border_max_x = 1
+ render.border_max_y = 1
+
+ return {"FINISHED"}
+
+
+def button_render_border_camera(self, context):
+ view3d = context.space_data.region_3d
+
+ if view3d.view_perspective == "CAMERA":
+ layout = self.layout
+ layout.separator()
+ layout.operator(AMTH_VIEW3D_OT_render_border_camera.bl_idname,
+ text="Camera as Render Border",
+ icon="FULLSCREEN_ENTER")
+
+
+def register():
+ bpy.utils.register_class(AMTH_VIEW3D_OT_render_border_camera)
+ bpy.types.VIEW3D_MT_object_context_menu.append(button_render_border_camera)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_VIEW3D_OT_render_border_camera)
+ bpy.types.VIEW3D_MT_object_context_menu.remove(button_render_border_camera)
diff --git a/amaranth/render/final_resolution.py b/amaranth/render/final_resolution.py
new file mode 100644
index 00000000..deb81a66
--- /dev/null
+++ b/amaranth/render/final_resolution.py
@@ -0,0 +1,53 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+UI: Final Resolution
+
+Always wondered how big the render was going to be when rendering at a
+certain %?
+This feature displays a "Final Resolution" label with the size in pixels
+of your render, it also displays the size for border renders.
+
+On the 'Dimensions' panel, Render properties.
+"""
+import bpy
+
+
+def render_final_resolution_ui(self, context):
+
+ rd = context.scene.render
+ layout = self.layout
+
+ final_res_x = (rd.resolution_x * rd.resolution_percentage) / 100
+ final_res_y = (rd.resolution_y * rd.resolution_percentage) / 100
+
+ if rd.use_border:
+ final_res_x_border = round(
+ (final_res_x * (rd.border_max_x - rd.border_min_x)))
+ final_res_y_border = round(
+ (final_res_y * (rd.border_max_y - rd.border_min_y)))
+ layout.label(text="Final Resolution: {} x {} [Border: {} x {}]".format(
+ str(final_res_x)[:-2], str(final_res_y)[:-2],
+ str(final_res_x_border), str(final_res_y_border)))
+ else:
+ layout.label(text="Final Resolution: {} x {}".format(
+ str(final_res_x)[:-2], str(final_res_y)[:-2]))
+
+
+def register():
+ bpy.types.RENDER_PT_dimensions.append(render_final_resolution_ui)
+
+
+def unregister():
+ bpy.types.RENDER_PT_dimensions.remove(render_final_resolution_ui)
diff --git a/amaranth/render/meshlight_add.py b/amaranth/render/meshlight_add.py
new file mode 100644
index 00000000..a4c4230c
--- /dev/null
+++ b/amaranth/render/meshlight_add.py
@@ -0,0 +1,194 @@
+import bpy
+from mathutils import Vector
+from amaranth.utils import cycles_exists
+
+
+# FEATURE: Add Meshlight
+class AMTH_OBJECT_OT_meshlight_add(bpy.types.Operator):
+
+ """Add a light emitting mesh"""
+ bl_idname = "object.meshlight_add"
+ bl_label = "Add Meshlight"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ single_sided: bpy.props.BoolProperty(
+ name="Single Sided",
+ default=True,
+ description="Only emit light on one side",
+ )
+
+ is_constant: bpy.props.BoolProperty(
+ name="Constant Falloff",
+ default=False,
+ description="Energy is constant (i.e. the Sun), "
+ "independent of how close to the source you are",
+ )
+
+ visible: bpy.props.BoolProperty(
+ name="Visible on Camera",
+ default=False,
+ description="Whether to show the meshlight source on Camera",
+ )
+
+ size: bpy.props.FloatProperty(
+ name="Size",
+ description="Meshlight size. Lower is sharper shadows, higher is softer",
+ min=0.01, max=100.0,
+ default=1.0,
+ )
+
+ strength: bpy.props.FloatProperty(
+ name="Strength",
+ min=0.01, max=100000.0,
+ default=1.5,
+ step=0.25,
+ )
+
+ temperature: bpy.props.FloatProperty(
+ name="Temperature",
+ min=800, max=12000.0,
+ default=5500.0,
+ step=800.0,
+ description="Temperature in Kelvin. Lower is warmer, higher is colder",
+ )
+
+ rotation: bpy.props.FloatVectorProperty(
+ name="Rotation",
+ subtype='EULER',
+ )
+
+ def execute(self, context):
+ scene = context.scene
+ # exists = False
+ number = 1
+
+ for obs in bpy.data.objects:
+ if obs.name.startswith("light_meshlight"):
+ number += 1
+
+ meshlight_name = 'light_meshlight_%.2d' % number
+
+ bpy.ops.mesh.primitive_grid_add(
+ x_subdivisions=4, y_subdivisions=4,
+ rotation=self.rotation, size=self.size)
+
+ bpy.context.object.name = meshlight_name
+ meshlight = scene.objects[meshlight_name]
+ meshlight.show_wire = True
+ meshlight.show_all_edges = True
+
+ material = bpy.data.materials.get(meshlight_name)
+
+ if not material:
+ material = bpy.data.materials.new(meshlight_name)
+
+ bpy.ops.object.material_slot_add()
+ meshlight.active_material = material
+
+ material.use_nodes = True
+ material.diffuse_color = (1, 0.5, 0, 1)
+ nodes = material.node_tree.nodes
+ links = material.node_tree.links
+
+ # clear default nodes to start nice fresh
+ for no in nodes:
+ nodes.remove(no)
+
+ if self.single_sided:
+ geometry = nodes.new(type="ShaderNodeNewGeometry")
+
+ transparency = nodes.new(type="ShaderNodeBsdfTransparent")
+ transparency.inputs[0].default_value = (1, 1, 1, 1)
+ transparency.location = geometry.location
+ transparency.location += Vector((0.0, -55.0))
+
+ emission = nodes.new(type="ShaderNodeEmission")
+ emission.inputs['Strength'].default_value = self.strength
+ emission.location = transparency.location
+ emission.location += Vector((0.0, -80.0))
+
+ blackbody = nodes.new(type="ShaderNodeBlackbody")
+ blackbody.inputs['Temperature'].default_value = self.temperature
+ blackbody.location = emission.location
+ blackbody.location += Vector((-180.0, 0.0))
+ blackbody.label = 'Temperature'
+
+ mix = nodes.new(type="ShaderNodeMixShader")
+ mix.location = geometry.location
+ mix.location += Vector((180.0, 0.0))
+ mix.inputs[2].show_expanded = True
+
+ output = nodes.new(type="ShaderNodeOutputMaterial")
+ output.inputs[1].hide = True
+ output.inputs[2].hide = True
+ output.location = mix.location
+ output.location += Vector((180.0, 0.0))
+
+ # Make links
+ links.new(geometry.outputs['Backfacing'], mix.inputs[0])
+ links.new(transparency.outputs['BSDF'], mix.inputs[1])
+ links.new(emission.outputs['Emission'], mix.inputs[2])
+ links.new(blackbody.outputs['Color'], emission.inputs['Color'])
+ links.new(mix.outputs['Shader'], output.inputs['Surface'])
+
+ for sockets in geometry.outputs:
+ sockets.hide = True
+ else:
+ emission = nodes.new(type="ShaderNodeEmission")
+ emission.inputs['Strength'].default_value = self.strength
+
+ blackbody = nodes.new(type="ShaderNodeBlackbody")
+ blackbody.inputs['Temperature'].default_value = self.temperature
+ blackbody.location = emission.location
+ blackbody.location += Vector((-180.0, 0.0))
+ blackbody.label = 'Temperature'
+
+ output = nodes.new(type="ShaderNodeOutputMaterial")
+ output.inputs[1].hide = True
+ output.inputs[2].hide = True
+ output.location = emission.location
+ output.location += Vector((180.0, 0.0))
+
+ links.new(blackbody.outputs['Color'], emission.inputs['Color'])
+ links.new(emission.outputs['Emission'], output.inputs['Surface'])
+
+ if self.is_constant:
+ falloff = nodes.new(type="ShaderNodeLightFalloff")
+ falloff.inputs['Strength'].default_value = self.strength
+ falloff.location = emission.location
+ falloff.location += Vector((-180.0, -80.0))
+
+ links.new(falloff.outputs['Constant'], emission.inputs['Strength'])
+
+ for sockets in falloff.outputs:
+ sockets.hide = True
+
+ # so it shows slider on properties editor
+ for sockets in emission.inputs:
+ sockets.show_expanded = True
+
+ material.cycles.sample_as_light = True
+ meshlight.cycles_visibility.shadow = False
+ meshlight.cycles_visibility.camera = self.visible
+
+ return {'FINISHED'}
+
+
+def ui_menu_lamps_add(self, context):
+ if cycles_exists() and context.scene.render.engine == 'CYCLES':
+ self.layout.separator()
+ self.layout.operator(
+ AMTH_OBJECT_OT_meshlight_add.bl_idname,
+ icon="LIGHT_AREA", text="Meshlight")
+
+# //FEATURE: Add Meshlight: Single Sided
+
+
+def register():
+ bpy.utils.register_class(AMTH_OBJECT_OT_meshlight_add)
+ bpy.types.VIEW3D_MT_light_add.append(ui_menu_lamps_add)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_meshlight_add)
+ bpy.types.VIEW3D_MT_light_add.remove(ui_menu_lamps_add)
diff --git a/amaranth/render/meshlight_select.py b/amaranth/render/meshlight_select.py
new file mode 100644
index 00000000..cad3a95a
--- /dev/null
+++ b/amaranth/render/meshlight_select.py
@@ -0,0 +1,63 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Select Meshlights
+
+Select all the meshes that emit light. On the header of the 3D View, top
+of the select menu.
+"""
+
+import bpy
+from amaranth import utils
+
+
+class AMTH_OBJECT_OT_select_meshlights(bpy.types.Operator):
+
+ """Select light emitting meshes"""
+ bl_idname = "object.select_meshlights"
+ bl_label = "Select Meshlights"
+ bl_options = {"UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.render.engine == "CYCLES"
+
+ def execute(self, context):
+ # Deselect everything first
+ bpy.ops.object.select_all(action="DESELECT")
+
+ for ob in context.scene.objects:
+ if utils.cycles_is_emission(context, ob):
+ ob.select_set(True)
+ context.view_layer.objects.active = ob
+
+ if not context.selected_objects and not context.view_layer.objects.active:
+ self.report({"INFO"}, "No meshlights to select")
+
+ return {"FINISHED"}
+
+
+def button_select_meshlights(self, context):
+ if utils.cycles_exists() and utils.cycles_active(context):
+ self.layout.operator('object.select_meshlights', icon="LIGHT_SUN")
+
+
+def register():
+ bpy.utils.register_class(AMTH_OBJECT_OT_select_meshlights)
+ bpy.types.VIEW3D_MT_select_object.append(button_select_meshlights)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_select_meshlights)
+ bpy.types.VIEW3D_MT_select_object.remove(button_select_meshlights)
diff --git a/amaranth/render/passepartout.py b/amaranth/render/passepartout.py
new file mode 100644
index 00000000..7d65299c
--- /dev/null
+++ b/amaranth/render/passepartout.py
@@ -0,0 +1,45 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Passepartout on Specials menu
+
+The passepartout value of local cameras is now available on the Specials
+menu for easy access.
+Under Specials menu W, when in Camera view.
+"""
+
+import bpy
+
+
+def button_camera_passepartout(self, context):
+ view3d = context.space_data.region_3d
+ cam = context.scene.camera
+
+ if view3d.view_perspective == "CAMERA":
+ if cam is None or not hasattr(cam, "data") or cam.type != "CAMERA":
+ return
+
+ layout = self.layout
+ if cam.data.show_passepartout:
+ layout.prop(cam.data, "passepartout_alpha", text="Passepartout")
+ else:
+ layout.prop(cam.data, "show_passepartout")
+
+
+def register():
+ bpy.types.VIEW3D_MT_object_context_menu.append(button_camera_passepartout)
+
+
+def unregister():
+ bpy.types.VIEW3D_MT_object_context_menu.remove(button_camera_passepartout)
diff --git a/amaranth/render/render_output_z.py b/amaranth/render/render_output_z.py
new file mode 100644
index 00000000..bbde91ff
--- /dev/null
+++ b/amaranth/render/render_output_z.py
@@ -0,0 +1,53 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+EXR Render: Warn when Z not connected
+Display a little warning label when exporting EXR, with Z Buffer enabled, but
+forgot to plug the Z input in the Compositor.
+
+Might be a bit too specific, but found it nice to remember to plug the Z input
+if we explicitely specify for Z Buffers to be saved (because it's disabled by
+default).
+
+Find it on the Output panel, Render properties.
+"""
+import bpy
+
+
+# // FEATURE: Object ID for objects inside DupliGroups
+# UI: Warning about Z not connected when using EXR
+def ui_render_output_z(self, context):
+
+ scene = bpy.context.scene
+ image = scene.render.image_settings
+ if scene.render.use_compositing and \
+ image.file_format == 'OPEN_EXR' and \
+ image.use_zbuffer:
+ if scene.node_tree and scene.node_tree.nodes:
+ for no in scene.node_tree.nodes:
+ if no.type == 'COMPOSITE':
+ if not no.inputs['Z'].is_linked:
+ self.layout.label(
+ text="The Z output in node \"%s\" is not connected" %
+ no.name, icon="ERROR")
+
+# // UI: Warning about Z not connected
+
+
+def register():
+ bpy.types.RENDER_PT_output.append(ui_render_output_z)
+
+
+def unregister():
+ bpy.types.RENDER_PT_output.remove(ui_render_output_z)
diff --git a/amaranth/render/samples_scene.py b/amaranth/render/samples_scene.py
new file mode 100644
index 00000000..87e94c4c
--- /dev/null
+++ b/amaranth/render/samples_scene.py
@@ -0,0 +1,254 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Cycles: Samples per Scene
+
+When working in production, it's often more convenient to do lighting and
+compositing in different scenes (so you can later append the comp scene
+ to bring together nodes, settings, lamps, RenderLayers).
+
+This would lead to work with more than one scene. When doing render tests
+you want to know at a glance how many samples the other scenes have,
+without manually switching. This is the idea behind the feature.
+
+Find it on the Sampling panel, on Render properties.
+Developed during Caminandes Open Movie Project
+"""
+
+import bpy
+from amaranth import utils
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+ )
+
+
+class AMTH_RENDER_OT_cycles_samples_percentage_set(bpy.types.Operator):
+
+ """Save the current number of samples per shader as final (gets saved in .blend)"""
+ bl_idname = "scene.amaranth_cycles_samples_percentage_set"
+ bl_label = "Set as Render Samples"
+
+ def execute(self, context):
+ cycles = context.scene.cycles
+ cycles.use_samples_final = True
+
+ context.scene["amth_cycles_samples_final"] = [
+ cycles.diffuse_samples,
+ cycles.glossy_samples,
+ cycles.transmission_samples,
+ cycles.ao_samples,
+ cycles.mesh_light_samples,
+ cycles.subsurface_samples,
+ cycles.volume_samples]
+
+ self.report({"INFO"}, "Render Samples Saved")
+
+ return {"FINISHED"}
+
+
+class AMTH_RENDER_OT_cycles_samples_percentage(bpy.types.Operator):
+
+ """Set a percentage of the final render samples"""
+ bl_idname = "scene.amaranth_cycles_samples_percentage"
+ bl_label = "Set Render Samples Percentage"
+
+ percent: IntProperty(
+ name="Percentage",
+ description="Percentage to divide render samples by",
+ subtype="PERCENTAGE", default=0
+ )
+
+ def execute(self, context):
+ percent = self.percent
+ cycles = context.scene.cycles
+ cycles_samples_final = context.scene["amth_cycles_samples_final"]
+
+ cycles.use_samples_final = False
+
+ if percent == 100:
+ cycles.use_samples_final = True
+
+ cycles.diffuse_samples = int((cycles_samples_final[0] / 100) * percent)
+ cycles.glossy_samples = int((cycles_samples_final[1] / 100) * percent)
+ cycles.transmission_samples = int(
+ (cycles_samples_final[2] / 100) * percent)
+ cycles.ao_samples = int((cycles_samples_final[3] / 100) * percent)
+ cycles.mesh_light_samples = int(
+ (cycles_samples_final[4] / 100) * percent)
+ cycles.subsurface_samples = int(
+ (cycles_samples_final[5] / 100) * percent)
+ cycles.volume_samples = int((cycles_samples_final[6] / 100) * percent)
+
+ return {"FINISHED"}
+
+
+def render_cycles_scene_samples(self, context):
+
+ layout = self.layout
+ scene = context.scene
+ render = scene.render
+ if utils.cycles_exists():
+ cscene = scene.cycles
+ list_sampling = scene.amaranth_cycles_list_sampling
+
+ # Set Render Samples
+ if utils.cycles_exists() and cscene.progressive == "BRANCHED_PATH":
+ layout.separator()
+ split = layout.split()
+ col = split.column()
+
+ col.operator(
+ AMTH_RENDER_OT_cycles_samples_percentage_set.bl_idname,
+ text="%s" %
+ "Set as Render Samples" if cscene.use_samples_final else "Set New Render Samples",
+ icon="%s" %
+ "PINNED" if cscene.use_samples_final else "UNPINNED")
+
+ col = split.column()
+ row = col.row(align=True)
+ row.enabled = True if scene.get("amth_cycles_samples_final") else False
+
+ row.operator(
+ AMTH_RENDER_OT_cycles_samples_percentage.bl_idname,
+ text="100%").percent = 100
+ row.operator(
+ AMTH_RENDER_OT_cycles_samples_percentage.bl_idname,
+ text="75%").percent = 75
+ row.operator(
+ AMTH_RENDER_OT_cycles_samples_percentage.bl_idname,
+ text="50%").percent = 50
+ row.operator(
+ AMTH_RENDER_OT_cycles_samples_percentage.bl_idname,
+ text="25%").percent = 25
+
+ # List Samples
+ #if (len(scene.render.layers) > 1) or (len(bpy.data.scenes) > 1):
+ if (len(scene.render.views) > 1) or (len(bpy.data.scenes) > 1):
+
+ box = layout.box()
+ row = box.row(align=True)
+ col = row.column(align=True)
+
+ row = col.row(align=True)
+ row.alignment = "LEFT"
+ row.prop(scene, "amaranth_cycles_list_sampling",
+ icon="%s" % "TRIA_DOWN" if list_sampling else "TRIA_RIGHT",
+ emboss=False)
+
+ if list_sampling:
+ #if len(scene.render.layers) == 1 and render.layers[0].samples == 0:
+ if len(scene.render.views) == 1 and render.view_layers[0].samples == 0:
+ pass
+ else:
+ col.separator()
+ #col.label(text="RenderLayers:", icon="RENDERLAYERS")
+ col.label(text="View Layers:", icon="RENDERLAYERS")
+
+ #for rl in scene.render.layers:
+ for rl in scene.view_layers:
+ row = col.row(align=True)
+ row.label(text=rl.name, icon="BLANK1")
+ row.prop(
+ rl, "samples", text="%s" %
+ "Samples" if rl.samples > 0 else "Automatic (%s)" %
+ (cscene.aa_samples if cscene.progressive == "BRANCHED_PATH" else cscene.samples))
+
+ if (len(bpy.data.scenes) > 1):
+ col.separator()
+
+ col.label(text="Scenes:", icon="SCENE_DATA")
+
+ if utils.cycles_exists() and cscene.progressive == "PATH":
+ for s in bpy.data.scenes:
+ if s != scene:
+ row = col.row(align=True)
+ if s.render.engine == "CYCLES":
+ cscene = s.cycles
+
+ #row.label(s.name)
+ row.label(text=s.name)
+ row.prop(cscene, "samples", icon="BLANK1")
+ else:
+ row.label(
+ text="Scene: '%s' is not using Cycles" %
+ s.name)
+ else:
+ for s in bpy.data.scenes:
+ if s != scene:
+ row = col.row(align=True)
+ if s.render.engine == "CYCLES":
+ cscene = s.cycles
+
+ row.label(text=s.name, icon="BLANK1")
+ row.prop(cscene, "aa_samples",
+ text="AA Samples")
+ else:
+ row.label(
+ text="Scene: '%s' is not using Cycles" %
+ s.name)
+
+
+def init():
+ scene = bpy.types.Scene
+ if utils.cycles_exists():
+ scene.amaranth_cycles_list_sampling = bpy.props.BoolProperty(
+ default=False,
+ name="Samples Per:")
+ # Note: add versioning code to adress changes introduced in 2.79.1
+ if bpy.app.version >= (2, 79, 1):
+ from cycles import properties as _cycles_props
+ _cycles_props.CyclesRenderSettings.use_samples_final = BoolProperty(
+ name="Use Final Render Samples",
+ description="Use current shader samples as final render samples",
+ default=False
+ )
+ else:
+ bpy.types.CyclesRenderSettings.use_samples_final = BoolProperty(
+ name="Use Final Render Samples",
+ description="Use current shader samples as final render samples",
+ default=False
+ )
+
+
+
+def clear():
+ wm = bpy.context.window_manager
+ for p in ("amarath_cycles_list_sampling", "use_samples_final"):
+ if p in wm:
+ del wm[p]
+
+
+def register():
+ init()
+ bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage)
+ bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage_set)
+ if utils.cycles_exists():
+ if bpy.app.version >= (2, 79, 1):
+ bpy.types.CYCLES_RENDER_PT_sampling.append(render_cycles_scene_samples)
+ else:
+ bpy.types.CyclesRender_PT_sampling.append(render_cycles_scene_samples)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage)
+ bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage_set)
+ if utils.cycles_exists():
+ if bpy.app.version >= (2, 79, 1):
+ bpy.types.CYCLES_RENDER_PT_sampling.remove(render_cycles_scene_samples)
+ else:
+ bpy.types.CyclesRender_PT_sampling.remove(render_cycles_scene_samples)
+
+
+ clear()
diff --git a/amaranth/scene/__init__.py b/amaranth/scene/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/amaranth/scene/__init__.py
diff --git a/amaranth/scene/current_blend.py b/amaranth/scene/current_blend.py
new file mode 100644
index 00000000..e3f4ca91
--- /dev/null
+++ b/amaranth/scene/current_blend.py
@@ -0,0 +1,80 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+File Browser > Go to Current Blend's Folder
+
+For when you're lost browsing files and want to go back to the currently
+open blend's directory. Look for it on the File Browser's header, only
+shows up if the file is saved.
+"""
+
+import bpy
+
+# From space_filebrowser.py
+def panel_poll_is_upper_region(region):
+ # The upper region is left-aligned, the lower is split into it then.
+ # Note that after "Flip Regions" it's right-aligned.
+ return region.alignment in {'LEFT', 'RIGHT'}
+
+
+class AMTH_FILE_OT_directory_current_blend(bpy.types.Operator):
+
+ """Go to the directory of the currently open blend file"""
+ bl_idname = "file.directory_current_blend"
+ bl_label = "Current Blend's Folder"
+
+ def execute(self, context):
+ bpy.ops.file.select_bookmark(dir="//")
+ return {"FINISHED"}
+
+
+class FILEBROWSER_PT_amaranth(bpy.types.Panel):
+ bl_space_type = 'FILE_BROWSER'
+ bl_region_type = 'TOOLS'
+ bl_category = "Bookmarks"
+ bl_label = "Amaranth"
+ bl_options = {'HIDE_HEADER'}
+
+ @classmethod
+ def poll(cls, context):
+ return panel_poll_is_upper_region(context.region)
+
+ def draw(self, context):
+ layout = self.layout
+ layout.scale_x = 1.3
+ layout.scale_y = 1.3
+
+ if bpy.data.filepath:
+ row = layout.row()
+ flow = row.grid_flow(row_major=False, columns=0, even_columns=False, even_rows=False, align=True)
+
+ subrow = flow.row()
+ subsubrow = subrow.row(align=True)
+ subsubrow.operator(
+ AMTH_FILE_OT_directory_current_blend.bl_idname,
+ icon="DESKTOP")
+
+
+classes = (
+ AMTH_FILE_OT_directory_current_blend,
+ FILEBROWSER_PT_amaranth
+)
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
diff --git a/amaranth/scene/debug.py b/amaranth/scene/debug.py
new file mode 100755
index 00000000..c4962b5d
--- /dev/null
+++ b/amaranth/scene/debug.py
@@ -0,0 +1,1408 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Scene Debug Panel
+
+This is something I've been wanting to have for a while, a way to know
+certain info about your scene. A way to "debug" it, especially when
+working in production with other teams, this came in very handy.
+
+Being mostly a lighting guy myself, I needed two main features to start with:
+
+* List Cycles Material using X shader
+Where X is any shader type you want. It will display (and print on console)
+a list of all the materials containing the shader you specified above.
+Good for finding out if there's any Meshlight (Emission) material hidden,
+or if there are many glossy shaders making things noisy.
+A current limitation is that it doesn't look inside node groups (yet,
+working on it!). It works since 0.8.8!
+
+Under the "Scene Debug" panel in Scene properties.
+
+* Lighter's Corner
+This is an UI List of Lights in the scene(s).
+It allows you to quickly see how many lights you have, select them by
+clicking on their name, see their type (icon), samples number (if using
+Branched Path Tracing), size, and change their visibility.
+
+"""
+
+# TODO: module cleanup! maybe break it up in a package
+# dicts instead of if, elif, else all over the place.
+# helper functions instead of everything on the execute method.
+# str.format() + dicts instead of inline % op all over the place.
+# remove/manage debug print calls.
+# avoid duplicate code/patterns through helper functions.
+
+import os
+import bpy
+from amaranth import utils
+from bpy.types import (
+ Operator,
+ Panel,
+ UIList,
+ PropertyGroup,
+ )
+from bpy.props import (
+ BoolProperty,
+ CollectionProperty,
+ EnumProperty,
+ IntProperty,
+ PointerProperty,
+ StringProperty,
+ )
+
+# default string used in the List Users for Datablock section menus
+USER_X_NAME_EMPTY = "Data Block not selected/existing"
+
+
+class AMTH_store_data():
+ # used by: AMTH_SCENE_OT_list_users_for_x operator
+ users = {
+ 'OBJECT_DATA': [], # Store Objects with Material
+ 'MATERIAL': [], # Materials (Node tree)
+ 'LIGHT': [], # Lights
+ 'WORLD': [], # World
+ 'TEXTURE': [], # Textures (Psys, Brushes)
+ 'MODIFIER': [], # Modifiers
+ 'MESH_DATA': [], # Vertex Colors
+ 'VIEW3D': [], # Background Images
+ 'NODETREE': [], # Compositor
+ }
+ libraries = [] # Libraries x type
+
+ # used by: AMTH_SCENE_OT_list_missing_material_slots operator
+ obj_mat_slots = [] # Missing material slots
+ obj_mat_slots_lib = [] # Libraries with missing material slots
+
+ # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator
+ mat_shaders = [] # Materials that use a specific shader
+
+ # used by : AMTH_SCENE_OT_list_missing_node_links operator
+ count_groups = 0 # Missing node groups count
+ count_images = 0 # Missing node images
+ count_image_node_unlinked = 0 # Unlinked Image nodes
+
+
+def call_update_datablock_type(self, context):
+ try:
+ # Note: this is pretty weak, but updates the operator enum selection
+ bpy.ops.scene.amth_list_users_for_x_type(list_type_select='0')
+ except:
+ pass
+
+
+def init():
+ scene = bpy.types.Scene
+
+ scene.amaranth_lighterscorner_list_meshlights = BoolProperty(
+ default=False,
+ name="List Meshlights",
+ description="Include light emitting meshes on the list"
+ )
+ amth_datablock_types = (
+ ("IMAGE_DATA", "Image", "Image Datablocks", 0),
+ ("MATERIAL", "Material", "Material Datablocks", 1),
+ ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2),
+ )
+ scene.amth_datablock_types = EnumProperty(
+ items=amth_datablock_types,
+ name="Type",
+ description="Datablock Type",
+ default="MATERIAL",
+ update=call_update_datablock_type,
+ options={"SKIP_SAVE"}
+ )
+ if utils.cycles_exists():
+ cycles_shader_node_types = (
+ ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0),
+ ("BSDF_GLOSSY", "Glossy BSDF", "", 1),
+ ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2),
+ ("BSDF_REFRACTION", "Refraction BSDF", "", 3),
+ ("BSDF_GLASS", "Glass BSDF", "", 4),
+ ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5),
+ ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6),
+ ("BSDF_VELVET", "Velvet BSDF", "", 7),
+ ("BSDF_TOON", "Toon BSDF", "", 8),
+ ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9),
+ ("EMISSION", "Emission", "", 10),
+ ("BSDF_HAIR", "Hair BSDF", "", 11),
+ ("BACKGROUND", "Background", "", 12),
+ ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13),
+ ("HOLDOUT", "Holdout", "", 14),
+ ("VOLUME_ABSORPTION", "Volume Absorption", "", 15),
+ ("VOLUME_SCATTER", "Volume Scatter", "", 16),
+ ("MIX_SHADER", "Mix Shader", "", 17),
+ ("ADD_SHADER", "Add Shader", "", 18),
+ ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19),
+ )
+ scene.amaranth_cycles_node_types = EnumProperty(
+ items=cycles_shader_node_types,
+ name="Shader"
+ )
+
+
+def clear():
+ props = (
+ "amaranth_cycles_node_types",
+ "amaranth_lighterscorner_list_meshlights",
+ )
+ wm = bpy.context.window_manager
+ for p in props:
+ if wm.get(p):
+ del wm[p]
+
+
+def print_with_count_list(text="", send_list=[]):
+ if text:
+ print("\n* {}\n".format(text))
+ if not send_list:
+ print("List is empty, no items to display")
+ return
+
+ for i, entry in enumerate(send_list):
+ print('{:02d}. {}'.format(i + 1, send_list[i]))
+ print("\n")
+
+
+def print_grammar(line="", single="", multi="", cond=[]):
+ phrase = single if len(cond) == 1 else multi
+ print("\n* {} {}:\n".format(line, phrase))
+
+
+def reset_global_storage(what="NONE"):
+ if what == "NONE":
+ return
+
+ if what == "XTYPE":
+ for user in AMTH_store_data.users:
+ AMTH_store_data.users[user] = []
+ AMTH_store_data.libraries = []
+
+ elif what == "MAT_SLOTS":
+ AMTH_store_data.obj_mat_slots[:] = []
+ AMTH_store_data.obj_mat_slots_lib[:] = []
+
+ elif what == "NODE_LINK":
+ AMTH_store_data.obj_mat_slots[:] = []
+ AMTH_store_data.count_groups = 0
+ AMTH_store_data.count_images = 0
+ AMTH_store_data.count_image_node_unlinked = 0
+
+ elif what == "SHADER":
+ AMTH_store_data.mat_shaders[:] = []
+
+
+class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator):
+ """List Cycles materials containing a specific shader"""
+ bl_idname = "scene.cycles_list_nodes"
+ bl_label = "List Materials"
+
+ @classmethod
+ def poll(cls, context):
+ return utils.cycles_exists() and utils.cycles_active(context)
+
+ def execute(self, context):
+ node_type = context.scene.amaranth_cycles_node_types
+ roughness = False
+ shaders_roughness = ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS")
+
+ reset_global_storage("SHADER")
+
+ print("\n=== Cycles Shader Type: {} === \n".format(node_type))
+
+ for ma in bpy.data.materials:
+ if not ma.node_tree:
+ continue
+
+ nodes = ma.node_tree.nodes
+ print_unconnected = (
+ "Note: \nOutput from \"{}\" node in material \"{}\" "
+ "not connected\n".format(node_type, ma.name)
+ )
+
+ for no in nodes:
+ if no.type == node_type:
+ for ou in no.outputs:
+ if ou.links:
+ connected = True
+ if no.type in shaders_roughness:
+ roughness = "R: {:.4f}".format(
+ no.inputs["Roughness"].default_value
+ )
+ else:
+ roughness = False
+ else:
+ connected = False
+ print(print_unconnected)
+
+ if ma.name not in AMTH_store_data.mat_shaders:
+ AMTH_store_data.mat_shaders.append(
+ "%s%s [%s] %s%s%s" %
+ ("[L] " if ma.library else "",
+ ma.name,
+ ma.users,
+ "[F]" if ma.use_fake_user else "",
+ " - [%s]" %
+ roughness if roughness else "",
+ " * Output not connected" if not connected else "")
+ )
+ elif no.type == "GROUP":
+ if no.node_tree:
+ for nog in no.node_tree.nodes:
+ if nog.type == node_type:
+ for ou in nog.outputs:
+ if ou.links:
+ connected = True
+ if nog.type in shaders_roughness:
+ roughness = "R: {:.4f}".format(
+ nog.inputs["Roughness"].default_value
+ )
+ else:
+ roughness = False
+ else:
+ connected = False
+ print(print_unconnected)
+
+ if ma.name not in AMTH_store_data.mat_shaders:
+ AMTH_store_data.mat_shaders.append(
+ '%s%s%s [%s] %s%s%s' %
+ ("[L] " if ma.library else "",
+ "Node Group: %s%s -> " %
+ ("[L] " if no.node_tree.library else "",
+ no.node_tree.name),
+ ma.name,
+ ma.users,
+ "[F]" if ma.use_fake_user else "",
+ " - [%s]" %
+ roughness if roughness else "",
+ " * Output not connected" if not connected else "")
+ )
+ AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders)))
+
+ message = "No materials with nodes type {} found".format(node_type)
+ if len(AMTH_store_data.mat_shaders) > 0:
+ message = "A total of {} {} using {} found".format(
+ len(AMTH_store_data.mat_shaders),
+ "material" if len(AMTH_store_data.mat_shaders) == 1 else "materials",
+ node_type)
+ print_with_count_list(send_list=AMTH_store_data.mat_shaders)
+
+ self.report({'INFO'}, message)
+ AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders)))
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_amaranth_object_select(Operator):
+ """Select object"""
+ bl_idname = "scene.amaranth_object_select"
+ bl_label = "Select Object"
+
+ object_name: StringProperty()
+
+ def execute(self, context):
+ if not (self.object_name and self.object_name in bpy.data.objects):
+ self.report({'WARNING'},
+ "Object with the given name could not be found. Operation Cancelled")
+ return {"CANCELLED"}
+
+ obj = bpy.data.objects[self.object_name]
+
+ bpy.ops.object.select_all(action="DESELECT")
+ obj.select_set(True)
+ context.view_layer.objects.active = obj
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_list_missing_node_links(Operator):
+ """Print a list of missing node links"""
+ bl_idname = "scene.list_missing_node_links"
+ bl_label = "List Missing Node Links"
+
+ def execute(self, context):
+ missing_groups = []
+ missing_images = []
+ image_nodes_unlinked = []
+ libraries = []
+
+ reset_global_storage(what="NODE_LINK")
+
+ for ma in bpy.data.materials:
+ if not ma.node_tree:
+ continue
+
+ for no in ma.node_tree.nodes:
+ if no.type == "GROUP":
+ if not no.node_tree:
+ AMTH_store_data.count_groups += 1
+
+ users_ngroup = []
+
+ for ob in bpy.data.objects:
+ if ob.material_slots and ma.name in ob.material_slots:
+ users_ngroup.append("%s%s%s" % (
+ "[L] " if ob.library else "",
+ "[F] " if ob.use_fake_user else "",
+ ob.name))
+
+ missing_groups.append(
+ "MA: %s%s%s [%s]%s%s%s\n" %
+ ("[L] " if ma.library else "",
+ "[F] " if ma.use_fake_user else "",
+ ma.name,
+ ma.users,
+ " *** No users *** " if ma.users == 0 else "",
+ "\nLI: %s" %
+ ma.library.filepath if ma.library else "",
+ "\nOB: %s" %
+ ", ".join(users_ngroup) if users_ngroup else "")
+ )
+ if ma.library:
+ libraries.append(ma.library.filepath)
+
+ if no.type == "TEX_IMAGE":
+
+ outputs_empty = not no.outputs["Color"].is_linked and \
+ not no.outputs["Alpha"].is_linked
+
+ if no.image:
+ image_path_exists = os.path.exists(
+ bpy.path.abspath(
+ no.image.filepath,
+ library=no.image.library)
+ )
+
+ if outputs_empty or not no.image or not image_path_exists:
+
+ users_images = []
+
+ for ob in bpy.data.objects:
+ if ob.material_slots and ma.name in ob.material_slots:
+ users_images.append("%s%s%s" % (
+ "[L] " if ob.library else "",
+ "[F] " if ob.use_fake_user else "",
+ ob.name))
+
+ if outputs_empty:
+ AMTH_store_data.count_image_node_unlinked += 1
+
+ image_nodes_unlinked.append(
+ "%s%s%s%s%s [%s]%s%s%s%s%s\n" %
+ ("NO: %s" %
+ no.name,
+ "\nMA: ",
+ "[L] " if ma.library else "",
+ "[F] " if ma.use_fake_user else "",
+ ma.name,
+ ma.users,
+ " *** No users *** " if ma.users == 0 else "",
+ "\nLI: %s" %
+ ma.library.filepath if ma.library else "",
+ "\nIM: %s" %
+ no.image.name if no.image else "",
+ "\nLI: %s" %
+ no.image.filepath if no.image and no.image.filepath else "",
+ "\nOB: %s" %
+ ', '.join(users_images) if users_images else ""))
+
+ if not no.image or not image_path_exists:
+ AMTH_store_data.count_images += 1
+
+ missing_images.append(
+ "MA: %s%s%s [%s]%s%s%s%s%s\n" %
+ ("[L] " if ma.library else "",
+ "[F] " if ma.use_fake_user else "",
+ ma.name,
+ ma.users,
+ " *** No users *** " if ma.users == 0 else "",
+ "\nLI: %s" %
+ ma.library.filepath if ma.library else "",
+ "\nIM: %s" %
+ no.image.name if no.image else "",
+ "\nLI: %s" %
+ no.image.filepath if no.image and no.image.filepath else "",
+ "\nOB: %s" %
+ ', '.join(users_images) if users_images else ""))
+
+ if ma.library:
+ libraries.append(ma.library.filepath)
+
+ # Remove duplicates and sort
+ missing_groups = sorted(list(set(missing_groups)))
+ missing_images = sorted(list(set(missing_images)))
+ image_nodes_unlinked = sorted(list(set(image_nodes_unlinked)))
+ libraries = sorted(list(set(libraries)))
+
+ print(
+ "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" %
+ ("No" if AMTH_store_data.count_images == 0 else str(
+ AMTH_store_data.count_images),
+ "node" if AMTH_store_data.count_images == 1 else "nodes",
+ "no" if AMTH_store_data.count_groups == 0 else str(
+ AMTH_store_data.count_groups),
+ "group" if AMTH_store_data.count_groups == 1 else "groups",
+ "no" if AMTH_store_data.count_image_node_unlinked == 0 else str(
+ AMTH_store_data.count_image_node_unlinked),
+ "node" if AMTH_store_data.count_groups == 1 else "nodes")
+ )
+ # List Missing Node Groups
+ if missing_groups:
+ print_with_count_list("Missing Node Group Links", missing_groups)
+
+ # List Missing Image Nodes
+ if missing_images:
+ print_with_count_list("Missing Image Nodes Link", missing_images)
+
+ # List Image Nodes with its outputs unlinked
+ if image_nodes_unlinked:
+ print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked)
+
+ if missing_groups or missing_images or image_nodes_unlinked:
+ if libraries:
+ print_grammar("That's bad, run check", "this library", "these libraries", libraries)
+ print_with_count_list(send_list=libraries)
+ else:
+ self.report({"INFO"}, "Yay! No missing node links")
+
+ if missing_groups and missing_images:
+ self.report(
+ {"WARNING"},
+ "%d missing image %s and %d missing node %s found" %
+ (AMTH_store_data.count_images,
+ "node" if AMTH_store_data.count_images == 1 else "nodes",
+ AMTH_store_data.count_groups,
+ "group" if AMTH_store_data.count_groups == 1 else "groups")
+ )
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_list_missing_material_slots(Operator):
+ """List objects with empty material slots"""
+ bl_idname = "scene.list_missing_material_slots"
+ bl_label = "List Empty Material Slots"
+
+ def execute(self, context):
+ reset_global_storage("MAT_SLOTS")
+
+ for ob in bpy.data.objects:
+ for ma in ob.material_slots:
+ if not ma.material:
+ AMTH_store_data.obj_mat_slots.append('{}{}'.format(
+ '[L] ' if ob.library else '', ob.name))
+ if ob.library:
+ AMTH_store_data.obj_mat_slots_lib.append(ob.library.filepath)
+
+ AMTH_store_data.obj_mat_slots = sorted(list(set(AMTH_store_data.obj_mat_slots)))
+ AMTH_store_data.obj_mat_slots_lib = sorted(list(set(AMTH_store_data.obj_mat_slots_lib)))
+
+ if len(AMTH_store_data.obj_mat_slots) == 0:
+ self.report({"INFO"},
+ "No objects with empty material slots found")
+ return {"FINISHED"}
+
+ print(
+ "\n* A total of {} {} with empty material slots was found \n".format(
+ len(AMTH_store_data.obj_mat_slots),
+ "object" if len(AMTH_store_data.obj_mat_slots) == 1 else "objects")
+ )
+ print_with_count_list(send_list=AMTH_store_data.obj_mat_slots)
+
+ if AMTH_store_data.obj_mat_slots_lib:
+ print_grammar("Check", "this library", "these libraries",
+ AMTH_store_data.obj_mat_slots_lib
+ )
+ print_with_count_list(send_list=AMTH_store_data.obj_mat_slots_lib)
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_list_users_for_x_type(Operator):
+ bl_idname = "scene.amth_list_users_for_x_type"
+ bl_label = "Select"
+ bl_description = "Select Datablock Name"
+
+ @staticmethod
+ def fill_where():
+ where = []
+ data_block = bpy.context.scene.amth_datablock_types
+
+ if data_block == 'IMAGE_DATA':
+ for im in bpy.data.images:
+ if im.name not in {'Render Result', 'Viewer Node'}:
+ where.append(im)
+
+ elif data_block == 'MATERIAL':
+ where = bpy.data.materials
+
+ elif data_block == 'GROUP_VCOL':
+ for ob in bpy.data.objects:
+ if ob.type == 'MESH':
+ for v in ob.data.vertex_colors:
+ if v and v not in where:
+ where.append(v)
+ where = list(set(where))
+
+ return where
+
+ def avail(self, context):
+ datablock_type = bpy.context.scene.amth_datablock_types
+ where = AMTH_SCENE_OT_list_users_for_x_type.fill_where()
+ items = [(str(i), x.name, x.name, datablock_type, i) for i, x in enumerate(where)]
+ items = sorted(list(set(items)))
+ if not items:
+ items = [('0', USER_X_NAME_EMPTY, USER_X_NAME_EMPTY, "INFO", 0)]
+ return items
+
+ list_type_select: EnumProperty(
+ items=avail,
+ name="Available",
+ options={"SKIP_SAVE"}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.scene.amth_datablock_types
+
+ def execute(self, context):
+ where = self.fill_where()
+ bpy.context.scene.amth_list_users_for_x_name = \
+ where[int(self.list_type_select)].name if where else USER_X_NAME_EMPTY
+
+ return {'FINISHED'}
+
+
+class AMTH_SCENE_OT_list_users_for_x(Operator):
+ """List users for a particular datablock"""
+ bl_idname = "scene.amth_list_users_for_x"
+ bl_label = "List Users for Datablock"
+
+ name: StringProperty()
+
+ def execute(self, context):
+ d = bpy.data
+ x = self.name if self.name else context.scene.amth_list_users_for_x_name
+
+ if USER_X_NAME_EMPTY in x:
+ self.report({'INFO'},
+ "Please select a DataBlock name first. Operation Cancelled")
+ return {"CANCELLED"}
+
+ dtype = context.scene.amth_datablock_types
+
+ reset_global_storage("XTYPE")
+
+ # IMAGE TYPE
+ if dtype == 'IMAGE_DATA':
+ # Check Materials
+ for ma in d.materials:
+ # Cycles
+ if utils.cycles_exists():
+ if ma and ma.node_tree and ma.node_tree.nodes:
+ materials = []
+
+ for nd in ma.node_tree.nodes:
+ if nd and nd.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
+ materials.append(nd)
+
+ if nd and nd.type == 'GROUP':
+ if nd.node_tree and nd.node_tree.nodes:
+ for ng in nd.node_tree.nodes:
+ if ng.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}:
+ materials.append(ng)
+
+ for no in materials:
+ if no.image and no.image.name == x:
+ objects = []
+
+ for ob in d.objects:
+ if ma.name in ob.material_slots:
+ objects.append(ob.name)
+ links = False
+
+ for o in no.outputs:
+ if o.links:
+ links = True
+
+ name = '"{0}" {1}{2}'.format(
+ ma.name,
+ 'in object: {0}'.format(objects) if objects else ' (unassigned)',
+ '' if links else ' (unconnected)')
+
+ if name not in AMTH_store_data.users['MATERIAL']:
+ AMTH_store_data.users['MATERIAL'].append(name)
+ # Check Lights
+ for la in d.lights:
+ # Cycles
+ if utils.cycles_exists():
+ if la and la.node_tree and la.node_tree.nodes:
+ for no in la.node_tree.nodes:
+ if no and \
+ no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
+ no.image and no.image.name == x:
+ if la.name not in AMTH_store_data.users['LIGHT']:
+ AMTH_store_data.users['LIGHT'].append(la.name)
+ # Check World
+ for wo in d.worlds:
+ # Cycles
+ if utils.cycles_exists():
+ if wo and wo.node_tree and wo.node_tree.nodes:
+ for no in wo.node_tree.nodes:
+ if no and \
+ no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \
+ no.image and no.image.name == x:
+ if wo.name not in AMTH_store_data.users['WORLD']:
+ AMTH_store_data.users['WORLD'].append(wo.name)
+ # Check Textures
+ for te in d.textures:
+ if te and te.type == 'IMAGE' and te.image:
+ name = te.image.name
+
+ if name == x and \
+ name not in AMTH_store_data.users['TEXTURE']:
+ AMTH_store_data.users['TEXTURE'].append(te.name)
+ # Check Modifiers in Objects
+ for ob in d.objects:
+ for mo in ob.modifiers:
+ if mo.type in {'UV_PROJECT'}:
+ image = mo.image
+
+ if mo and image and image.name == x:
+ name = '"{0}" modifier in {1}'.format(mo.name, ob.name)
+ if name not in AMTH_store_data.users['MODIFIER']:
+ AMTH_store_data.users['MODIFIER'].append(name)
+ # Check Background Images in Viewports
+ for scr in d.screens:
+ for ar in scr.areas:
+ if ar.type == 'VIEW_3D':
+ if ar.spaces and \
+ ar.spaces.active and \
+ ar.spaces.active.background_images:
+ for bg in ar.spaces.active.background_images:
+ image = bg.image
+
+ if bg and image and image.name == x:
+ name = 'Background for 3D Viewport in Screen "{0}"'\
+ .format(scr.name)
+ if name not in AMTH_store_data.users['VIEW3D']:
+ AMTH_store_data.users['VIEW3D'].append(name)
+ # Check the Compositor
+ for sce in d.scenes:
+ if sce.node_tree and sce.node_tree.nodes:
+ nodes = []
+ for nd in sce.node_tree.nodes:
+ if nd.type == 'IMAGE':
+ nodes.append(nd)
+ elif nd.type == 'GROUP':
+ if nd.node_tree and nd.node_tree.nodes:
+ for ng in nd.node_tree.nodes:
+ if ng.type == 'IMAGE':
+ nodes.append(ng)
+
+ for no in nodes:
+ if no.image and no.image.name == x:
+ links = False
+
+ for o in no.outputs:
+ if o.links:
+ links = True
+
+ name = 'Node {0} in Compositor (Scene "{1}"){2}'.format(
+ no.name,
+ sce.name,
+ '' if links else ' (unconnected)')
+
+ if name not in AMTH_store_data.users['NODETREE']:
+ AMTH_store_data.users['NODETREE'].append(name)
+ # MATERIAL TYPE
+ if dtype == 'MATERIAL':
+ # Check Materials - Note: build an object_check list as only strings are stored
+ object_check = [d.objects[names] for names in AMTH_store_data.users['OBJECT_DATA'] if
+ names in d.objects]
+ for ob in d.objects:
+ for ma in ob.material_slots:
+ if ma.name == x:
+ if ma not in object_check:
+ AMTH_store_data.users['OBJECT_DATA'].append(ob.name)
+
+ if ob.library:
+ AMTH_store_data.libraries.append(ob.library.filepath)
+ # VERTEX COLOR TYPE
+ elif dtype == 'GROUP_VCOL':
+ # Check VCOL in Meshes
+ for ob in bpy.data.objects:
+ if ob.type == 'MESH':
+ for v in ob.data.vertex_colors:
+ if v.name == x:
+ name = '{0}'.format(ob.name)
+
+ if name not in AMTH_store_data.users['MESH_DATA']:
+ AMTH_store_data.users['MESH_DATA'].append(name)
+ # Check VCOL in Materials
+ for ma in d.materials:
+ # Cycles
+ if utils.cycles_exists():
+ if ma and ma.node_tree and ma.node_tree.nodes:
+ for no in ma.node_tree.nodes:
+ if no and no.type in {'ATTRIBUTE'}:
+ if no.attribute_name == x:
+ objects = []
+
+ for ob in d.objects:
+ if ma.name in ob.material_slots:
+ objects.append(ob.name)
+
+ if objects:
+ name = '{0} in object: {1}'.format(ma.name, objects)
+ else:
+ name = '{0} (unassigned)'.format(ma.name)
+
+ if name not in AMTH_store_data.users['MATERIAL']:
+ AMTH_store_data.users['MATERIAL'].append(name)
+
+ AMTH_store_data.libraries = sorted(list(set(AMTH_store_data.libraries)))
+
+ # Print on console
+ empty = True
+ for t in AMTH_store_data.users:
+ if AMTH_store_data.users[t]:
+ empty = False
+ print('\n== {0} {1} use {2} "{3}" ==\n'.format(
+ len(AMTH_store_data.users[t]),
+ t,
+ dtype,
+ x))
+ for p in AMTH_store_data.users[t]:
+ print(' {0}'.format(p))
+
+ if AMTH_store_data.libraries:
+ print_grammar("Check", "this library", "these libraries",
+ AMTH_store_data.libraries
+ )
+ print_with_count_list(send_list=AMTH_store_data.libraries)
+
+ if empty:
+ self.report({'INFO'}, "No users for {}".format(x))
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_list_users_debug_clear(Operator):
+ """Clear the list bellow"""
+ bl_idname = "scene.amth_list_users_debug_clear"
+ bl_label = "Clear Debug Panel lists"
+
+ what: StringProperty(
+ name="",
+ default="NONE",
+ options={'HIDDEN'}
+ )
+
+ def execute(self, context):
+ reset_global_storage(self.what)
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_blender_instance_open(Operator):
+ """Open in a new Blender instance"""
+ bl_idname = "scene.blender_instance_open"
+ bl_label = "Open Blender Instance"
+
+ filepath: StringProperty()
+
+ def execute(self, context):
+ if self.filepath:
+ filepath = os.path.normpath(bpy.path.abspath(self.filepath))
+
+ import subprocess
+ try:
+ subprocess.Popen([bpy.app.binary_path, filepath])
+ except:
+ print("Error opening a new Blender instance")
+ import traceback
+ traceback.print_exc()
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_OT_Collection_List_Refresh(Operator):
+ bl_idname = "scene.amaranth_lighters_corner_refresh"
+ bl_label = "Refresh"
+ bl_description = ("Generate/Refresh the Lists\n"
+ "Use to generate/refresh the list or after changes to Data")
+ bl_options = {"REGISTER", "INTERNAL"}
+
+ what: StringProperty(default="NONE")
+
+ def execute(self, context):
+ message = "No changes applied"
+
+ if self.what == "LIGHTS":
+ fill_ligters_corner_props(context, refresh=True)
+
+ found_lights = len(context.window_manager.amth_lighters_state.keys())
+ message = "No Lights in the Data" if found_lights == 0 else \
+ "Generated list for {} found light(s)".format(found_lights)
+
+ elif self.what == "IMAGES":
+ fill_missing_images_props(context, refresh=True)
+
+ found_images = len(context.window_manager.amth_missing_images_state.keys())
+ message = "Great! No missing Images" if found_images == 0 else \
+ "Missing {} image(s) in the Data".format(found_images)
+
+ self.report({'INFO'}, message)
+
+ return {"FINISHED"}
+
+
+class AMTH_SCENE_PT_scene_debug(Panel):
+ """Scene Debug"""
+ bl_label = "Scene Debug"
+ bl_space_type = "PROPERTIES"
+ bl_region_type = "WINDOW"
+ bl_context = "scene"
+ bl_options = {"DEFAULT_CLOSED"}
+
+ def draw_header(self, context):
+ layout = self.layout
+ layout.label(text="", icon="RADIOBUT_ON")
+
+ def draw_label(self, layout, body_text, single, multi, lists, ico="BLANK1"):
+ layout.label(
+ text="{} {} {}".format(
+ str(len(lists)), body_text,
+ single if len(lists) == 1 else multi),
+ icon=ico
+ )
+
+ def draw_miss_link(self, layout, text1, single, multi, text2, count, ico="BLANK1"):
+ layout.label(
+ text="{} {} {} {}".format(
+ count, text1,
+ single if count == 1 else multi, text2),
+ icon=ico
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ scene = context.scene
+
+ has_images = len(bpy.data.images)
+ engine = scene.render.engine
+
+ # List Missing Images
+ box = layout.box()
+ split = box.split(factor=0.8, align=True)
+ row = split.row()
+
+ if has_images:
+ subrow = split.row(align=True)
+ subrow.alignment = "RIGHT"
+ subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname,
+ text="", icon="FILE_REFRESH").what = "IMAGES"
+ image_state = context.window_manager.amth_missing_images_state
+
+ row.label(
+ text="{} Image Blocks present in the Data".format(has_images),
+ icon="IMAGE_DATA"
+ )
+ if len(image_state.keys()) > 0:
+ box.template_list(
+ 'AMTH_UL_MissingImages_UI',
+ 'amth_collection_index_prop',
+ context.window_manager,
+ 'amth_missing_images_state',
+ context.window_manager.amth_collection_index_prop,
+ 'index_image',
+ rows=3
+ )
+ else:
+ row.label(text="No images loaded yet", icon="RIGHTARROW_THIN")
+
+ # List Cycles Materials by Shader
+ if utils.cycles_exists() and engine == "CYCLES":
+ box = layout.box()
+ split = box.split()
+ col = split.column(align=True)
+ col.prop(scene, "amaranth_cycles_node_types",
+ icon="MATERIAL")
+
+ row = split.row(align=True)
+ row.operator(AMTH_SCENE_OT_cycles_shader_list_nodes.bl_idname,
+ icon="SORTSIZE",
+ text="List Materials Using Shader")
+ if len(AMTH_store_data.mat_shaders) != 0:
+ row.operator(
+ AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
+ icon="X", text="").what = "SHADER"
+ col.separator()
+
+ if len(AMTH_store_data.mat_shaders) != 0:
+ col = box.column(align=True)
+ self.draw_label(col, "found", "material", "materials",
+ AMTH_store_data.mat_shaders, "INFO"
+ )
+ for i, mat in enumerate(AMTH_store_data.mat_shaders):
+ col.label(
+ text="{}".format(AMTH_store_data.mat_shaders[i]), icon="MATERIAL"
+ )
+
+ # List Missing Node Trees
+ box = layout.box()
+ row = box.row(align=True)
+ split = row.split()
+ col = split.column(align=True)
+
+ split = col.split(align=True)
+ split.label(text="Node Links")
+ row = split.row(align=True)
+ row.operator(AMTH_SCENE_OT_list_missing_node_links.bl_idname,
+ icon="NODETREE")
+
+ if AMTH_store_data.count_groups != 0 or \
+ AMTH_store_data.count_images != 0 or \
+ AMTH_store_data.count_image_node_unlinked != 0:
+
+ row.operator(
+ AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
+ icon="X", text="").what = "NODE_LINK"
+ col.label(text="Warning! Check Console", icon="ERROR")
+
+ if AMTH_store_data.count_groups != 0:
+ self.draw_miss_link(col, "node", "group", "groups", "missing link",
+ AMTH_store_data.count_groups, "NODE_TREE"
+ )
+ if AMTH_store_data.count_images != 0:
+ self.draw_miss_link(col, "image", "node", "nodes", "missing link",
+ AMTH_store_data.count_images, "IMAGE_DATA"
+ )
+ if AMTH_store_data.count_image_node_unlinked != 0:
+ self.draw_miss_link(col, "image", "node", "nodes", "with no output conected",
+ AMTH_store_data.count_image_node_unlinked, "NODE"
+ )
+
+ # List Empty Materials Slots
+ box = layout.box()
+ split = box.split()
+ col = split.column(align=True)
+ col.label(text="Material Slots")
+
+ row = split.row(align=True)
+ row.operator(AMTH_SCENE_OT_list_missing_material_slots.bl_idname,
+ icon="MATERIAL",
+ text="List Empty Materials Slots"
+ )
+ if len(AMTH_store_data.obj_mat_slots) != 0:
+ row.operator(
+ AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
+ icon="X", text="").what = "MAT_SLOTS"
+
+ col.separator()
+ col = box.column(align=True)
+ self.draw_label(col, "found empty material slot", "object", "objects",
+ AMTH_store_data.obj_mat_slots, "INFO"
+ )
+ for entry, obs in enumerate(AMTH_store_data.obj_mat_slots):
+ row = col.row()
+ row.alignment = "LEFT"
+ row.label(
+ text="{}".format(AMTH_store_data.obj_mat_slots[entry]),
+ icon="OBJECT_DATA")
+
+ if AMTH_store_data.obj_mat_slots_lib:
+ col.separator()
+ col.label("Check {}:".format(
+ "this library" if
+ len(AMTH_store_data.obj_mat_slots_lib) == 1 else
+ "these libraries")
+ )
+ for ilib, libs in enumerate(AMTH_store_data.obj_mat_slots_lib):
+ row = col.row(align=True)
+ row.alignment = "LEFT"
+ row.operator(
+ AMTH_SCENE_OT_blender_instance_open.bl_idname,
+ text=AMTH_store_data.obj_mat_slots_lib[ilib],
+ icon="LINK_BLEND",
+ emboss=False).filepath = AMTH_store_data.obj_mat_slots_lib[ilib]
+
+ box = layout.box()
+ row = box.row(align=True)
+ row.label(text="List Users for Datablock")
+
+ col = box.column(align=True)
+ split = col.split()
+ row = split.row(align=True)
+ row.prop(
+ scene, "amth_datablock_types",
+ icon=scene.amth_datablock_types,
+ text=""
+ )
+ row.operator_menu_enum(
+ "scene.amth_list_users_for_x_type",
+ "list_type_select",
+ text=scene.amth_list_users_for_x_name
+ )
+
+ row = split.row(align=True)
+ row.enabled = True if USER_X_NAME_EMPTY not in scene.amth_list_users_for_x_name else False
+ row.operator(
+ AMTH_SCENE_OT_list_users_for_x.bl_idname,
+ icon="COLLAPSEMENU").name = scene.amth_list_users_for_x_name
+
+ if any(val for val in AMTH_store_data.users.values()):
+ col = box.column(align=True)
+
+ for t in AMTH_store_data.users:
+
+ for ma in AMTH_store_data.users[t]:
+ subrow = col.row(align=True)
+ subrow.alignment = "LEFT"
+
+ if t == 'OBJECT_DATA':
+ text_lib = " [L] " if \
+ ma in bpy.data.objects and bpy.data.objects[ma].library else ""
+ subrow.operator(
+ AMTH_SCENE_OT_amaranth_object_select.bl_idname,
+ text="{} {}{}".format(text_lib, ma,
+ "" if ma in context.scene.objects else " [Not in Scene]"),
+ icon=t,
+ emboss=False).object_name = ma
+ else:
+ subrow.label(text=ma, icon=t)
+ row.operator(
+ AMTH_SCENE_OT_list_users_debug_clear.bl_idname,
+ icon="X", text="").what = "XTYPE"
+
+ if AMTH_store_data.libraries:
+ count_lib = 0
+
+ col.separator()
+ col.label("Check {}:".format(
+ "this library" if
+ len(AMTH_store_data.libraries) == 1 else
+ "these libraries")
+ )
+ for libs in AMTH_store_data.libraries:
+ count_lib += 1
+ row = col.row(align=True)
+ row.alignment = "LEFT"
+ row.operator(
+ AMTH_SCENE_OT_blender_instance_open.bl_idname,
+ text=AMTH_store_data.libraries[count_lib - 1],
+ icon="LINK_BLEND",
+ emboss=False).filepath = AMTH_store_data.libraries[count_lib - 1]
+
+
+class AMTH_PT_LightersCorner(Panel):
+ """The Lighters Panel"""
+ bl_label = "Lighter's Corner"
+ bl_idname = "AMTH_SCENE_PT_lighters_corner"
+ bl_space_type = 'PROPERTIES'
+ bl_region_type = 'WINDOW'
+ bl_context = "scene"
+ bl_options = {"DEFAULT_CLOSED"}
+
+ def draw_header(self, context):
+ layout = self.layout
+ layout.label(text="", icon="LIGHT_SUN")
+
+ def draw(self, context):
+ layout = self.layout
+ state_props = len(context.window_manager.amth_lighters_state)
+ engine = context.scene.render.engine
+ box = layout.box()
+ row = box.row(align=True)
+
+ if utils.cycles_exists():
+ row.prop(context.scene, "amaranth_lighterscorner_list_meshlights")
+
+ subrow = row.row(align=True)
+ subrow.alignment = "RIGHT"
+ subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname,
+ text="", icon="FILE_REFRESH").what = "LIGHTS"
+
+ if not state_props:
+ row = box.row()
+ message = "Please Refresh" if len(bpy.data.lights) > 0 else "No Lights in Data"
+ row.label(text=message, icon="INFO")
+ else:
+ row = box.row(align=True)
+ split = row.split(factor=0.5, align=True)
+ col = split.column(align=True)
+
+ col.label(text="Name/Library link")
+
+ if engine in ["CYCLES", "BLENDER_RENDER"]:
+ splits = 0.6 if engine == "BLENDER_RENDER" else 0.4
+ splita = split.split(factor=splits, align=True)
+ col = splita.column(align=True)
+ col.alignment = "LEFT"
+ col.label(text="Samples")
+
+ if utils.cycles_exists() and engine == "CYCLES":
+ col = splita.column(align=True)
+ col.label(text="Size")
+
+ cols = row.row(align=True)
+ cols.alignment = "RIGHT"
+ cols.label(text="{}Render Visibility/Selection".format(
+ "Rays /" if utils.cycles_exists() else "")
+ )
+ box.template_list(
+ 'AMTH_UL_LightersCorner_UI',
+ 'amth_collection_index_prop',
+ context.window_manager,
+ 'amth_lighters_state',
+ context.window_manager.amth_collection_index_prop,
+ 'index',
+ rows=5
+ )
+
+
+class AMTH_UL_MissingImages_UI(UIList):
+
+ def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ text_lib = item.text_lib
+ has_filepath = item.has_filepath
+ is_library = item.is_library
+
+ split = layout.split(factor=0.4)
+ row = split.row(align=True)
+ row.alignment = "LEFT"
+ row.label(text=text_lib, icon="IMAGE_DATA")
+ image = bpy.data.images.get(item.name, None)
+
+ subrow = split.row(align=True)
+ splitp = subrow.split(factor=0.8, align=True).row(align=True)
+ splitp.alignment = "LEFT"
+ row_lib = subrow.row(align=True)
+ row_lib.alignment = "RIGHT"
+ if not image:
+ splitp.label(text="Image is not available", icon="ERROR")
+ else:
+ splitp.label(text=has_filepath, icon="LIBRARY_DATA_DIRECT")
+ if is_library:
+ row_lib.operator(
+ AMTH_SCENE_OT_blender_instance_open.bl_idname,
+ text="",
+ emboss=False, icon="LINK_BLEND").filepath = is_library
+
+
+class AMTH_UL_LightersCorner_UI(UIList):
+
+ def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ icon_type = item.icon_type
+ engine = context.scene.render.engine
+ text_lib = item.text_lib
+ is_library = item.is_library
+
+ split = layout.split(factor=0.35)
+ row = split.row(align=True)
+ row.alignment = "LEFT"
+ row.label(text=text_lib, icon=icon_type)
+ ob = bpy.data.objects.get(item.name, None)
+ if not ob:
+ row.label(text="Object is not available", icon="ERROR")
+ else:
+ if is_library:
+ row.operator(
+ AMTH_SCENE_OT_blender_instance_open.bl_idname,
+ text="",
+ emboss=False, icon="LINK_BLEND").filepath = is_library
+
+ rows = split.row(align=True)
+ splits = 0.9 if engine == "BLENDER_RENDER" else 0.4
+ splitlamp = rows.split(factor=splits, align=True)
+ splitlampb = splitlamp.row(align=True)
+ splitlampc = splitlamp.row(align=True)
+ splitlampd = rows.row(align=True)
+ splitlampd.alignment = "RIGHT"
+
+ if utils.cycles_exists() and engine == "CYCLES":
+ if "LIGHT" in icon_type:
+ clamp = ob.data.cycles
+ if context.scene.cycles.progressive == "BRANCHED_PATH":
+ splitlampb.prop(clamp, "samples", text="")
+ if context.scene.cycles.progressive == "PATH":
+ splitlampb.label(text="N/A")
+ lamp = ob.data
+ if lamp.type in ["POINT", "SUN", "SPOT"]:
+ splitlampc.label(text="{:.2f}".format(lamp.shadow_soft_size))
+ elif lamp.type == "HEMI":
+ splitlampc.label(text="N/A")
+ elif lamp.type == "AREA" and lamp.shape == "RECTANGLE":
+ splitlampc.label(
+ text="{:.2f} x {:.2f}".format(lamp.size, lamp.size_y)
+ )
+ else:
+ splitlampc.label(text="{:.2f}".format(lamp.size))
+ else:
+ splitlampb.label(text="N/A")
+ if engine == "BLENDER_RENDER":
+ if "LIGHT" in icon_type:
+ lamp = ob.data
+ if lamp.type == "HEMI":
+ splitlampb.label(text="Not Available")
+ elif lamp.type == "AREA" and lamp.shadow_method == "RAY_SHADOW":
+ splitlampb.prop(lamp, "shadow_ray_samples_x", text="X")
+ if lamp.shape == "RECTANGLE":
+ splitlampb.prop(lamp, "shadow_ray_samples_y", text="Y")
+ elif lamp.shadow_method == "RAY_SHADOW":
+ splitlampb.prop(lamp, "shadow_ray_samples", text="Ray Samples")
+ elif lamp.shadow_method == "BUFFER_SHADOW":
+ splitlampb.prop(lamp, "shadow_buffer_samples", text="Buffer Samples")
+ else:
+ splitlampb.label(text="No Shadow")
+ else:
+ splitlampb.label(text="N/A")
+ if utils.cycles_exists():
+ visibility = ob.cycles_visibility
+ splitlampd.prop(visibility, "camera", text="")
+ splitlampd.prop(visibility, "diffuse", text="")
+ splitlampd.prop(visibility, "glossy", text="")
+ splitlampd.prop(visibility, "shadow", text="")
+ splitlampd.separator()
+ splitlampd.prop(ob, "hide", text="", emboss=False)
+ splitlampd.prop(ob, "hide_render", text="", emboss=False)
+ splitlampd.operator(
+ AMTH_SCENE_OT_amaranth_object_select.bl_idname,
+ text="",
+ emboss=False, icon="RESTRICT_SELECT_OFF").object_name = item.name
+
+
+def fill_missing_images_props(context, refresh=False):
+ image_state = context.window_manager.amth_missing_images_state
+ if refresh:
+ for key in image_state.keys():
+ index = image_state.find(key)
+ if index != -1:
+ image_state.remove(index)
+
+ for im in bpy.data.images:
+ if im.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"):
+ if not im.packed_file and \
+ not os.path.exists(bpy.path.abspath(im.filepath, library=im.library)):
+ text_l = "{}{} [{}]{}".format("[L] " if im.library else "", im.name,
+ im.users, " [F]" if im.use_fake_user else "")
+ prop = image_state.add()
+ prop.name = im.name
+ prop.text_lib = text_l
+ prop.has_filepath = im.filepath if im.filepath else "No Filepath"
+ prop.is_library = im.library.filepath if im.library else ""
+
+
+def fill_ligters_corner_props(context, refresh=False):
+ light_state = context.window_manager.amth_lighters_state
+ list_meshlights = context.scene.amaranth_lighterscorner_list_meshlights
+ if refresh:
+ for key in light_state.keys():
+ index = light_state.find(key)
+ if index != -1:
+ light_state.remove(index)
+
+ for ob in bpy.data.objects:
+ if ob.name not in light_state.keys() or refresh:
+ is_light = ob.type == "LIGHT"
+ is_emission = True if utils.cycles_is_emission(
+ context, ob) and list_meshlights else False
+
+ if is_light or is_emission:
+ icons = "LIGHT_%s" % ob.data.type if is_light else "MESH_GRID"
+ text_l = "{} {}{}".format(" [L] " if ob.library else "", ob.name,
+ "" if ob.name in context.scene.objects else " [Not in Scene]")
+ prop = light_state.add()
+ prop.name = ob.name
+ prop.icon_type = icons
+ prop.text_lib = text_l
+ prop.is_library = ob.library.filepath if ob.library else ""
+
+
+class AMTH_LightersCornerStateProp(PropertyGroup):
+ icon_type: StringProperty()
+ text_lib: StringProperty()
+ is_library: StringProperty()
+
+
+class AMTH_MissingImagesStateProp(PropertyGroup):
+ text_lib: StringProperty()
+ has_filepath: StringProperty()
+ is_library: StringProperty()
+
+
+class AMTH_LightersCollectionIndexProp(PropertyGroup):
+ index: IntProperty(
+ name="index"
+ )
+ index_image: IntProperty(
+ name="index"
+ )
+
+
+classes = (
+ AMTH_SCENE_PT_scene_debug,
+ AMTH_SCENE_OT_list_users_debug_clear,
+ AMTH_SCENE_OT_blender_instance_open,
+ AMTH_SCENE_OT_amaranth_object_select,
+ AMTH_SCENE_OT_list_missing_node_links,
+ AMTH_SCENE_OT_list_missing_material_slots,
+ AMTH_SCENE_OT_cycles_shader_list_nodes,
+ AMTH_SCENE_OT_list_users_for_x,
+ AMTH_SCENE_OT_list_users_for_x_type,
+ AMTH_SCENE_OT_Collection_List_Refresh,
+ AMTH_LightersCornerStateProp,
+ AMTH_LightersCollectionIndexProp,
+ AMTH_MissingImagesStateProp,
+ AMTH_PT_LightersCorner,
+ AMTH_UL_LightersCorner_UI,
+ AMTH_UL_MissingImages_UI,
+)
+
+
+def register():
+ init()
+
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+ bpy.types.Scene.amth_list_users_for_x_name = StringProperty(
+ default="Select DataBlock Name",
+ name="Name",
+ description=USER_X_NAME_EMPTY,
+ options={"SKIP_SAVE"}
+ )
+ bpy.types.WindowManager.amth_collection_index_prop = PointerProperty(
+ type=AMTH_LightersCollectionIndexProp
+ )
+ bpy.types.WindowManager.amth_lighters_state = CollectionProperty(
+ type=AMTH_LightersCornerStateProp
+ )
+ bpy.types.WindowManager.amth_missing_images_state = CollectionProperty(
+ type=AMTH_MissingImagesStateProp
+ )
+
+
+def unregister():
+ clear()
+
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+ del bpy.types.Scene.amth_list_users_for_x_name
+ del bpy.types.WindowManager.amth_collection_index_prop
+ del bpy.types.WindowManager.amth_lighters_state
+ del bpy.types.WindowManager.amth_missing_images_state
diff --git a/amaranth/scene/goto_library.py b/amaranth/scene/goto_library.py
new file mode 100644
index 00000000..08f1689c
--- /dev/null
+++ b/amaranth/scene/goto_library.py
@@ -0,0 +1,90 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+File Browser: Libraries Bookmark
+
+The "Libraries" panel on the File Browser displays the path to all the
+libraries linked to that .blend. So you can quickly go to the folders
+related to the file.
+
+Click on any path to go to that directory.
+Developed during Caminandes Open Movie Project
+"""
+
+import bpy
+
+
+class AMTH_FILE_PT_libraries(bpy.types.Panel):
+ bl_space_type = "FILE_BROWSER"
+ bl_region_type = "TOOLS"
+ bl_category = "Bookmarks"
+ bl_label = "Libraries"
+
+ def draw(self, context):
+ layout = self.layout
+
+ libs = bpy.data.libraries
+ libslist = []
+
+ # Build the list of folders from libraries
+ import os.path
+
+ for lib in libs:
+ directory_name = os.path.dirname(lib.filepath)
+ libslist.append(directory_name)
+
+ # Remove duplicates and sort by name
+ libslist = set(libslist)
+ libslist = sorted(libslist)
+
+ # Draw the box with libs
+ row = layout.row()
+ box = row.box()
+
+ if libslist:
+ col = box.column()
+ for filepath in libslist:
+ if filepath != "//":
+ row = col.row()
+ row.alignment = "LEFT"
+ props = row.operator(
+ AMTH_FILE_OT_directory_go_to.bl_idname,
+ text=filepath, icon="BOOKMARKS",
+ emboss=False)
+ props.filepath = filepath
+ else:
+ box.label(text="No libraries loaded")
+
+
+class AMTH_FILE_OT_directory_go_to(bpy.types.Operator):
+
+ """Go to this library"s directory"""
+ bl_idname = "file.directory_go_to"
+ bl_label = "Go To"
+
+ filepath: bpy.props.StringProperty(subtype="FILE_PATH")
+
+ def execute(self, context):
+ bpy.ops.file.select_bookmark(dir=self.filepath)
+ return {"FINISHED"}
+
+
+def register():
+ bpy.utils.register_class(AMTH_FILE_PT_libraries)
+ bpy.utils.register_class(AMTH_FILE_OT_directory_go_to)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_FILE_PT_libraries)
+ bpy.utils.unregister_class(AMTH_FILE_OT_directory_go_to)
diff --git a/amaranth/scene/material_remove_unassigned.py b/amaranth/scene/material_remove_unassigned.py
new file mode 100644
index 00000000..f19bc3d2
--- /dev/null
+++ b/amaranth/scene/material_remove_unassigned.py
@@ -0,0 +1,108 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+import bpy
+
+
+# FEATURE: Delete Materials not assigned to any verts
+class AMTH_OBJECT_OT_material_remove_unassigned(bpy.types.Operator):
+
+ """Remove materials not assigned to any vertex"""
+ bl_idname = "object.amaranth_object_material_remove_unassigned"
+ bl_label = "Remove Unassigned Materials"
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object.material_slots
+
+ def execute(self, context):
+
+ scene = context.scene
+ act_ob = context.active_object
+ count = len(act_ob.material_slots)
+ materials_removed = []
+ act_ob.active_material_index = 0
+ is_visible = True
+
+ if act_ob not in context.visible_objects:
+ is_visible = False
+ n = -1
+ for lay in act_ob.layers:
+ n += 1
+ if lay:
+ break
+
+ scene.layers[n] = True
+
+ for slot in act_ob.material_slots:
+ count -= 1
+
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_all(action="DESELECT")
+ act_ob.active_material_index = count
+ bpy.ops.object.material_slot_select()
+
+ if act_ob.data.total_vert_sel == 0 or \
+ (len(act_ob.material_slots) == 1 and not
+ act_ob.material_slots[0].material):
+ materials_removed.append(
+ "%s" %
+ act_ob.active_material.name if act_ob.active_material else "Empty")
+ bpy.ops.object.mode_set(mode="OBJECT")
+ bpy.ops.object.material_slot_remove()
+ else:
+ pass
+
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_all(action="DESELECT")
+ bpy.ops.object.mode_set(mode="OBJECT")
+
+ if materials_removed:
+ print(
+ "\n* Removed %s Unassigned Materials \n" %
+ len(materials_removed))
+
+ count_mr = 0
+
+ for mr in materials_removed:
+ count_mr += 1
+ print(
+ "%0.2d. %s" %
+ (count_mr, materials_removed[count_mr - 1]))
+
+ print("\n")
+ self.report({"INFO"}, "Removed %s Unassigned Materials" %
+ len(materials_removed))
+
+ if not is_visible:
+ scene.layers[n] = False
+
+ return {"FINISHED"}
+
+
+def ui_material_remove_unassigned(self, context):
+ self.layout.operator(
+ AMTH_OBJECT_OT_material_remove_unassigned.bl_idname,
+ icon="X")
+
+# // FEATURE: Delete Materials not assigned to any verts
+
+
+def register():
+ bpy.utils.register_class(AMTH_OBJECT_OT_material_remove_unassigned)
+ bpy.types.MATERIAL_MT_context_menu.append(ui_material_remove_unassigned)
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_OBJECT_OT_material_remove_unassigned)
+ bpy.types.MATERIAL_MT_context_menu.remove(ui_material_remove_unassigned)
diff --git a/amaranth/scene/refresh.py b/amaranth/scene/refresh.py
new file mode 100644
index 00000000..3ef5e6b7
--- /dev/null
+++ b/amaranth/scene/refresh.py
@@ -0,0 +1,76 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Refresh Scene
+
+Refresh the current scene, useful when working with libraries or drivers.
+Could also add an option to refresh the VSE maybe? Usage: Hit F5 or find
+it on the Specials menu W.
+"""
+
+import bpy
+
+
+KEYMAPS = list()
+
+
+class AMTH_SCENE_OT_refresh(bpy.types.Operator):
+ """Refresh the current scene"""
+ bl_idname = "scene.refresh"
+ bl_label = "Refresh!"
+
+ def execute(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return {"CANCELLED"}
+
+ preferences = context.preferences.addons["amaranth"].preferences
+ scene = context.scene
+
+ if preferences.use_scene_refresh:
+ # Changing the frame is usually the best way to go
+ scene.frame_current = scene.frame_current
+ self.report({"INFO"}, "Scene Refreshed!")
+
+ return {"FINISHED"}
+
+
+def button_refresh(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ if context.preferences.addons["amaranth"].preferences.use_scene_refresh:
+ self.layout.separator()
+ self.layout.operator(AMTH_SCENE_OT_refresh.bl_idname,
+ text="Refresh!",
+ icon="FILE_REFRESH")
+
+
+def register():
+ bpy.utils.register_class(AMTH_SCENE_OT_refresh)
+ bpy.types.VIEW3D_MT_object_context_menu.append(button_refresh)
+ kc = bpy.context.window_manager.keyconfigs.addon
+ km = kc.keymaps.new(name="Window")
+ kmi = km.keymap_items.new("scene.refresh", "F5", "PRESS",
+ alt=True)
+ KEYMAPS.append((km, kmi))
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_SCENE_OT_refresh)
+ bpy.types.VIEW3D_MT_object_context_menu.remove(button_refresh)
+ for km, kmi in KEYMAPS:
+ km.keymap_items.remove(kmi)
+ KEYMAPS.clear()
diff --git a/amaranth/scene/save_reload.py b/amaranth/scene/save_reload.py
new file mode 100644
index 00000000..7b961b64
--- /dev/null
+++ b/amaranth/scene/save_reload.py
@@ -0,0 +1,78 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Save & Reload File
+
+When working with linked libraries, very often you need to save and load
+again to see the changes.
+This does it in one go, without asking, so be careful :)
+Usage: Hit Ctrl + Shift + W or find it at the bottom of the File menu.
+"""
+
+import bpy
+
+
+KEYMAPS = list()
+
+
+class AMTH_WM_OT_save_reload(bpy.types.Operator):
+ """Save and Reload the current blend file"""
+ bl_idname = "wm.save_reload"
+ bl_label = "Save & Reload"
+
+ def save_reload(self, context, path):
+ if not path:
+ bpy.ops.wm.save_as_mainfile("INVOKE_AREA")
+ return
+ bpy.ops.wm.save_mainfile()
+ self.report({"INFO"}, "Saved & Reloaded")
+ bpy.ops.wm.open_mainfile("EXEC_DEFAULT", filepath=path)
+
+ def execute(self, context):
+ path = bpy.data.filepath
+ self.save_reload(context, path)
+
+ return {"FINISHED"}
+
+
+def button_save_reload(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ if context.preferences.addons["amaranth"].preferences.use_file_save_reload:
+ self.layout.separator()
+ self.layout.operator(
+ AMTH_WM_OT_save_reload.bl_idname,
+ text="Save & Reload",
+ icon="FILE_REFRESH")
+
+
+def register():
+ bpy.utils.register_class(AMTH_WM_OT_save_reload)
+ bpy.types.TOPBAR_MT_file.append(button_save_reload)
+ wm = bpy.context.window_manager
+ kc = wm.keyconfigs.addon
+ km = kc.keymaps.new(name="Window")
+ kmi = km.keymap_items.new("wm.save_reload", "W", "PRESS",
+ shift=True, ctrl=True)
+ KEYMAPS.append((km, kmi))
+
+
+def unregister():
+ bpy.utils.unregister_class(AMTH_WM_OT_save_reload)
+ bpy.types.TOPBAR_MT_file.remove(button_save_reload)
+ for km, kmi in KEYMAPS:
+ km.keymap_items.remove(kmi)
+ KEYMAPS.clear()
diff --git a/amaranth/scene/stats.py b/amaranth/scene/stats.py
new file mode 100644
index 00000000..10d8be44
--- /dev/null
+++ b/amaranth/scene/stats.py
@@ -0,0 +1,62 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+Scene, Cameras, and Meshlights Count
+
+Increase the stats by displaying the number of scenes, cameras, and light
+emitting meshes.
+On the Info header.
+"""
+
+import bpy
+from amaranth import utils
+
+
+def stats_scene(self, context):
+ get_addon = "amaranth" in context.preferences.addons.keys()
+ if not get_addon:
+ return
+
+ if context.preferences.addons["amaranth"].preferences.use_scene_stats:
+ scenes_count = str(len(bpy.data.scenes))
+ cameras_count = str(len(bpy.data.cameras))
+ cameras_selected = 0
+ meshlights = 0
+ meshlights_visible = 0
+
+ for ob in context.scene.objects:
+ if utils.cycles_is_emission(context, ob):
+ meshlights += 1
+ if ob in context.visible_objects:
+ meshlights_visible += 1
+
+ if ob in context.selected_objects:
+ if ob.type == 'CAMERA':
+ cameras_selected += 1
+
+ meshlights_string = '| Meshlights:{}/{}'.format(
+ meshlights_visible, meshlights)
+
+ row = self.layout.row(align=True)
+ row.label(text="Scenes:{} | Cameras:{}/{} {}".format(
+ scenes_count, cameras_selected, cameras_count,
+ meshlights_string if utils.cycles_active(context) else ''))
+
+
+def register():
+ bpy.types.STATUSBAR_HT_header.append(stats_scene)
+
+
+def unregister():
+ bpy.types.STATUSBAR_HT_header.remove(stats_scene)
diff --git a/amaranth/utils.py b/amaranth/utils.py
new file mode 100644
index 00000000..f6367875
--- /dev/null
+++ b/amaranth/utils.py
@@ -0,0 +1,64 @@
+# 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 2
+# 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
+# MERCHANTABILITY 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, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import bpy
+
+
+# FUNCTION: Checks if cycles is available
+def cycles_exists():
+ return hasattr(bpy.types.Scene, "cycles")
+
+
+# FUNCTION: Checks if cycles is the active renderer
+def cycles_active(context):
+ return context.scene.render.engine == "CYCLES"
+
+
+# FUNCTION: Check if material has Emission (for select and stats)
+def cycles_is_emission(context, ob):
+ is_emission = False
+
+ if not ob.material_slots:
+ return is_emission
+
+ for ma in ob.material_slots:
+ if not ma.material:
+ continue
+ if ma.material.node_tree and ma.material.node_tree.nodes:
+ for no in ma.material.node_tree.nodes:
+ if not no.type in ("EMISSION", "GROUP"):
+ continue
+ for ou in no.outputs:
+ if not ou.links:
+ continue
+ if no.type == "GROUP" and no.node_tree and no.node_tree.nodes:
+ for gno in no.node_tree.nodes:
+ if gno.type != "EMISSION":
+ continue
+ for gou in gno.outputs:
+ if ou.links and gou.links:
+ is_emission = True
+ elif no.type == "EMISSION":
+ if ou.links:
+ is_emission = True
+ return is_emission
+
+
+# FUNCTION: Check if object has keyframes for a specific frame
+def is_keyframe(ob, frame):
+ if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None:
+ for fcu in ob.animation_data.action.fcurves:
+ if frame in (p.co.x for p in fcu.keyframe_points):
+ return True
+ return False