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

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'release/scripts/modules/bpy_extras')
-rw-r--r--release/scripts/modules/bpy_extras/__init__.py1
-rw-r--r--release/scripts/modules/bpy_extras/anim_utils.py25
-rw-r--r--release/scripts/modules/bpy_extras/image_utils.py2
-rw-r--r--release/scripts/modules/bpy_extras/io_utils.py113
-rw-r--r--release/scripts/modules/bpy_extras/keyconfig_utils.py114
-rw-r--r--release/scripts/modules/bpy_extras/keyconfig_utils_experimental.py244
-rw-r--r--release/scripts/modules/bpy_extras/mesh_utils.py190
-rw-r--r--release/scripts/modules/bpy_extras/node_shader_utils.py774
-rw-r--r--release/scripts/modules/bpy_extras/node_utils.py32
-rw-r--r--release/scripts/modules/bpy_extras/object_utils.py89
-rw-r--r--release/scripts/modules/bpy_extras/view3d_utils.py4
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