diff options
Diffstat (limited to 'release/scripts/modules/bpy_extras')
-rw-r--r-- | release/scripts/modules/bpy_extras/__init__.py | 1 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/anim_utils.py | 25 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/image_utils.py | 2 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/io_utils.py | 113 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/keyconfig_utils.py | 114 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/keyconfig_utils_experimental.py | 244 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/mesh_utils.py | 190 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/node_shader_utils.py | 774 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/node_utils.py | 32 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/object_utils.py | 89 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/view3d_utils.py | 4 |
11 files changed, 1288 insertions, 300 deletions
diff --git a/release/scripts/modules/bpy_extras/__init__.py b/release/scripts/modules/bpy_extras/__init__.py index d2578e1812a..1caef074d43 100644 --- a/release/scripts/modules/bpy_extras/__init__.py +++ b/release/scripts/modules/bpy_extras/__init__.py @@ -29,5 +29,6 @@ __all__ = ( "image_utils", "keyconfig_utils", "mesh_utils", + "node_utils", "view3d_utils", ) diff --git a/release/scripts/modules/bpy_extras/anim_utils.py b/release/scripts/modules/bpy_extras/anim_utils.py index 7216add2c2c..c545d237329 100644 --- a/release/scripts/modules/bpy_extras/anim_utils.py +++ b/release/scripts/modules/bpy_extras/anim_utils.py @@ -168,7 +168,8 @@ def bake_action_iter( for name, pbone in obj.pose.bones.items(): if do_visual_keying: # Get the final transform of the bone in its own local space... - matrix[name] = obj.convert_space(pbone, pbone.matrix, 'POSE', 'LOCAL') + matrix[name] = obj.convert_space(pose_bone=pbone, matrix=pbone.matrix, + from_space='POSE', to_space='LOCAL') else: matrix[name] = pbone.matrix_basis.copy() @@ -272,13 +273,13 @@ def bake_action_iter( for (f, matrix, bbones) in pose_info: pbone.matrix_basis = matrix[name].copy() - pbone.keyframe_insert("location", -1, f, name, options) + pbone.keyframe_insert("location", index=-1, frame=f, group=name, options=options) rotation_mode = pbone.rotation_mode if rotation_mode == 'QUATERNION': - pbone.keyframe_insert("rotation_quaternion", -1, f, name, options) + pbone.keyframe_insert("rotation_quaternion", index=-1, frame=f, group=name, options=options) elif rotation_mode == 'AXIS_ANGLE': - pbone.keyframe_insert("rotation_axis_angle", -1, f, name, options) + pbone.keyframe_insert("rotation_axis_angle", index=-1, frame=f, group=name, options=options) else: # euler, XYZ, ZXY etc if euler_prev is not None: euler = pbone.rotation_euler.copy() @@ -288,9 +289,9 @@ def bake_action_iter( del euler else: euler_prev = pbone.rotation_euler.copy() - pbone.keyframe_insert("rotation_euler", -1, f, name, options) + pbone.keyframe_insert("rotation_euler", index=-1, frame=f, group=name, options=options) - pbone.keyframe_insert("scale", -1, f, name, options) + pbone.keyframe_insert("scale", index=-1, frame=f, group=name, options=options) # Bendy Bones if pbone.bone.bbone_segments > 1: @@ -298,7 +299,7 @@ def bake_action_iter( for bb_prop in BBONE_PROPS: # update this property with value from bbone_shape, then key it setattr(pbone, bb_prop, bbone_shape[bb_prop]) - pbone.keyframe_insert(bb_prop, -1, f, name, options) + pbone.keyframe_insert(bb_prop, index=-1, frame=f, group=name, options=options) # object. TODO. multiple objects if do_object: @@ -313,13 +314,13 @@ def bake_action_iter( name = "Action Bake" # XXX: placeholder obj.matrix_basis = matrix - obj.keyframe_insert("location", -1, f, name, options) + obj.keyframe_insert("location", index=-1, frame=f, group=name, options=options) rotation_mode = obj.rotation_mode if rotation_mode == 'QUATERNION': - obj.keyframe_insert("rotation_quaternion", -1, f, name, options) + obj.keyframe_insert("rotation_quaternion", index=-1, frame=f, group=name, options=options) elif rotation_mode == 'AXIS_ANGLE': - obj.keyframe_insert("rotation_axis_angle", -1, f, name, options) + obj.keyframe_insert("rotation_axis_angle", index=-1, frame=f, group=name, options=options) else: # euler, XYZ, ZXY etc if euler_prev is not None: euler = obj.rotation_euler.copy() @@ -329,9 +330,9 @@ def bake_action_iter( del euler else: euler_prev = obj.rotation_euler.copy() - obj.keyframe_insert("rotation_euler", -1, f, name, options) + obj.keyframe_insert("rotation_euler", index=-1, frame=f, group=name, options=options) - obj.keyframe_insert("scale", -1, f, name, options) + obj.keyframe_insert("scale", index=-1, frame=f, group=name, options=options) if do_parents_clear: obj.parent = None diff --git a/release/scripts/modules/bpy_extras/image_utils.py b/release/scripts/modules/bpy_extras/image_utils.py index 49fce7d27c7..63850b63b57 100644 --- a/release/scripts/modules/bpy_extras/image_utils.py +++ b/release/scripts/modules/bpy_extras/image_utils.py @@ -103,7 +103,7 @@ def load_image(imagepath, path = os.path.abspath(path) try: - image = bpy.data.images.load(path, check_existing) + image = bpy.data.images.load(path, check_existing=check_existing) except RuntimeError: image = None diff --git a/release/scripts/modules/bpy_extras/io_utils.py b/release/scripts/modules/bpy_extras/io_utils.py index e2c2e4c9b93..e74631256e3 100644 --- a/release/scripts/modules/bpy_extras/io_utils.py +++ b/release/scripts/modules/bpy_extras/io_utils.py @@ -21,7 +21,7 @@ __all__ = ( "ExportHelper", "ImportHelper", - "orientation_helper_factory", + "orientation_helper", "axis_conversion", "axis_conversion_ensure", "create_derived_objects", @@ -52,25 +52,19 @@ def _check_axis_conversion(op): class ExportHelper: - filepath = StringProperty( + filepath: StringProperty( name="File Path", description="Filepath used for exporting the file", maxlen=1024, subtype='FILE_PATH', ) - check_existing = BoolProperty( + check_existing: BoolProperty( name="Check Existing", description="Check and warn on overwriting existing files", default=True, options={'HIDDEN'}, ) - # needed for mix-ins - order = [ - "filepath", - "check_existing", - ] - # subclasses can override with decorator # True == use ext, False == no ext, None == do nothing. check_extension = True @@ -112,18 +106,13 @@ class ExportHelper: class ImportHelper: - filepath = StringProperty( + filepath: StringProperty( name="File Path", description="Filepath used for importing the file", maxlen=1024, subtype='FILE_PATH', ) - # needed for mix-ins - order = [ - "filepath", - ] - def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} @@ -132,53 +121,53 @@ class ImportHelper: return _check_axis_conversion(self) -def orientation_helper_factory(name, axis_forward='Y', axis_up='Z'): - members = {} - - def _update_axis_forward(self, context): - if self.axis_forward[-1] == self.axis_up[-1]: - self.axis_up = (self.axis_up[0:-1] + - 'XYZ'[('XYZ'.index(self.axis_up[-1]) + 1) % 3]) - - members['axis_forward'] = EnumProperty( - name="Forward", - items=( - ('X', "X Forward", ""), - ('Y', "Y Forward", ""), - ('Z', "Z Forward", ""), - ('-X', "-X Forward", ""), - ('-Y', "-Y Forward", ""), - ('-Z', "-Z Forward", ""), - ), - default=axis_forward, - update=_update_axis_forward, - ) - - def _update_axis_up(self, context): - if self.axis_up[-1] == self.axis_forward[-1]: - self.axis_forward = (self.axis_forward[0:-1] + - 'XYZ'[('XYZ'.index(self.axis_forward[-1]) + 1) % 3]) - - members['axis_up'] = EnumProperty( - name="Up", - items=( - ('X', "X Up", ""), - ('Y', "Y Up", ""), - ('Z', "Z Up", ""), - ('-X', "-X Up", ""), - ('-Y', "-Y Up", ""), - ('-Z', "-Z Up", ""), - ), - default=axis_up, - update=_update_axis_up, - ) - - members["order"] = [ - "axis_forward", - "axis_up", - ] - - return type(name, (object,), members) +def orientation_helper(axis_forward='Y', axis_up='Z'): + """ + A decorator for import/export classes, generating properties needed by the axis conversion system and IO helpers, + with specified default values (axes). + """ + def wrapper(cls): + def _update_axis_forward(self, context): + if self.axis_forward[-1] == self.axis_up[-1]: + self.axis_up = (self.axis_up[0:-1] + + 'XYZ'[('XYZ'.index(self.axis_up[-1]) + 1) % 3]) + + cls.__annotations__['axis_forward'] = EnumProperty( + name="Forward", + items=( + ('X', "X Forward", ""), + ('Y', "Y Forward", ""), + ('Z', "Z Forward", ""), + ('-X', "-X Forward", ""), + ('-Y', "-Y Forward", ""), + ('-Z', "-Z Forward", ""), + ), + default=axis_forward, + update=_update_axis_forward, + ) + + def _update_axis_up(self, context): + if self.axis_up[-1] == self.axis_forward[-1]: + self.axis_forward = (self.axis_forward[0:-1] + + 'XYZ'[('XYZ'.index(self.axis_forward[-1]) + 1) % 3]) + + cls.__annotations__['axis_up'] = EnumProperty( + name="Up", + items=( + ('X', "X Up", ""), + ('Y', "Y Up", ""), + ('Z', "Z Up", ""), + ('-X', "-X Up", ""), + ('-Y', "-Y Up", ""), + ('-Z', "-Z Up", ""), + ), + default=axis_up, + update=_update_axis_up, + ) + + return cls + + return wrapper # Axis conversion function, not pretty LUT diff --git a/release/scripts/modules/bpy_extras/keyconfig_utils.py b/release/scripts/modules/bpy_extras/keyconfig_utils.py index 0ff6595eca9..3caf45a72af 100644 --- a/release/scripts/modules/bpy_extras/keyconfig_utils.py +++ b/release/scripts/modules/bpy_extras/keyconfig_utils.py @@ -18,12 +18,36 @@ # <pep8 compliant> + +def _km_expand_from_toolsystem(space_type, context_mode): + def _fn(): + from bl_ui.space_toolsystem_common import ToolSelectPanelHelper + for cls in ToolSelectPanelHelper.__subclasses__(): + if cls.bl_space_type == space_type: + return cls.keymap_ui_hierarchy(context_mode) + raise Exception("keymap not found") + return _fn + + +def _km_hierarchy_iter_recursive(items): + for sub in items: + if callable(sub): + yield from sub() + else: + yield (*sub[:3], list(_km_hierarchy_iter_recursive(sub[3]))) + + +def km_hierarchy(): + return list(_km_hierarchy_iter_recursive(_km_hierarchy)) + + # bpy.type.KeyMap: (km.name, km.space_type, km.region_type, [...]) # ('Script', 'EMPTY', 'WINDOW', []), -KM_HIERARCHY = [ +# Access via 'km_hierarchy'. +_km_hierarchy = [ ('Window', 'EMPTY', 'WINDOW', []), # file save, window change, exit ('Screen', 'EMPTY', 'WINDOW', [ # full screen, undo, screenshot ('Screen Editing', 'EMPTY', 'WINDOW', []), # re-sizing, action corners @@ -36,26 +60,54 @@ KM_HIERARCHY = [ ('User Interface', 'EMPTY', 'WINDOW', []), ('3D View', 'VIEW_3D', 'WINDOW', [ # view 3d navigation and generic stuff (select, transform) - ('Object Mode', 'EMPTY', 'WINDOW', []), - ('Mesh', 'EMPTY', 'WINDOW', []), - ('Curve', 'EMPTY', 'WINDOW', []), - ('Armature', 'EMPTY', 'WINDOW', []), - ('Metaball', 'EMPTY', 'WINDOW', []), - ('Lattice', 'EMPTY', 'WINDOW', []), - ('Font', 'EMPTY', 'WINDOW', []), - - ('Pose', 'EMPTY', 'WINDOW', []), - - ('Vertex Paint', 'EMPTY', 'WINDOW', []), - ('Weight Paint', 'EMPTY', 'WINDOW', []), + ('Object Mode', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'OBJECT'), + ]), + ('Mesh', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_MESH'), + ]), + ('Curve', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_CURVE'), + ]), + ('Armature', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_ARMATURE'), + ]), + ('Metaball', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_METABALL'), + ]), + ('Lattice', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_LATTICE'), + ]), + ('Font', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'EDIT_TEXT'), + ]), + + ('Pose', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'POSE'), + ]), + + ('Vertex Paint', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'PAINT_VERTEX'), + ]), + ('Weight Paint', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'PAINT_WEIGHT'), + ]), ('Weight Paint Vertex Selection', 'EMPTY', 'WINDOW', []), ('Face Mask', 'EMPTY', 'WINDOW', []), - ('Image Paint', 'EMPTY', 'WINDOW', []), # image and view3d - ('Sculpt', 'EMPTY', 'WINDOW', []), - - ('Particle', 'EMPTY', 'WINDOW', []), + # image and view3d + ('Image Paint', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'PAINT_TEXTURE'), + ]), + ('Sculpt', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'SCULPT'), + ]), + + ('Particle', 'EMPTY', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', 'PARTICLE'), + ]), ('Knife Tool Modal Map', 'EMPTY', 'WINDOW', []), + ('Custom Normals Modal Map', 'EMPTY', 'WINDOW', []), ('Paint Stroke Modal', 'EMPTY', 'WINDOW', []), ('Paint Curve', 'EMPTY', 'WINDOW', []), @@ -68,7 +120,10 @@ KM_HIERARCHY = [ ('View3D Zoom Modal', 'EMPTY', 'WINDOW', []), ('View3D Dolly Modal', 'EMPTY', 'WINDOW', []), - ('3D View Generic', 'VIEW_3D', 'WINDOW', []), # toolbar and properties + # toolbar and properties + ('3D View Generic', 'VIEW_3D', 'WINDOW', [ + _km_expand_from_toolsystem('VIEW_3D', None), + ]), ]), ('Graph Editor', 'GRAPH_EDITOR', 'WINDOW', [ @@ -87,7 +142,9 @@ KM_HIERARCHY = [ ('UV Editor', 'EMPTY', 'WINDOW', []), # image (reverse order, UVEdit before Image) ('Image Paint', 'EMPTY', 'WINDOW', []), # image and view3d ('UV Sculpt', 'EMPTY', 'WINDOW', []), - ('Image Generic', 'IMAGE_EDITOR', 'WINDOW', []), + ('Image Generic', 'IMAGE_EDITOR', 'WINDOW', [ + _km_expand_from_toolsystem('IMAGE_EDITOR', None), + ]), ]), ('Outliner', 'OUTLINER', 'WINDOW', []), @@ -99,7 +156,6 @@ KM_HIERARCHY = [ ('SequencerCommon', 'SEQUENCE_EDITOR', 'WINDOW', []), ('SequencerPreview', 'SEQUENCE_EDITOR', 'WINDOW', []), ]), - ('Logic Editor', 'LOGIC_EDITOR', 'WINDOW', []), ('File Browser', 'FILE_BROWSER', 'WINDOW', [ ('File Browser Main', 'FILE_BROWSER', 'WINDOW', []), @@ -122,6 +178,12 @@ KM_HIERARCHY = [ ('Grease Pencil', 'EMPTY', 'WINDOW', [ # grease pencil stuff (per region) ('Grease Pencil Stroke Edit Mode', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Paint (Draw brush)', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Paint (Fill)', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Paint (Erase)', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Paint Mode', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Sculpt Mode', 'EMPTY', 'WINDOW', []), + ('Grease Pencil Stroke Weight Mode', 'EMPTY', 'WINDOW', []), ]), ('Mask Editing', 'EMPTY', 'WINDOW', []), ('Frames', 'EMPTY', 'WINDOW', []), # frame navigation (per region) @@ -132,7 +194,7 @@ KM_HIERARCHY = [ ('View3D Gesture Circle', 'EMPTY', 'WINDOW', []), ('Gesture Straight Line', 'EMPTY', 'WINDOW', []), ('Gesture Zoom Border', 'EMPTY', 'WINDOW', []), - ('Gesture Border', 'EMPTY', 'WINDOW', []), + ('Gesture Box', 'EMPTY', 'WINDOW', []), ('Standard Modal Map', 'EMPTY', 'WINDOW', []), ('Transform Modal Map', 'EMPTY', 'WINDOW', []), @@ -404,7 +466,15 @@ def keyconfig_test(kc): # Function body result = False - for entry in KM_HIERARCHY: + for entry in km_hierarchy(): if testEntry(kc, entry): result = True return result + + +# Note, we may eventually replace existing logic with this +# so key configs are always data. +from .keyconfig_utils_experimental import ( + keyconfig_export_as_data, + keyconfig_import_from_data, +) diff --git a/release/scripts/modules/bpy_extras/keyconfig_utils_experimental.py b/release/scripts/modules/bpy_extras/keyconfig_utils_experimental.py new file mode 100644 index 00000000000..cd82460e8e0 --- /dev/null +++ b/release/scripts/modules/bpy_extras/keyconfig_utils_experimental.py @@ -0,0 +1,244 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +__all__ = ( + "keyconfig_export_as_data", + "keyconfig_import_from_data", +) + + +def indent(levels): + return levels * " " + + +def round_float_32(f): + from struct import pack, unpack + return unpack("f", pack("f", f))[0] + + +def repr_f32(f): + f_round = round_float_32(f) + f_str = repr(f) + f_str_frac = f_str.partition(".")[2] + if not f_str_frac: + return f_str + for i in range(1, len(f_str_frac)): + f_test = round(f, i) + f_test_round = round_float_32(f_test) + if f_test_round == f_round: + return "%.*f" % (i, f_test) + return f_str + + +def kmi_args_as_data(kmi): + s = [ + f"\"type\": '{kmi.type}'", + f"\"value\": '{kmi.value}'" + ] + + if kmi.any: + s.append("\"any\": True") + else: + if kmi.shift: + s.append("\"shift\": True") + if kmi.ctrl: + s.append("\"ctrl\": True") + if kmi.alt: + s.append("\"alt\": True") + if kmi.oskey: + s.append("\"oskey\": True") + if kmi.key_modifier and kmi.key_modifier != 'NONE': + s.append(f"\"key_modifier\": '{kmi.key_modifier}'") + + return "{" + ", ".join(s) + "}" + + +def _kmi_properties_to_lines_recursive(level, properties, lines): + from bpy.types import OperatorProperties + + def string_value(value): + if isinstance(value, (str, bool, int)): + return repr(value) + elif isinstance(value, float): + return repr_f32(value) + elif getattr(value, '__len__', False): + return repr(tuple(value)) + raise Exception(f"Export key configuration: can't write {value!r}") + + for pname in properties.bl_rna.properties.keys(): + if pname != "rna_type": + value = getattr(properties, pname) + if isinstance(value, OperatorProperties): + lines_test = [] + _kmi_properties_to_lines_recursive(level + 2, value, lines_test) + if lines_test: + lines.append(f"(") + lines.append(f"\"{pname}\",\n") + lines.append(f"{indent(level + 3)}" "[") + lines.extend(lines_test) + lines.append("],\n") + lines.append(f"{indent(level + 3)}" "),\n" f"{indent(level + 2)}") + del lines_test + elif properties.is_property_set(pname): + value = string_value(value) + lines.append((f"(\"{pname}\", {value:s}),\n" f"{indent(level + 2)}")) + + +def _kmi_properties_to_lines(level, kmi_props, lines): + if kmi_props is None: + return + + lines_test = [f"\"properties\":\n" f"{indent(level + 1)}" "["] + _kmi_properties_to_lines_recursive(level, kmi_props, lines_test) + if len(lines_test) > 1: + lines_test.append("],\n") + lines.extend(lines_test) + + +def _kmi_attrs_or_none(level, kmi): + lines = [] + _kmi_properties_to_lines(level + 1, kmi.properties, lines) + if kmi.active is False: + lines.append(f"{indent(level)}\"active\":" "False,\n") + if not lines: + return None + return "".join(lines) + + +def keyconfig_export_as_data(wm, kc, filepath, *, all_keymaps=False): + # Alternate foramt + + # Generate a list of keymaps to export: + # + # First add all user_modified keymaps (found in keyconfigs.user.keymaps list), + # then add all remaining keymaps from the currently active custom keyconfig. + # + # This will create a final list of keymaps that can be used as a "diff" against + # the default blender keyconfig, recreating the current setup from a fresh blender + # without needing to export keymaps which haven't been edited. + + from .keyconfig_utils import keyconfig_merge + + class FakeKeyConfig: + keymaps = [] + edited_kc = FakeKeyConfig() + for km in wm.keyconfigs.user.keymaps: + if all_keymaps or km.is_user_modified: + edited_kc.keymaps.append(km) + # merge edited keymaps with non-default keyconfig, if it exists + if kc != wm.keyconfigs.default: + export_keymaps = keyconfig_merge(edited_kc, kc) + else: + export_keymaps = keyconfig_merge(edited_kc, edited_kc) + + with open(filepath, "w") as fh: + fw = fh.write + fw("keyconfig_data = \\\n[") + + for km, kc_x in export_keymaps: + km = km.active() + fw("(") + fw(f"\"{km.name:s}\",\n") + fw(f"{indent(2)}" "{") + fw(f"\"space_type\": '{km.space_type:s}'") + fw(f", \"region_type\": '{km.region_type:s}'") + # We can detect from the kind of items. + if km.is_modal: + fw(", \"modal\": True") + fw("},\n") + fw(f"{indent(2)}" "{") + is_modal = km.is_modal + fw(f"\"items\":\n") + fw(f"{indent(3)}[") + for kmi in km.keymap_items: + if is_modal: + kmi_id = kmi.propvalue + else: + kmi_id = kmi.idname + fw(f"(") + kmi_args = kmi_args_as_data(kmi) + kmi_data = _kmi_attrs_or_none(4, kmi) + fw(f"\"{kmi_id:s}\"") + if kmi_data is None: + fw(f", ") + else: + fw(",\n" f"{indent(5)}") + + fw(kmi_args) + if kmi_data is None: + fw(", None),\n") + else: + fw(",\n") + fw(f"{indent(5)}" "{") + fw(kmi_data) + fw(f"{indent(6)}") + fw("},\n" f"{indent(5)}") + fw("),\n") + fw(f"{indent(4)}") + fw("],\n" f"{indent(3)}") + fw("},\n" f"{indent(2)}") + fw("),\n" f"{indent(1)}") + + fw("]\n") + fw("\n\n") + fw("if __name__ == \"__main__\":\n") + fw(" import os\n") + fw(" from bpy_extras.keyconfig_utils import keyconfig_import_from_data\n") + fw(" keyconfig_import_from_data(os.path.splitext(os.path.basename(__file__))[0], keyconfig_data)\n") + + +def keyconfig_import_from_data(name, keyconfig_data): + # Load data in the format defined above. + # + # Runs at load time, keep this fast! + + def kmi_props_setattr(kmi_props, attr, value): + if type(value) is list: + kmi_subprop = getattr(kmi_props, attr) + for subattr, subvalue in value: + kmi_props_setattr(kmi_subprop, subattr, subvalue) + return + + try: + setattr(kmi_props, attr, value) + except AttributeError: + print(f"Warning: property '{attr}' not found in keymap item '{kmi_props.__class__.__name__}'") + except Exception as ex: + print(f"Warning: {ex!r}") + + import bpy + wm = bpy.context.window_manager + kc = wm.keyconfigs.new(name) + del name + + for (km_name, km_args, km_content) in keyconfig_data: + km = kc.keymaps.new(km_name, **km_args) + is_modal = km_args.get("modal", False) + new_fn = getattr(km.keymap_items, "new_modal" if is_modal else "new") + for (kmi_idname, kmi_args, kmi_data) in km_content["items"]: + kmi = new_fn(kmi_idname, **kmi_args) + if kmi_data is not None: + if not kmi_data.get("active", True): + kmi.active = False + kmi_props_data = kmi_data.get("properties", None) + if kmi_props_data is not None: + kmi_props = kmi.properties + for attr, value in kmi_props_data: + kmi_props_setattr(kmi_props, attr, value) diff --git a/release/scripts/modules/bpy_extras/mesh_utils.py b/release/scripts/modules/bpy_extras/mesh_utils.py index a7872daca67..a09282da2fe 100644 --- a/release/scripts/modules/bpy_extras/mesh_utils.py +++ b/release/scripts/modules/bpy_extras/mesh_utils.py @@ -20,13 +20,12 @@ __all__ = ( "mesh_linked_uv_islands", - "mesh_linked_tessfaces", + "mesh_linked_triangles", "edge_face_count_dict", "edge_face_count", - "edge_loops_from_tessfaces", "edge_loops_from_edges", "ngon_tessellate", - "face_random_points", + "triangle_random_points", ) @@ -90,41 +89,41 @@ def mesh_linked_uv_islands(mesh): return poly_islands -def mesh_linked_tessfaces(mesh): +def mesh_linked_triangles(mesh): """ - Splits the mesh into connected faces, use this for separating cubes from + Splits the mesh into connected triangles, use this for separating cubes from other mesh elements within 1 mesh datablock. :arg mesh: the mesh used to group with. :type mesh: :class:`bpy.types.Mesh` - :return: lists of lists containing faces. + :return: lists of lists containing triangles. :rtype: list """ # Build vert face connectivity - vert_faces = [[] for i in range(len(mesh.vertices))] - for f in mesh.tessfaces: - for v in f.vertices: - vert_faces[v].append(f) + vert_tris = [[] for i in range(len(mesh.vertices))] + for t in mesh.loop_triangles: + for v in t.vertices: + vert_tris[v].append(t) - # sort faces into connectivity groups - face_groups = [[f] for f in mesh.tessfaces] - # map old, new face location - face_mapping = list(range(len(mesh.tessfaces))) + # sort triangles into connectivity groups + tri_groups = [[t] for t in mesh.loop_triangles] + # map old, new tri location + tri_mapping = list(range(len(mesh.loop_triangles))) - # Now clump faces iteratively + # Now clump triangles iteratively ok = True while ok: ok = False - for i, f in enumerate(mesh.tessfaces): - mapped_index = face_mapping[f.index] - mapped_group = face_groups[mapped_index] + for i, t in enumerate(mesh.loop_triangles): + mapped_index = tri_mapping[t.index] + mapped_group = tri_groups[mapped_index] - for v in f.vertices: - for nxt_f in vert_faces[v]: - if nxt_f != f: - nxt_mapped_index = face_mapping[nxt_f.index] + for v in t.vertices: + for nxt_t in vert_tris[v]: + if nxt_t != t: + nxt_mapped_index = tri_mapping[nxt_t.index] # We are not a part of the same group if mapped_index != nxt_mapped_index: @@ -132,18 +131,18 @@ def mesh_linked_tessfaces(mesh): # Assign mapping to this group so they # all map to this group - for grp_f in face_groups[nxt_mapped_index]: - face_mapping[grp_f.index] = mapped_index + for grp_t in tri_groups[nxt_mapped_index]: + tri_mapping[grp_t.index] = mapped_index - # Move faces into this group - mapped_group.extend(face_groups[nxt_mapped_index]) + # Move triangles into this group + mapped_group.extend(tri_groups[nxt_mapped_index]) # remove reference to the list - face_groups[nxt_mapped_index] = None + tri_groups[nxt_mapped_index] = None - # return all face groups that are not null - # this is all the faces that are connected in their own lists. - return [fg for fg in face_groups if fg] + # return all tri groups that are not null + # this is all the triangles that are connected in their own lists. + return [tg for tg in tri_groups if tg] def edge_face_count_dict(mesh): @@ -177,87 +176,6 @@ def edge_face_count(mesh): return [get(edge_face_count, ed.key, 0) for ed in mesh.edges] -def edge_loops_from_tessfaces(mesh, tessfaces=None, seams=()): - """ - Edge loops defined by faces - - Takes me.tessfaces or a list of faces and returns the edge loops - These edge loops are the edges that sit between quads, so they don't touch - 1 quad, note: not connected will make 2 edge loops, - both only containing 2 edges. - - return a list of edge key lists - [[(0, 1), (4, 8), (3, 8)], ...] - - :arg mesh: the mesh used to get edge loops from. - :type mesh: :class:`bpy.types.Mesh` - :arg tessfaces: optional face list to only use some of the meshes faces. - :type tessfaces: :class:`bpy.types.MeshTessFace`, sequence or or NoneType - :return: return a list of edge vertex index lists. - :rtype: list - """ - - OTHER_INDEX = 2, 3, 0, 1 # opposite face index - - if tessfaces is None: - tessfaces = mesh.tessfaces - - edges = {} - - for f in tessfaces: - if len(f.vertices) == 4: - edge_keys = f.edge_keys - for i, edkey in enumerate(f.edge_keys): - edges.setdefault(edkey, []).append(edge_keys[OTHER_INDEX[i]]) - - for edkey in seams: - edges[edkey] = [] - - # Collect edge loops here - edge_loops = [] - - for edkey, ed_adj in edges.items(): - if 0 < len(ed_adj) < 3: # 1 or 2 - # Seek the first edge - context_loop = [edkey, ed_adj[0]] - edge_loops.append(context_loop) - if len(ed_adj) == 2: - other_dir = ed_adj[1] - else: - other_dir = None - - del ed_adj[:] - - flipped = False - - while 1: - # from knowing the last 2, look for the next. - ed_adj = edges[context_loop[-1]] - if len(ed_adj) != 2: - # the original edge had 2 other edges - if other_dir and flipped is False: - flipped = True # only flip the list once - context_loop.reverse() - del ed_adj[:] - context_loop.append(other_dir) # save 1 look-up - - ed_adj = edges[context_loop[-1]] - if len(ed_adj) != 2: - del ed_adj[:] - break - else: - del ed_adj[:] - break - - i = ed_adj.index(context_loop[-2]) - context_loop.append(ed_adj[not i]) - - # Don't look at this again - del ed_adj[:] - - return edge_loops - - def edge_loops_from_edges(mesh, edges=None): """ Edge loops defined by edges @@ -511,54 +429,42 @@ def ngon_tessellate(from_data, indices, fix_loops=True): return fill -def face_random_points(num_points, tessfaces): +def triangle_random_points(num_points, loop_triangles): """ - Generates a list of random points over mesh tessfaces. + Generates a list of random points over mesh loop triangles. - :arg num_points: the number of random points to generate on each face. + :arg num_points: the number of random points to generate on each triangle. :type int: - :arg tessfaces: list of the faces to generate points on. - :type tessfaces: :class:`bpy.types.MeshTessFace`, sequence - :return: list of random points over all faces. + :arg loop_triangles: list of the triangles to generate points on. + :type loop_triangles: :class:`bpy.types.MeshLoopTriangle`, sequence + :return: list of random points over all triangles. :rtype: list """ from random import random from mathutils.geometry import area_tri - # Split all quads into 2 tris, tris remain unchanged - tri_faces = [] - for f in tessfaces: - tris = [] - verts = f.id_data.vertices - fv = f.vertices[:] - tris.append((verts[fv[0]].co, - verts[fv[1]].co, - verts[fv[2]].co, - )) - if len(fv) == 4: - tris.append((verts[fv[0]].co, - verts[fv[3]].co, - verts[fv[2]].co, - )) - tri_faces.append(tris) - - # For each face, generate the required number of random points - sampled_points = [None] * (num_points * len(tessfaces)) - for i, tf in enumerate(tri_faces): + # For each triangle, generate the required number of random points + sampled_points = [None] * (num_points * len(loop_triangles)) + for i, lt in enumerate(loop_triangles): + # Get triangle vertex coordinates + verts = lt.id_data.vertices + ltv = lt.vertices[:] + tv = (verts[ltv[0]].co, verts[ltv[1]].co, verts[ltv[2]].co) + for k in range(num_points): # If this is a quad, we need to weight its 2 tris by their area - if len(tf) != 1: - area1 = area_tri(*tf[0]) - area2 = area_tri(*tf[1]) + if len(tv) != 1: + area1 = area_tri(*tv[0]) + area2 = area_tri(*tv[1]) area_tot = area1 + area2 area1 = area1 / area_tot area2 = area2 / area_tot - vecs = tf[0 if (random() < area1) else 1] + vecs = tv[0 if (random() < area1) else 1] else: - vecs = tf[0] + vecs = tv[0] u1 = random() u2 = random() diff --git a/release/scripts/modules/bpy_extras/node_shader_utils.py b/release/scripts/modules/bpy_extras/node_shader_utils.py new file mode 100644 index 00000000000..d06d66d3cb5 --- /dev/null +++ b/release/scripts/modules/bpy_extras/node_shader_utils.py @@ -0,0 +1,774 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import bpy +from mathutils import Color, Vector + +__all__ = ( + "PrincipledBSDFWrapper", +) + + +def _set_check(func): + from functools import wraps + + @wraps(func) + def wrapper(self, *args, **kwargs): + if self.is_readonly: + assert(not "Trying to set value to read-only shader!") + return + return func(self, *args, **kwargs) + return wrapper + +def rgb_to_rgba(rgb): + return list(rgb) + [1.0] + +def rgba_to_rgb(rgba): + return Color((rgba[0], rgba[1], rgba[2])) + + +class ShaderWrapper(): + """ + Base class with minimal common ground for all types of shader interfaces we may want/need to implement. + """ + + # The two mandatory nodes any children class should support. + NODES_LIST = ( + "node_out", + + "_node_texcoords", + ) + + __slots__ = ( + "is_readonly", + "material", + "_textures", + "_grid_locations", + *NODES_LIST, + ) + + _col_size = 300 + _row_size = 300 + + def _grid_to_location(self, x, y, dst_node=None, ref_node=None): + if ref_node is not None: # x and y are relative to this node location. + nx = round(ref_node.location.x / self._col_size) + ny = round(ref_node.location.y / self._row_size) + x += nx + y += ny + loc = None + while True: + loc = (x * self._col_size, y * self._row_size) + if loc not in self._grid_locations: + break + loc = (x * self._col_size, (y - 1) * self._row_size) + if loc not in self._grid_locations: + break + loc = (x * self._col_size, (y - 2) * self._row_size) + if loc not in self._grid_locations: + break + x -= 1 + self._grid_locations.add(loc) + if dst_node is not None: + dst_node.location = loc + dst_node.width = min(dst_node.width, self._col_size - 20) + return loc + + def __init__(self, material, is_readonly=True, use_nodes=True): + self.is_readonly = is_readonly + self.material = material + if not is_readonly: + self.use_nodes = use_nodes + self.update() + + def update(self): # Should be re-implemented by children classes... + for node in self.NODES_LIST: + setattr(self, node, None) + self._textures = {} + self._grid_locations = set() + + + def use_nodes_get(self): + return self.material.use_nodes + + @_set_check + def use_nodes_set(self, val): + self.material.use_nodes = val + self.update() + + use_nodes = property(use_nodes_get, use_nodes_set) + + + def node_texcoords_get(self): + if not self.use_nodes: + return None + if self._node_texcoords is ...: + # Running only once, trying to find a valid texcoords node. + for n in self.material.node_tree.nodes: + if n.bl_idname == 'ShaderNodeTexCoord': + self._node_texcoords = n + self._grid_to_location(0, 0, ref_node=n) + break + if self._node_texcoords is ...: + self._node_texcoords = None + if self._node_texcoords is None and not self.is_readonly: + tree = self.material.node_tree + nodes = tree.nodes + links = tree.links + + node_texcoords = nodes.new(type='ShaderNodeTexCoord') + node_texcoords.label = "Texture Coords" + self._grid_to_location(-5, 1, dst_node=node_texcoords) + self._node_texcoords = node_texcoords + return self._node_texcoords + + node_texcoords = property(node_texcoords_get) + + +class PrincipledBSDFWrapper(ShaderWrapper): + """ + Hard coded shader setup, based in Principled BSDF. + Should cover most common cases on import, and gives a basic nodal shaders support for export. + Supports basic: diffuse/spec/reflect/transparency/normal, with texturing. + """ + NODES_LIST = ( + "node_out", + "node_principled_bsdf", + + "_node_normalmap", + "_node_texcoords", + ) + + __slots__ = ( + "is_readonly", + "material", + *NODES_LIST, + ) + + NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST + + def __init__(self, material, is_readonly=True, use_nodes=True): + super(PrincipledBSDFWrapper, self).__init__(material, is_readonly, use_nodes) + + + def update(self): + super(PrincipledBSDFWrapper, self).update() + + if not self.use_nodes: + return + + tree = self.material.node_tree + + nodes = tree.nodes + links = tree.links + + # -------------------------------------------------------------------- + # Main output and shader. + node_out = None + node_principled = None + for n in nodes: + if n.bl_idname == 'ShaderNodeOutputMaterial' and n.inputs[0].is_linked: + node_out = n + node_principled = n.inputs[0].links[0].from_node + elif n.bl_idname == 'ShaderNodeBsdfPrincipled' and n.outputs[0].is_linked: + node_principled = n + for lnk in n.outputs[0].links: + node_out = lnk.to_node + if node_out.bl_idname == 'ShaderNodeOutputMaterial': + break + if ( + node_out is not None and node_principled is not None and + node_out.bl_idname == 'ShaderNodeOutputMaterial' and + node_principled.bl_idname == 'ShaderNodeBsdfPrincipled' + ): + break + node_out = node_principled = None # Could not find a valid pair, let's try again + + if node_out is not None: + self._grid_to_location(0, 0, ref_node=node_out) + elif not self.is_readonly: + node_out = nodes.new(type='ShaderNodeOutputMaterial') + node_out.label = "Material Out" + node_out.target = 'ALL' + self._grid_to_location(1, 1, dst_node=node_out) + self.node_out = node_out + + if node_principled is not None: + self._grid_to_location(0, 0, ref_node=node_principled) + elif not self.is_readonly: + node_principled = nodes.new(type='ShaderNodeBsdfPrincipled') + node_principled.label = "Principled BSDF" + self._grid_to_location(0, 1, dst_node=node_principled) + # Link + links.new(node_principled.outputs["BSDF"], self.node_out.inputs["Surface"]) + self.node_principled_bsdf = node_principled + + # -------------------------------------------------------------------- + # Normal Map, lazy initialization... + self._node_normalmap = ... + + # -------------------------------------------------------------------- + # Tex Coords, lazy initialization... + self._node_texcoords = ... + + + def node_normalmap_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + node_principled = self.node_principled_bsdf + if self._node_normalmap is ...: + # Running only once, trying to find a valid normalmap node. + if node_principled.inputs["Normal"].is_linked: + node_normalmap = node_principled.inputs["Normal"].links[0].from_node + if node_normalmap.bl_idname == 'ShaderNodeNormalMap': + self._node_normalmap = node_normalmap + self._grid_to_location(0, 0, ref_node=node_normalmap) + if self._node_normalmap is ...: + self._node_normalmap = None + if self._node_normalmap is None and not self.is_readonly: + tree = self.material.node_tree + nodes = tree.nodes + links = tree.links + + node_normalmap = nodes.new(type='ShaderNodeNormalMap') + node_normalmap.label = "Normal/Map" + self._grid_to_location(-1, -2, dst_node=node_normalmap, ref_node=node_principled) + # Link + links.new(node_normalmap.outputs["Normal"], node_principled.inputs["Normal"]) + return self._node_normalmap + + node_normalmap = property(node_normalmap_get) + + + # -------------------------------------------------------------------- + # Base Color. + + def base_color_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return self.material.diffuse_color + return rgba_to_rgb(self.node_principled_bsdf.inputs["Base Color"].default_value) + + @_set_check + def base_color_set(self, color): + self.material.diffuse_color = color + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Base Color"].default_value = rgb_to_rgba(color) + + base_color = property(base_color_get, base_color_set) + + + def base_color_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["Base Color"], + grid_row_diff=1, + ) + + base_color_texture = property(base_color_texture_get) + + + # -------------------------------------------------------------------- + # Specular. + + def specular_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return self.material.specular_intensity + return self.node_principled_bsdf.inputs["Specular"].default_value + + @_set_check + def specular_set(self, value): + self.material.specular_intensity = value + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Specular"].default_value = value + + specular = property(specular_get, specular_set) + + + def specular_tint_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return 0.0 + return rgba_to_rgb(self.node_principled_bsdf.inputs["Specular Tint"].default_value) + + @_set_check + def specular_tint_set(self, value): + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Specular Tint"].default_value = rgb_to_rgba(value) + + specular_tint = property(specular_tint_get, specular_tint_set) + + + # Will only be used as gray-scale one... + def specular_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + print("NO NODES!") + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["Specular"], + grid_row_diff=0, + ) + + specular_texture = property(specular_texture_get) + + + # -------------------------------------------------------------------- + # Roughness (also sort of inverse of specular hardness...). + + def roughness_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return self.material.roughness + return self.node_principled_bsdf.inputs["Roughness"].default_value + + @_set_check + def roughness_set(self, value): + self.material.roughness = value + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Roughness"].default_value = value + + roughness = property(roughness_get, roughness_set) + + + # Will only be used as gray-scale one... + def roughness_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["Roughness"], + grid_row_diff=0, + ) + + roughness_texture = property(roughness_texture_get) + + + # -------------------------------------------------------------------- + # Metallic (a.k.a reflection, mirror). + + def metallic_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return self.material.metallic + return self.node_principled_bsdf.inputs["Metallic"].default_value + + @_set_check + def metallic_set(self, value): + self.material.metallic = value + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Metallic"].default_value = value + + metallic = property(metallic_get, metallic_set) + + + # Will only be used as gray-scale one... + def metallic_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["Metallic"], + grid_row_diff=0, + ) + + metallic_texture = property(metallic_texture_get) + + + # -------------------------------------------------------------------- + # Transparency settings. + + def ior_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return 1.0 + return self.node_principled_bsdf.inputs["IOR"].default_value + + @_set_check + def ior_set(self, value): + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["IOR"].default_value = value + + ior = property(ior_get, ior_set) + + + # Will only be used as gray-scale one... + def ior_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["IOR"], + grid_row_diff=-1, + ) + + ior_texture = property(ior_texture_get) + + + def transmission_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return 0.0 + return self.node_principled_bsdf.inputs["Transmission"].default_value + + @_set_check + def transmission_set(self, value): + if self.use_nodes and self.node_principled_bsdf is not None: + self.node_principled_bsdf.inputs["Transmission"].default_value = value + + transmission = property(transmission_get, transmission_set) + + + # Will only be used as gray-scale one... + def transmission_texture_get(self): + if not self.use_nodes or self.node_principled_bsdf is None: + return None + return ShaderImageTextureWrapper( + self, self.node_principled_bsdf, + self.node_principled_bsdf.inputs["Transmission"], + grid_row_diff=-1, + ) + + transmission_texture = property(transmission_texture_get) + + + # TODO: Do we need more complex handling for alpha (allowing masking and such)? + # Would need extra mixing nodes onto Base Color maybe, or even its own shading chain... + + # -------------------------------------------------------------------- + # Normal map. + + def normalmap_strength_get(self): + if not self.use_nodes or self.node_normalmap is None: + return 0.0 + return self.node_normalmap.inputs["Strength"].default_value + + @_set_check + def normalmap_strength_set(self, value): + if self.use_nodes and self.node_normalmap is not None: + self.node_normalmap.inputs["Strength"].default_value = value + + normalmap_strength = property(normalmap_strength_get, normalmap_strength_set) + + + def normalmap_texture_get(self): + if not self.use_nodes or self.node_normalmap is None: + return None + return ShaderImageTextureWrapper( + self, self.node_normalmap, + self.node_normalmap.inputs["Color"], + grid_row_diff=-2, + ) + + normalmap_texture = property(normalmap_texture_get) + + + +class ShaderImageTextureWrapper(): + """ + Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations), + and texture coordinates source. + """ + + # Note: this class assumes we are using nodes, otherwise it should never be used... + + NODES_LIST = ( + "node_dst", + "socket_dst", + + "_node_image", + "_node_mapping", + ) + + __slots__ = ( + "owner_shader", + "is_readonly", + "grid_row_diff", + "use_alpha", + *NODES_LIST, + ) + + def __new__(cls, owner_shader: ShaderWrapper, node_dst, socket_dst, *args, **kwargs): + instance = owner_shader._textures.get((node_dst, socket_dst), None) + if instance is not None: + return instance + instance = super(ShaderImageTextureWrapper, cls).__new__(cls) + owner_shader._textures[(node_dst, socket_dst)] = instance + return instance + + def __init__(self, owner_shader: ShaderWrapper, node_dst, socket_dst, grid_row_diff=0, use_alpha=False): + self.owner_shader = owner_shader + self.is_readonly = owner_shader.is_readonly + self.node_dst = node_dst + self.socket_dst = socket_dst + self.grid_row_diff = grid_row_diff + self.use_alpha = use_alpha + + self._node_image = ... + self._node_mapping = ... + + tree = node_dst.id_data + nodes = tree.nodes + links = tree.links + + if socket_dst.is_linked: + from_node = socket_dst.links[0].from_node + if from_node.bl_idname == 'ShaderNodeTexImage': + self._node_image = from_node + + if self.node_image is not None: + socket_dst = self.node_image.inputs["Vector"] + if socket_dst.is_linked: + from_node = socket_dst.links[0].from_node + if from_node.bl_idname == 'ShaderNodeMapping': + self._node_mapping = from_node + + + def copy_from(self, tex): + # Avoid generating any node in source texture. + is_readonly_back = tex.is_readonly + tex.is_readonly = True + + if tex.node_image is not None: + self.image = tex.image + self.projection = tex.projection + self.texcoords = tex.texcoords + self.copy_mapping_from(tex) + + tex.is_readonly = is_readonly_back + + + def copy_mapping_from(self, tex): + # Avoid generating any node in source texture. + is_readonly_back = tex.is_readonly + tex.is_readonly = True + + if tex.node_mapping is None: # Used to actually remove mapping node. + if self.has_mapping_node(): + # We assume node_image can never be None in that case... + # Find potential existing link into image's Vector input. + socket_dst = socket_src = None + if self.node_mapping.inputs["Vector"].is_linked: + socket_dst = self.node_image.inputs["Vector"] + socket_src = self.node_mapping.inputs["Vector"].links[0].from_socket + + tree = self.owner_shader.material.node_tree + tree.nodes.remove(self.node_mapping) + self._node_mapping = None + + # If previously existing, re-link texcoords -> image + if socket_src is not None: + tree.links.new(socket_src, socket_dst) + elif self.node_mapping is not None: + self.translation = tex.translation + self.rotation = tex.rotation + self.scale = tex.scale + self.use_min = tex.use_min + self.use_max = tex.use_max + self.min = tex.min + self.max = tex.max + + tex.is_readonly = is_readonly_back + + + # -------------------------------------------------------------------- + # Image. + + def node_image_get(self): + if self._node_image is ...: + # Running only once, trying to find a valid image node. + if self.socket_dst.is_linked: + node_image = self.socket_dst.links[0].from_node + if node_image.bl_idname == 'ShaderNodeTexImage': + self._node_image = node_image + self.owner_shader._grid_to_location(0, 0, ref_node=node_image) + if self._node_image is ...: + self._node_image = None + if self._node_image is None and not self.is_readonly: + tree = self.owner_shader.material.node_tree + + node_image = tree.nodes.new(type='ShaderNodeTexImage') + self.owner_shader._grid_to_location(-1, 0 + self.grid_row_diff, dst_node=node_image, ref_node=self.node_dst) + + tree.links.new(node_image.outputs["Alpha" if self.use_alpha else "Color"], self.socket_dst) + + self._node_image = node_image + return self._node_image + + node_image = property(node_image_get) + + + def image_get(self): + return self.node_image.image if self.node_image is not None else None + + @_set_check + def image_set(self, image): + self.node_image.image = image + + image = property(image_get, image_set) + + + def projection_get(self): + return self.node_image.projection if self.node_image is not None else 'FLAT' + + @_set_check + def projection_set(self, projection): + self.node_image.projection = projection + + projection = property(projection_get, projection_set) + + + def texcoords_get(self): + if self.node_image is not None: + socket = (self.node_mapping if self.has_mapping_node() else self.node_image).inputs["Vector"] + if socket.is_linked: + return socket.links[0].from_socket.name + return 'UV' + + @_set_check + def texcoords_set(self, texcoords): + # Image texture node already defaults to UVs, no extra node needed. + # ONLY in case we do not have any texcoords mapping!!! + if texcoords == 'UV' and not self.has_mapping_node(): + return + tree = self.node_image.id_data + links = tree.links + node_dst = self.node_mapping if self.has_mapping_node() else self.node_image + socket_src = self.owner_shader.node_texcoords.outputs[texcoords] + links.new(socket_src, node_dst.inputs["Vector"]) + + texcoords = property(texcoords_get, texcoords_set) + + + def extension_get(self): + return self.node_image.extension if self.node_image is not None else 'REPEAT' + + @_set_check + def extension_set(self, extension): + self.node_image.extension = extension + + extension = property(extension_get, extension_set) + + + # -------------------------------------------------------------------- + # Mapping. + + def has_mapping_node(self): + return self._node_mapping not in {None, ...} + + def node_mapping_get(self): + if self._node_mapping is ...: + # Running only once, trying to find a valid mapping node. + if self.node_image is None: + return None + if self.node_image.inputs["Vector"].is_linked: + node_mapping = self.node_image.inputs["Vector"].links[0].from_node + if node_mapping.bl_idname == 'ShaderNodeMapping': + self._node_mapping = node_mapping + self.owner_shader._grid_to_location(0, 0 + self.grid_row_diff, ref_node=node_mapping) + if self._node_mapping is ...: + self._node_mapping = None + if self._node_mapping is None and not self.is_readonly: + # Find potential existing link into image's Vector input. + socket_dst = self.node_image.inputs["Vector"] + # If not already existing, we need to create texcoords -> mapping link (from UV). + socket_src = (socket_dst.links[0].from_socket if socket_dst.is_linked + else self.owner_shader.node_texcoords.outputs['UV']) + + tree = self.owner_shader.material.node_tree + node_mapping = tree.nodes.new(type='ShaderNodeMapping') + node_mapping.vector_type = 'TEXTURE' + self.owner_shader._grid_to_location(-1, 0, dst_node=node_mapping, ref_node=self.node_image) + + # Link mapping -> image node. + tree.links.new(node_mapping.outputs["Vector"], socket_dst) + # Link texcoords -> mapping. + tree.links.new(socket_src, node_mapping.inputs["Vector"]) + + self._node_mapping = node_mapping + return self._node_mapping + + node_mapping = property(node_mapping_get) + + + def translation_get(self): + return self.node_mapping.translation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) + + @_set_check + def translation_set(self, translation): + self.node_mapping.translation = translation + + translation = property(translation_get, translation_set) + + + def rotation_get(self): + return self.node_mapping.rotation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) + + @_set_check + def rotation_set(self, rotation): + self.node_mapping.rotation = rotation + + rotation = property(rotation_get, rotation_set) + + + def scale_get(self): + return self.node_mapping.scale if self.node_mapping is not None else Vector((1.0, 1.0, 1.0)) + + @_set_check + def scale_set(self, scale): + self.node_mapping.scale = scale + + scale = property(scale_get, scale_set) + + + def use_min_get(self): + return self.node_mapping.use_min if self_mapping.node is not None else False + + @_set_check + def use_min_set(self, use_min): + self.node_mapping.use_min = use_min + + use_min = property(use_min_get, use_min_set) + + + def use_max_get(self): + return self.node_mapping.use_max if self_mapping.node is not None else False + + @_set_check + def use_max_set(self, use_max): + self.node_mapping.use_max = use_max + + use_max = property(use_max_get, use_max_set) + + + def min_get(self): + return self.node_mapping.min if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) + + @_set_check + def min_set(self, min): + self.node_mapping.min = min + + min = property(min_get, min_set) + + + def max_get(self): + return self.node_mapping.max if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) + + @_set_check + def max_set(self, max): + self.node_mapping.max = max + + max = property(max_get, max_set) diff --git a/release/scripts/modules/bpy_extras/node_utils.py b/release/scripts/modules/bpy_extras/node_utils.py new file mode 100644 index 00000000000..d4c6d5cd45a --- /dev/null +++ b/release/scripts/modules/bpy_extras/node_utils.py @@ -0,0 +1,32 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +__all__ = ( + "find_node_input", +) + + +# XXX Names are not unique. Returns the first match. +def find_node_input(node, name): + for input in node.inputs: + if input.name == name: + return input + + return None diff --git a/release/scripts/modules/bpy_extras/object_utils.py b/release/scripts/modules/bpy_extras/object_utils.py index 0a48d99b529..ab32daa9d67 100644 --- a/release/scripts/modules/bpy_extras/object_utils.py +++ b/release/scripts/modules/bpy_extras/object_utils.py @@ -100,12 +100,12 @@ def add_object_align_init(context, operator): if operator: properties.rotation = rotation.to_euler() - return location * rotation + return location @ rotation -def object_data_add(context, obdata, operator=None, use_active_layer=True, name=None): +def object_data_add(context, obdata, operator=None, name=None): """ - Add an object using the view context and preference to to initialize the + Add an object using the view context and preference to initialize the location, rotation and layer. :arg context: The context to use. @@ -117,53 +117,31 @@ def object_data_add(context, obdata, operator=None, use_active_layer=True, name= :arg name: Optional name :type name: string :return: the newly created object in the scene. - :rtype: :class:`bpy.types.ObjectBase` + :rtype: :class:`bpy.types.Object` """ scene = context.scene + layer = context.view_layer + layer_collection = context.layer_collection - # ugh, could be made nicer - for ob in scene.objects: - ob.select = False + for ob in layer.objects: + ob.select_set(action='DESELECT') + + if not layer_collection: + # when there is no collection linked to this view_layer create one + scene_collection = scene.master_collection.collections.new("") + layer_collection = layer.collections.link(scene_collection) + else: + scene_collection = layer_collection.collection if name is None: name = "Object" if obdata is None else obdata.name + obj_act = layer.objects.active obj_new = bpy.data.objects.new(name, obdata) - - base = scene.objects.link(obj_new) - base.select = True - - v3d = None - if context.space_data and context.space_data.type == 'VIEW_3D': - v3d = context.space_data - - if v3d and v3d.local_view: - base.layers_from_view(context.space_data) - - if operator is not None and any(operator.layers): - base.layers = operator.layers - else: - if use_active_layer: - if v3d and v3d.local_view: - base.layers[scene.active_layer] = True - else: - if v3d and not v3d.lock_camera_and_layers: - base.layers = [True if i == v3d.active_layer - else False for i in range(len(v3d.layers))] - else: - base.layers = [True if i == scene.active_layer - else False for i in range(len(scene.layers))] - else: - if v3d: - base.layers_from_view(context.space_data) - - if operator is not None: - operator.layers = base.layers - + scene_collection.objects.link(obj_new) + obj_new.select_set(action='SELECT') obj_new.matrix_world = add_object_align_init(context, operator) - obj_act = scene.objects.active - # XXX # caused because entering edit-mode does not add a empty undo slot! if context.user_preferences.edit.use_enter_edit_mode: @@ -174,8 +152,8 @@ def object_data_add(context, obdata, operator=None, use_active_layer=True, name= _obdata = bpy.data.meshes.new(name) obj_act = bpy.data.objects.new(_obdata.name, _obdata) obj_act.matrix_world = obj_new.matrix_world - scene.objects.link(obj_act) - scene.objects.active = obj_act + scene_collection.objects.link(obj_act) + layer.objects.active = obj_act bpy.ops.object.mode_set(mode='EDIT') # need empty undo step bpy.ops.ed.undo_push(message="Enter Editmode") @@ -183,11 +161,12 @@ def object_data_add(context, obdata, operator=None, use_active_layer=True, name= if obj_act and obj_act.mode == 'EDIT' and obj_act.type == obj_new.type: bpy.ops.mesh.select_all(action='DESELECT') + obj_act.select_set(action='SELECT') bpy.ops.object.mode_set(mode='OBJECT') - obj_act.select = True + obj_act.select_set(action='SELECT') scene.update() # apply location - # scene.objects.active = obj_new + # layer.objects.active = obj_new # Match up UV layers, this is needed so adding an object with UV's # doesn't create new layers when there happens to be a naming mis-match. @@ -200,16 +179,14 @@ def object_data_add(context, obdata, operator=None, use_active_layer=True, name= bpy.ops.object.join() # join into the active. if obdata: bpy.data.meshes.remove(obdata) - # base is freed, set to active object - base = scene.object_bases.active bpy.ops.object.mode_set(mode='EDIT') else: - scene.objects.active = obj_new + layer.objects.active = obj_new if context.user_preferences.edit.use_enter_edit_mode: bpy.ops.object.mode_set(mode='EDIT') - return base + return obj_new class AddObjectHelper: @@ -217,25 +194,19 @@ class AddObjectHelper: if not self.view_align: self.rotation.zero() - view_align = BoolProperty( + view_align: BoolProperty( name="Align to View", default=False, update=view_align_update_callback, ) - location = FloatVectorProperty( + location: FloatVectorProperty( name="Location", subtype='TRANSLATION', ) - rotation = FloatVectorProperty( + rotation: FloatVectorProperty( name="Rotation", subtype='EULER', ) - layers = BoolVectorProperty( - name="Layers", - size=20, - subtype='LAYER', - options={'HIDDEN', 'SKIP_SAVE'}, - ) @classmethod def poll(self, context): @@ -251,7 +222,7 @@ def object_add_grid_scale(context): space_data = context.space_data if space_data and space_data.type == 'VIEW_3D': - return space_data.grid_scale_unit + return space_data.overlay.grid_scale_unit return 1.0 @@ -337,7 +308,7 @@ def world_to_camera_view(scene, obj, coord): """ from mathutils import Vector - co_local = obj.matrix_world.normalized().inverted() * coord + co_local = obj.matrix_world.normalized().inverted() @ coord z = -co_local.z camera = obj.data diff --git a/release/scripts/modules/bpy_extras/view3d_utils.py b/release/scripts/modules/bpy_extras/view3d_utils.py index a4834a4531c..a2e394b270f 100644 --- a/release/scripts/modules/bpy_extras/view3d_utils.py +++ b/release/scripts/modules/bpy_extras/view3d_utils.py @@ -54,7 +54,7 @@ def region_2d_to_vector_3d(region, rv3d, coord): w = out.dot(persinv[3].xyz) + persinv[3][3] - view_vector = ((persinv * out) / w) - viewinv.translation + view_vector = ((persinv @ out) / w) - viewinv.translation else: view_vector = -viewinv.col[2].xyz @@ -179,7 +179,7 @@ def location_3d_to_region_2d(region, rv3d, coord, default=None): """ from mathutils import Vector - prj = rv3d.perspective_matrix * Vector((coord[0], coord[1], coord[2], 1.0)) + prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0)) if prj.w > 0.0: width_half = region.width / 2.0 height_half = region.height / 2.0 |