Add Chromium-only Blender WebEngine parity work
This commit is contained in:
20
blender-5.2.0/scripts/modules/bpy_extras/__init__.py
Normal file
20
blender-5.2.0/scripts/modules/bpy_extras/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Utility modules associated with the bpy module.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"anim_utils",
|
||||
"asset_utils",
|
||||
"object_utils",
|
||||
"io_utils",
|
||||
"image_utils",
|
||||
"keyconfig_utils",
|
||||
"mesh_utils",
|
||||
"node_utils",
|
||||
"view3d_utils",
|
||||
"id_map_utils",
|
||||
)
|
||||
1178
blender-5.2.0/scripts/modules/bpy_extras/anim_utils.py
Normal file
1178
blender-5.2.0/scripts/modules/bpy_extras/anim_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
91
blender-5.2.0/scripts/modules/bpy_extras/asset_utils.py
Normal file
91
blender-5.2.0/scripts/modules/bpy_extras/asset_utils.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Helpers for asset management tasks.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"AssetBrowserPanel",
|
||||
"AssetMetaDataPanel",
|
||||
"SpaceAssetInfo",
|
||||
)
|
||||
|
||||
|
||||
class SpaceAssetInfo:
|
||||
"""Utility class for checking if a space is an asset browser."""
|
||||
|
||||
@classmethod
|
||||
def is_asset_browser(cls, space_data):
|
||||
"""
|
||||
Check if the given space is an asset browser.
|
||||
|
||||
:param space_data: The space to check.
|
||||
:type space_data: :class:`bpy.types.Space`
|
||||
:return: True when the space is an asset browser.
|
||||
:rtype: bool
|
||||
"""
|
||||
return space_data and space_data.type == 'FILE_BROWSER' and space_data.browse_mode == 'ASSETS'
|
||||
|
||||
@classmethod
|
||||
def is_asset_browser_poll(cls, context):
|
||||
"""
|
||||
Poll whether the active space is an asset browser.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when the active space is an asset browser.
|
||||
:rtype: bool
|
||||
"""
|
||||
return cls.is_asset_browser(context.space_data)
|
||||
|
||||
|
||||
class AssetBrowserPanel:
|
||||
"""Mixin class for panels that should only show in the asset browser."""
|
||||
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
|
||||
@classmethod
|
||||
def asset_browser_panel_poll(cls, context):
|
||||
"""
|
||||
Check if the panel should be shown in the asset browser.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when the panel should be visible.
|
||||
:rtype: bool
|
||||
"""
|
||||
return SpaceAssetInfo.is_asset_browser_poll(context)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""
|
||||
Poll for asset browser visibility.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when the panel should be visible.
|
||||
:rtype: bool
|
||||
"""
|
||||
return cls.asset_browser_panel_poll(context)
|
||||
|
||||
|
||||
class AssetMetaDataPanel:
|
||||
"""Mixin class for panels that display asset metadata in the asset browser."""
|
||||
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOL_PROPS'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""
|
||||
Poll for asset browser with active asset metadata.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when the asset browser has active asset data.
|
||||
:rtype: bool
|
||||
"""
|
||||
active_file = context.active_file
|
||||
return SpaceAssetInfo.is_asset_browser_poll(context) and active_file and active_file.asset_data
|
||||
56
blender-5.2.0/scripts/modules/bpy_extras/bmesh_utils.py
Normal file
56
blender-5.2.0/scripts/modules/bpy_extras/bmesh_utils.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"bmesh_linked_uv_islands",
|
||||
)
|
||||
|
||||
|
||||
def match_uv(face, vert, uv, uv_layer):
|
||||
for loop in face.loops:
|
||||
if loop.vert == vert:
|
||||
return uv == loop[uv_layer].uv
|
||||
return False
|
||||
|
||||
|
||||
def bmesh_linked_uv_islands(bm, uv_layer):
|
||||
"""
|
||||
Returns lists of faces connected by UV islands.
|
||||
|
||||
For meshes use :class:`bpy.types.Mesh.mesh_linked_uv_islands` instead.
|
||||
|
||||
:param bm: the bmesh used to group with.
|
||||
:type bmesh: :class:`BMesh`
|
||||
:param uv_layer: the UV layer to source UVs from.
|
||||
:type bmesh: :class:`BMLayerItem`
|
||||
:return: list of lists containing polygon indices
|
||||
:rtype: list[list[int]]
|
||||
"""
|
||||
|
||||
result = []
|
||||
used = set()
|
||||
for seed_face in bm.faces:
|
||||
if seed_face in used:
|
||||
continue # Face has already been processed.
|
||||
used.add(seed_face)
|
||||
island = [seed_face]
|
||||
stack = [seed_face] # Faces still to consider on this island.
|
||||
while stack:
|
||||
current_face = stack.pop()
|
||||
for loop in current_face.loops:
|
||||
v = loop.vert
|
||||
uv = loop[uv_layer].uv
|
||||
for f in v.link_faces:
|
||||
if f is current_face or f in used:
|
||||
continue
|
||||
if not match_uv(f, v, uv, uv_layer):
|
||||
continue
|
||||
|
||||
# `f` is part of island, add to island and stack
|
||||
used.add(f)
|
||||
island.append(f)
|
||||
stack.append(f)
|
||||
result.append(island)
|
||||
|
||||
return result
|
||||
53
blender-5.2.0/scripts/modules/bpy_extras/id_map_utils.py
Normal file
53
blender-5.2.0/scripts/modules/bpy_extras/id_map_utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
__all__ = (
|
||||
"get_id_reference_map",
|
||||
"get_all_referenced_ids",
|
||||
)
|
||||
|
||||
|
||||
def get_id_reference_map():
|
||||
"""Return a dictionary of direct data-block references for every data-block in the blend file.
|
||||
|
||||
:return: Each datablock of the .blend file mapped to the set of IDs they directly reference.
|
||||
:rtype: dict[bpy.types.ID, set[bpy.types.ID]]
|
||||
"""
|
||||
inv_map = {}
|
||||
for key, values in bpy.data.user_map().items():
|
||||
for value in values:
|
||||
if value == key:
|
||||
# So an object is not considered to be referencing itself.
|
||||
continue
|
||||
inv_map.setdefault(value, set()).add(key)
|
||||
return inv_map
|
||||
|
||||
|
||||
def get_all_referenced_ids(id, ref_map):
|
||||
"""
|
||||
Return a set of IDs directly or indirectly referenced by id.
|
||||
|
||||
:param id: Datablock whose references we're interested in.
|
||||
:type id: bpy.types.ID
|
||||
:param ref_map: The global ID reference map, retrieved from get_id_reference_map()
|
||||
:type ref_map: dict[bpy.types.ID, set[bpy.types.ID]]
|
||||
:return: Set of datablocks referenced by `id`.
|
||||
:rtype: set[bpy.types.ID]
|
||||
"""
|
||||
|
||||
def recursive_helper(ref_map, id, referenced_ids, visited):
|
||||
if id in visited:
|
||||
# Avoid infinite recursion from circular references.
|
||||
return
|
||||
visited.add(id)
|
||||
for ref in ref_map.get(id, []):
|
||||
referenced_ids.add(ref)
|
||||
recursive_helper(ref_map=ref_map, id=ref, referenced_ids=referenced_ids, visited=visited)
|
||||
|
||||
referenced_ids = set()
|
||||
recursive_helper(ref_map=ref_map, id=id, referenced_ids=referenced_ids, visited=set())
|
||||
return referenced_ids
|
||||
196
blender-5.2.0/scripts/modules/bpy_extras/image_utils.py
Normal file
196
blender-5.2.0/scripts/modules/bpy_extras/image_utils.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"load_image",
|
||||
)
|
||||
|
||||
|
||||
# limited replacement for BPyImage.comprehensiveImageLoad
|
||||
def load_image(
|
||||
imagepath,
|
||||
dirname="",
|
||||
place_holder=False,
|
||||
recursive=False,
|
||||
ncase_cmp=True,
|
||||
convert_callback=None,
|
||||
verbose=False,
|
||||
relpath=None,
|
||||
check_existing=False,
|
||||
force_reload=False,
|
||||
):
|
||||
"""
|
||||
Return an image from the file path with options to search multiple paths
|
||||
and return a placeholder if it's not found.
|
||||
|
||||
:param imagepath: The image filename
|
||||
If a path precedes it, this will be searched as well.
|
||||
:type imagepath: str
|
||||
:param dirname: is the directory where the image may be located - any file at
|
||||
the end will be ignored.
|
||||
:type dirname: str
|
||||
:param place_holder: if True a new place holder image will be created.
|
||||
this is useful so later you can relink the image to its original data.
|
||||
:type place_holder: bool
|
||||
:param recursive: If True, directories will be recursively searched.
|
||||
Be careful with this if you have files in your root directory because
|
||||
it may take a long time.
|
||||
:type recursive: bool
|
||||
:param ncase_cmp: on non windows systems, find the correct case for the file.
|
||||
:type ncase_cmp: bool
|
||||
:param convert_callback: a function that takes an existing path and returns
|
||||
a new one. Use this when loading image formats blender may not support,
|
||||
the CONVERT_CALLBACK can take the path for a GIF (for example),
|
||||
convert it to a PNG and return the PNG's path.
|
||||
For formats blender can read, simply return the path that is given.
|
||||
:type convert_callback: Callable[[str], str] | None
|
||||
:param verbose: If True, print extra information when searching for the image.
|
||||
:type verbose: bool
|
||||
:param relpath: If not None, make the file relative to this path.
|
||||
:type relpath: str | None
|
||||
:param check_existing: If true,
|
||||
returns already loaded image data-block if possible
|
||||
(based on file path).
|
||||
:type check_existing: bool
|
||||
:param force_reload: If true,
|
||||
force reloading of image (only useful when ``check_existing``
|
||||
is also enabled).
|
||||
:type force_reload: bool
|
||||
:return: an image or None
|
||||
:rtype: :class:`bpy.types.Image` | None
|
||||
"""
|
||||
import os
|
||||
import bpy
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Utility Functions
|
||||
|
||||
def _image_load_placeholder(path):
|
||||
name = path
|
||||
if type(path) is str:
|
||||
name = name.encode("utf-8", "replace")
|
||||
name = name.decode("utf-8", "replace")
|
||||
name = os.path.basename(name)
|
||||
|
||||
image = bpy.data.images.new(name, 128, 128)
|
||||
# allow the path to be resolved later
|
||||
image.filepath = path
|
||||
image.source = 'FILE'
|
||||
return image
|
||||
|
||||
def _image_load(path):
|
||||
import bpy
|
||||
|
||||
if convert_callback:
|
||||
path = convert_callback(path)
|
||||
|
||||
# Ensure we're not relying on the 'CWD' to resolve the path.
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.abspath(path)
|
||||
|
||||
try:
|
||||
image = bpy.data.images.load(path, check_existing=check_existing)
|
||||
except RuntimeError:
|
||||
image = None
|
||||
|
||||
if verbose:
|
||||
if image:
|
||||
print(" image loaded '{:s}'".format(path))
|
||||
else:
|
||||
print(" image load failed '{:s}'".format(path))
|
||||
|
||||
# image path has been checked so the path could not be read for some
|
||||
# reason, so be sure to return a placeholder
|
||||
if place_holder and image is None:
|
||||
image = _image_load_placeholder(path)
|
||||
|
||||
if image:
|
||||
if force_reload:
|
||||
image.reload()
|
||||
if relpath is not None:
|
||||
# make relative
|
||||
from bpy.path import relpath as relpath_fn
|
||||
# can't always find the relative path
|
||||
# (between drive letters on windows)
|
||||
try:
|
||||
filepath_rel = relpath_fn(path, start=relpath)
|
||||
except ValueError:
|
||||
filepath_rel = None
|
||||
|
||||
if filepath_rel is not None:
|
||||
image.filepath_raw = filepath_rel
|
||||
|
||||
return image
|
||||
|
||||
def _recursive_search(paths, filename_check):
|
||||
for path in paths:
|
||||
for dirpath, _dirnames, filenames in os.walk(path):
|
||||
|
||||
# skip '.svn'
|
||||
if dirpath[0] in {".", b'.'}:
|
||||
continue
|
||||
|
||||
for filename in filenames:
|
||||
if filename_check(filename):
|
||||
yield os.path.join(dirpath, filename)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
imagepath = bpy.path.native_pathsep(imagepath)
|
||||
|
||||
if verbose:
|
||||
print("load_image('{:s}', '{:s}', ...)".format(imagepath, dirname))
|
||||
|
||||
if os.path.exists(imagepath):
|
||||
return _image_load(imagepath)
|
||||
|
||||
variants = [imagepath]
|
||||
|
||||
if dirname:
|
||||
variants += [
|
||||
os.path.join(dirname, imagepath),
|
||||
os.path.join(dirname, bpy.path.basename(imagepath)),
|
||||
]
|
||||
|
||||
for filepath_test in variants:
|
||||
if ncase_cmp:
|
||||
ncase_variants = (
|
||||
filepath_test,
|
||||
bpy.path.resolve_ncase(filepath_test),
|
||||
)
|
||||
else:
|
||||
ncase_variants = (filepath_test, )
|
||||
|
||||
for nfilepath in ncase_variants:
|
||||
if os.path.exists(nfilepath):
|
||||
return _image_load(nfilepath)
|
||||
|
||||
if recursive:
|
||||
search_paths = []
|
||||
|
||||
for dirpath_test in (os.path.dirname(imagepath), dirname):
|
||||
if os.path.exists(dirpath_test):
|
||||
search_paths.append(dirpath_test)
|
||||
search_paths[:] = bpy.path.reduce_dirs(search_paths)
|
||||
|
||||
imagepath_base = bpy.path.basename(imagepath)
|
||||
if ncase_cmp:
|
||||
imagepath_base = imagepath_base.lower()
|
||||
|
||||
def image_filter(fn):
|
||||
return (imagepath_base == fn.lower())
|
||||
else:
|
||||
def image_filter(fn):
|
||||
return (imagepath_base == fn)
|
||||
|
||||
nfilepath = next(_recursive_search(search_paths, image_filter), None)
|
||||
if nfilepath is not None:
|
||||
return _image_load(nfilepath)
|
||||
|
||||
# None of the paths exist so return placeholder
|
||||
if place_holder:
|
||||
return _image_load_placeholder(imagepath)
|
||||
|
||||
# TODO comprehensiveImageLoad also searched in bpy.config.textureDir
|
||||
return None
|
||||
725
blender-5.2.0/scripts/modules/bpy_extras/io_utils.py
Normal file
725
blender-5.2.0/scripts/modules/bpy_extras/io_utils.py
Normal file
@@ -0,0 +1,725 @@
|
||||
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"ExportHelper",
|
||||
"ImportHelper",
|
||||
"orientation_helper",
|
||||
"axis_conversion",
|
||||
"axis_conversion_ensure",
|
||||
"create_derived_objects",
|
||||
"poll_file_object_drop",
|
||||
"unpack_list",
|
||||
"unpack_face_list",
|
||||
"path_reference",
|
||||
"path_reference_copy",
|
||||
"path_reference_mode",
|
||||
"unique_name",
|
||||
)
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.app.translations import (
|
||||
contexts as i18n_contexts,
|
||||
pgettext_iface as iface_,
|
||||
pgettext_data as data_,
|
||||
)
|
||||
|
||||
|
||||
def _check_axis_conversion(op):
|
||||
if hasattr(op, "axis_forward") and hasattr(op, "axis_up"):
|
||||
return axis_conversion_ensure(
|
||||
op,
|
||||
"axis_forward",
|
||||
"axis_up",
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class ExportHelper:
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Filepath used for exporting the file",
|
||||
maxlen=1024,
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
check_existing: BoolProperty(
|
||||
name="Check Existing",
|
||||
description="Check and warn on overwriting existing files",
|
||||
default=True,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
# subclasses can override with decorator
|
||||
# True == use ext, False == no ext, None == do nothing.
|
||||
check_extension = True
|
||||
|
||||
def invoke(self, context, event):
|
||||
"""
|
||||
Invoke the file selector for exporting, setting a default filepath
|
||||
based on the current blend file name.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:param event: The window event.
|
||||
:type event: :class:`bpy.types.Event`
|
||||
:return: The operator return value.
|
||||
:rtype: set[str]
|
||||
"""
|
||||
del event
|
||||
import os
|
||||
if not self.filepath:
|
||||
blend_filepath = context.blend_data.filepath
|
||||
if not blend_filepath:
|
||||
blend_filepath = data_("Untitled")
|
||||
else:
|
||||
blend_filepath = os.path.splitext(blend_filepath)[0]
|
||||
|
||||
self.filepath = blend_filepath + self.filename_ext
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def check(self, context):
|
||||
"""
|
||||
Validate the filepath and axis conversion settings.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when a property was updated.
|
||||
:rtype: bool
|
||||
"""
|
||||
del context
|
||||
import os
|
||||
change_ext = False
|
||||
change_axis = _check_axis_conversion(self)
|
||||
|
||||
check_extension = self.check_extension
|
||||
|
||||
if check_extension is not None:
|
||||
filepath = self.filepath
|
||||
if os.path.basename(filepath):
|
||||
if check_extension:
|
||||
filepath = bpy.path.ensure_ext(
|
||||
os.path.splitext(filepath)[0],
|
||||
self.filename_ext,
|
||||
)
|
||||
if filepath != self.filepath:
|
||||
self.filepath = filepath
|
||||
change_ext = True
|
||||
|
||||
return (change_ext or change_axis)
|
||||
|
||||
|
||||
class ImportHelper:
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Filepath used for importing the file",
|
||||
maxlen=1024,
|
||||
subtype='FILE_PATH',
|
||||
options={'SKIP_PRESET', 'HIDDEN'}
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
"""
|
||||
Invoke the file selector for importing.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:param event: The window event.
|
||||
:type event: :class:`bpy.types.Event`
|
||||
:return: The operator return value.
|
||||
:rtype: set[str]
|
||||
"""
|
||||
del event
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke_popup(self, context, confirm_text=""):
|
||||
"""
|
||||
Invoke as a popup confirmation dialog when a filepath is already set,
|
||||
otherwise fall back to the file selector.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:param confirm_text: Label for the confirm button,
|
||||
defaults to the operator label.
|
||||
:type confirm_text: str
|
||||
:return: The operator return value.
|
||||
:rtype: set[str]
|
||||
"""
|
||||
if self.properties.is_property_set("filepath"):
|
||||
title = self.filepath
|
||||
if len(self.files) > 1:
|
||||
title = iface_("Import {:d} files").format(len(self.files))
|
||||
|
||||
if confirm_text:
|
||||
confirm_text = iface_(confirm_text)
|
||||
else:
|
||||
# Use the operator's bl_label, extracted with an "Operator" translation context.
|
||||
confirm_text = iface_(self.bl_label, i18n_contexts.operator_default)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self,
|
||||
confirm_text=confirm_text,
|
||||
title=title,
|
||||
translate=False,
|
||||
)
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def check(self, context):
|
||||
"""
|
||||
Validate axis conversion settings.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when a property was updated.
|
||||
:rtype: bool
|
||||
"""
|
||||
del context
|
||||
return _check_axis_conversion(self)
|
||||
|
||||
|
||||
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).
|
||||
|
||||
:param axis_forward: The default forward axis.
|
||||
:type axis_forward: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:param axis_up: The default up axis.
|
||||
:type axis_up: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:return: A class decorator.
|
||||
:rtype: Callable[[type], type]
|
||||
"""
|
||||
|
||||
def wrapper(cls):
|
||||
# Python 3.14+ (PEP 649): This workaround is no longer needed because annotations
|
||||
# are lazily evaluated. Accessing `cls.__annotations__` always returns a dict
|
||||
# specific to that class (never the parent's), so adding items is safe.
|
||||
import sys
|
||||
if sys.version_info < (3, 14):
|
||||
# Without this, we may end up adding those fields to some **parent** class'
|
||||
# `__annotations__` property (like the ImportHelper or ExportHelper ones)! See #58772.
|
||||
if "__annotations__" not in cls.__dict__:
|
||||
setattr(cls, "__annotations__", {})
|
||||
|
||||
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
|
||||
# use lookup table to convert between any axis
|
||||
_axis_convert_matrix = (
|
||||
((-1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
((-1.0, 0.0, 0.0), (0.0, 0.0, -1.0), (0.0, -1.0, 0.0)),
|
||||
((-1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 1.0, 0.0)),
|
||||
((-1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, -1.0)),
|
||||
((0.0, -1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 0.0, -1.0)),
|
||||
((0.0, 0.0, 1.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
|
||||
((0.0, 0.0, -1.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
|
||||
((0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
((0.0, -1.0, 0.0), (0.0, 0.0, 1.0), (-1.0, 0.0, 0.0)),
|
||||
((0.0, 0.0, -1.0), (0.0, -1.0, 0.0), (-1.0, 0.0, 0.0)),
|
||||
((0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0)),
|
||||
((0.0, 1.0, 0.0), (0.0, 0.0, -1.0), (-1.0, 0.0, 0.0)),
|
||||
((0.0, -1.0, 0.0), (0.0, 0.0, -1.0), (1.0, 0.0, 0.0)),
|
||||
((0.0, 0.0, 1.0), (0.0, -1.0, 0.0), (1.0, 0.0, 0.0)),
|
||||
((0.0, 0.0, -1.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)),
|
||||
((0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0)),
|
||||
((0.0, -1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
((0.0, 0.0, -1.0), (1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
|
||||
((0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
|
||||
((0.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, -1.0)),
|
||||
((1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, -1.0)),
|
||||
((1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, -1.0, 0.0)),
|
||||
((1.0, 0.0, 0.0), (0.0, 0.0, -1.0), (0.0, 1.0, 0.0)),
|
||||
)
|
||||
|
||||
# store args as a single int
|
||||
# (X Y Z -X -Y -Z) --> (0, 1, 2, 3, 4, 5)
|
||||
# each value is ((src_forward, src_up), (dst_forward, dst_up))
|
||||
# where all 4 values are or'd into a single value...
|
||||
# (i1<<0 | i1<<3 | i1<<6 | i1<<9)
|
||||
_axis_convert_lut = (
|
||||
{0x8C8, 0x4D0, 0x2E0, 0xAE8, 0x701, 0x511, 0x119, 0xB29, 0x682, 0x88A,
|
||||
0x09A, 0x2A2, 0x80B, 0x413, 0x223, 0xA2B, 0x644, 0x454, 0x05C, 0xA6C,
|
||||
0x745, 0x94D, 0x15D, 0x365},
|
||||
{0xAC8, 0x8D0, 0x4E0, 0x2E8, 0x741, 0x951, 0x159, 0x369, 0x702, 0xB0A,
|
||||
0x11A, 0x522, 0xA0B, 0x813, 0x423, 0x22B, 0x684, 0x894, 0x09C, 0x2AC,
|
||||
0x645, 0xA4D, 0x05D, 0x465},
|
||||
{0x4C8, 0x2D0, 0xAE0, 0x8E8, 0x681, 0x291, 0x099, 0x8A9, 0x642, 0x44A,
|
||||
0x05A, 0xA62, 0x40B, 0x213, 0xA23, 0x82B, 0x744, 0x354, 0x15C, 0x96C,
|
||||
0x705, 0x50D, 0x11D, 0xB25},
|
||||
{0x2C8, 0xAD0, 0x8E0, 0x4E8, 0x641, 0xA51, 0x059, 0x469, 0x742, 0x34A,
|
||||
0x15A, 0x962, 0x20B, 0xA13, 0x823, 0x42B, 0x704, 0xB14, 0x11C, 0x52C,
|
||||
0x685, 0x28D, 0x09D, 0x8A5},
|
||||
{0x708, 0xB10, 0x120, 0x528, 0x8C1, 0xAD1, 0x2D9, 0x4E9, 0x942, 0x74A,
|
||||
0x35A, 0x162, 0x64B, 0xA53, 0x063, 0x46B, 0x804, 0xA14, 0x21C, 0x42C,
|
||||
0x885, 0x68D, 0x29D, 0x0A5},
|
||||
{0xB08, 0x110, 0x520, 0x728, 0x941, 0x151, 0x359, 0x769, 0x802, 0xA0A,
|
||||
0x21A, 0x422, 0xA4B, 0x053, 0x463, 0x66B, 0x884, 0x094, 0x29C, 0x6AC,
|
||||
0x8C5, 0xACD, 0x2DD, 0x4E5},
|
||||
{0x508, 0x710, 0xB20, 0x128, 0x881, 0x691, 0x299, 0x0A9, 0x8C2, 0x4CA,
|
||||
0x2DA, 0xAE2, 0x44B, 0x653, 0xA63, 0x06B, 0x944, 0x754, 0x35C, 0x16C,
|
||||
0x805, 0x40D, 0x21D, 0xA25},
|
||||
{0x108, 0x510, 0x720, 0xB28, 0x801, 0x411, 0x219, 0xA29, 0x882, 0x08A,
|
||||
0x29A, 0x6A2, 0x04B, 0x453, 0x663, 0xA6B, 0x8C4, 0x4D4, 0x2DC, 0xAEC,
|
||||
0x945, 0x14D, 0x35D, 0x765},
|
||||
{0x748, 0x350, 0x160, 0x968, 0xAC1, 0x2D1, 0x4D9, 0x8E9, 0xA42, 0x64A,
|
||||
0x45A, 0x062, 0x68B, 0x293, 0x0A3, 0x8AB, 0xA04, 0x214, 0x41C, 0x82C,
|
||||
0xB05, 0x70D, 0x51D, 0x125},
|
||||
{0x948, 0x750, 0x360, 0x168, 0xB01, 0x711, 0x519, 0x129, 0xAC2, 0x8CA,
|
||||
0x4DA, 0x2E2, 0x88B, 0x693, 0x2A3, 0x0AB, 0xA44, 0x654, 0x45C, 0x06C,
|
||||
0xA05, 0x80D, 0x41D, 0x225},
|
||||
{0x348, 0x150, 0x960, 0x768, 0xA41, 0x051, 0x459, 0x669, 0xA02, 0x20A,
|
||||
0x41A, 0x822, 0x28B, 0x093, 0x8A3, 0x6AB, 0xB04, 0x114, 0x51C, 0x72C,
|
||||
0xAC5, 0x2CD, 0x4DD, 0x8E5},
|
||||
{0x148, 0x950, 0x760, 0x368, 0xA01, 0x811, 0x419, 0x229, 0xB02, 0x10A,
|
||||
0x51A, 0x722, 0x08B, 0x893, 0x6A3, 0x2AB, 0xAC4, 0x8D4, 0x4DC, 0x2EC,
|
||||
0xA45, 0x04D, 0x45D, 0x665},
|
||||
{0x688, 0x890, 0x0A0, 0x2A8, 0x4C1, 0x8D1, 0xAD9, 0x2E9, 0x502, 0x70A,
|
||||
0xB1A, 0x122, 0x74B, 0x953, 0x163, 0x36B, 0x404, 0x814, 0xA1C, 0x22C,
|
||||
0x445, 0x64D, 0xA5D, 0x065},
|
||||
{0x888, 0x090, 0x2A0, 0x6A8, 0x501, 0x111, 0xB19, 0x729, 0x402, 0x80A,
|
||||
0xA1A, 0x222, 0x94B, 0x153, 0x363, 0x76B, 0x444, 0x054, 0xA5C, 0x66C,
|
||||
0x4C5, 0x8CD, 0xADD, 0x2E5},
|
||||
{0x288, 0x690, 0x8A0, 0x0A8, 0x441, 0x651, 0xA59, 0x069, 0x4C2, 0x2CA,
|
||||
0xADA, 0x8E2, 0x34B, 0x753, 0x963, 0x16B, 0x504, 0x714, 0xB1C, 0x12C,
|
||||
0x405, 0x20D, 0xA1D, 0x825},
|
||||
{0x088, 0x290, 0x6A0, 0x8A8, 0x401, 0x211, 0xA19, 0x829, 0x442, 0x04A,
|
||||
0xA5A, 0x662, 0x14B, 0x353, 0x763, 0x96B, 0x4C4, 0x2D4, 0xADC, 0x8EC,
|
||||
0x505, 0x10D, 0xB1D, 0x725},
|
||||
{0x648, 0x450, 0x060, 0xA68, 0x2C1, 0x4D1, 0x8D9, 0xAE9, 0x282, 0x68A,
|
||||
0x89A, 0x0A2, 0x70B, 0x513, 0x123, 0xB2B, 0x204, 0x414, 0x81C, 0xA2C,
|
||||
0x345, 0x74D, 0x95D, 0x165},
|
||||
{0xA48, 0x650, 0x460, 0x068, 0x341, 0x751, 0x959, 0x169, 0x2C2, 0xACA,
|
||||
0x8DA, 0x4E2, 0xB0B, 0x713, 0x523, 0x12B, 0x284, 0x694, 0x89C, 0x0AC,
|
||||
0x205, 0xA0D, 0x81D, 0x425},
|
||||
{0x448, 0x050, 0xA60, 0x668, 0x281, 0x091, 0x899, 0x6A9, 0x202, 0x40A,
|
||||
0x81A, 0xA22, 0x50B, 0x113, 0xB23, 0x72B, 0x344, 0x154, 0x95C, 0x76C,
|
||||
0x2C5, 0x4CD, 0x8DD, 0xAE5},
|
||||
{0x048, 0xA50, 0x660, 0x468, 0x201, 0xA11, 0x819, 0x429, 0x342, 0x14A,
|
||||
0x95A, 0x762, 0x10B, 0xB13, 0x723, 0x52B, 0x2C4, 0xAD4, 0x8DC, 0x4EC,
|
||||
0x285, 0x08D, 0x89D, 0x6A5},
|
||||
{0x808, 0xA10, 0x220, 0x428, 0x101, 0xB11, 0x719, 0x529, 0x142, 0x94A,
|
||||
0x75A, 0x362, 0x8CB, 0xAD3, 0x2E3, 0x4EB, 0x044, 0xA54, 0x65C, 0x46C,
|
||||
0x085, 0x88D, 0x69D, 0x2A5},
|
||||
{0xA08, 0x210, 0x420, 0x828, 0x141, 0x351, 0x759, 0x969, 0x042, 0xA4A,
|
||||
0x65A, 0x462, 0xACB, 0x2D3, 0x4E3, 0x8EB, 0x084, 0x294, 0x69C, 0x8AC,
|
||||
0x105, 0xB0D, 0x71D, 0x525},
|
||||
{0x408, 0x810, 0xA20, 0x228, 0x081, 0x891, 0x699, 0x2A9, 0x102, 0x50A,
|
||||
0x71A, 0xB22, 0x4CB, 0x8D3, 0xAE3, 0x2EB, 0x144, 0x954, 0x75C, 0x36C,
|
||||
0x045, 0x44D, 0x65D, 0xA65},
|
||||
)
|
||||
|
||||
_axis_convert_num = {'X': 0, 'Y': 1, 'Z': 2, '-X': 3, '-Y': 4, '-Z': 5}
|
||||
|
||||
|
||||
def axis_conversion(from_forward='Y', from_up='Z', to_forward='Y', to_up='Z'):
|
||||
"""
|
||||
Each argument is an axis
|
||||
where the first 2 are a source and the second 2 are the target.
|
||||
|
||||
:param from_forward: Source forward axis.
|
||||
:type from_forward: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:param from_up: Source up axis.
|
||||
:type from_up: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:param to_forward: Target forward axis.
|
||||
:type to_forward: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:param to_up: Target up axis.
|
||||
:type to_up: Literal['X', 'Y', 'Z', '-X', '-Y', '-Z']
|
||||
:return: The conversion matrix.
|
||||
:rtype: :class:`mathutils.Matrix`
|
||||
"""
|
||||
from mathutils import Matrix
|
||||
from functools import reduce
|
||||
|
||||
if from_forward == to_forward and from_up == to_up:
|
||||
return Matrix().to_3x3()
|
||||
|
||||
if from_forward[-1] == from_up[-1] or to_forward[-1] == to_up[-1]:
|
||||
raise Exception("Invalid axis arguments passed, cannot use up/forward on the same axis")
|
||||
|
||||
value = reduce(
|
||||
int.__or__,
|
||||
(_axis_convert_num[a] << (i * 3) for i, a in enumerate((
|
||||
from_forward,
|
||||
from_up,
|
||||
to_forward,
|
||||
to_up,
|
||||
)))
|
||||
)
|
||||
|
||||
for i, axis_lut in enumerate(_axis_convert_lut):
|
||||
if value in axis_lut:
|
||||
return Matrix(_axis_convert_matrix[i])
|
||||
assert False, "unreachable"
|
||||
|
||||
|
||||
def axis_conversion_ensure(operator, forward_attr, up_attr):
|
||||
"""
|
||||
Function to ensure an operator has valid axis conversion settings, intended
|
||||
to be used from :class:`bpy.types.Operator.check`.
|
||||
|
||||
:param operator: the operator to access axis attributes from.
|
||||
:type operator: :class:`bpy.types.Operator`
|
||||
:param forward_attr: attribute storing the forward axis
|
||||
:type forward_attr: str
|
||||
:param up_attr: attribute storing the up axis
|
||||
:type up_attr: str
|
||||
:return: True if the value was modified.
|
||||
:rtype: bool
|
||||
"""
|
||||
def validate(axis_forward, axis_up):
|
||||
if axis_forward[-1] == axis_up[-1]:
|
||||
axis_up = axis_up[0:-1] + 'XYZ'[('XYZ'.index(axis_up[-1]) + 1) % 3]
|
||||
|
||||
return axis_forward, axis_up
|
||||
|
||||
axis = getattr(operator, forward_attr), getattr(operator, up_attr)
|
||||
axis_new = validate(*axis)
|
||||
|
||||
if axis != axis_new:
|
||||
setattr(operator, forward_attr, axis_new[0])
|
||||
setattr(operator, up_attr, axis_new[1])
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_derived_objects(depsgraph, objects):
|
||||
"""
|
||||
This function takes a sequence of objects, returning their instances.
|
||||
|
||||
:param depsgraph: The evaluated depsgraph.
|
||||
:type depsgraph: :class:`bpy.types.Depsgraph`
|
||||
:param objects: A sequence of objects.
|
||||
:type objects: Sequence[:class:`bpy.types.Object`]
|
||||
:return: A dictionary where each key is an object from ``objects``,
|
||||
values are lists of (object, matrix) tuples representing instances.
|
||||
:rtype: dict[:class:`bpy.types.Object`, list[tuple[:class:`bpy.types.Object`, :class:`mathutils.Matrix`]]]
|
||||
"""
|
||||
result = {}
|
||||
for ob in objects:
|
||||
ob_parent = ob.parent
|
||||
if ob_parent and ob_parent.instance_type in {'VERTS', 'FACES'}:
|
||||
continue
|
||||
result[ob] = [] if ob.is_instancer else [(ob, ob.matrix_world.copy())]
|
||||
|
||||
if result:
|
||||
for dup in depsgraph.object_instances:
|
||||
dup_parent = dup.parent
|
||||
if dup_parent is None:
|
||||
continue
|
||||
dup_parent_original = dup_parent.original
|
||||
if not dup_parent_original.is_instancer:
|
||||
# The instance has already been added (on assignment).
|
||||
continue
|
||||
instance_list = result.get(dup_parent_original)
|
||||
if instance_list is None:
|
||||
continue
|
||||
instance_list.append((dup.instance_object.original, dup.matrix_world.copy()))
|
||||
return result
|
||||
|
||||
|
||||
def unpack_list(list_of_tuples):
|
||||
"""
|
||||
Flatten a sequence of tuples into a single list.
|
||||
|
||||
:param list_of_tuples: A sequence of tuples to unpack.
|
||||
:type list_of_tuples: Sequence[tuple]
|
||||
:return: A flat list of all values.
|
||||
:rtype: list
|
||||
"""
|
||||
flat_list = []
|
||||
flat_list_extend = flat_list.extend # a tiny bit faster
|
||||
for t in list_of_tuples:
|
||||
flat_list_extend(t)
|
||||
return flat_list
|
||||
|
||||
|
||||
# same as above except that it adds 0 for triangle faces
|
||||
def unpack_face_list(list_of_tuples):
|
||||
"""
|
||||
Unpack a list of faces (triangles or quads) into a flat list,
|
||||
padding triangles with a zero to fit into groups of four.
|
||||
|
||||
:param list_of_tuples: A sequence of face index tuples (3 or 4 elements each).
|
||||
:type list_of_tuples: Sequence[tuple[int, ...]]
|
||||
:return: A flat list of face indices, padded with zeros.
|
||||
:rtype: list[int]
|
||||
"""
|
||||
# allocate the entire list
|
||||
flat_ls = [0] * (len(list_of_tuples) * 4)
|
||||
i = 0
|
||||
|
||||
for t in list_of_tuples:
|
||||
if len(t) == 3:
|
||||
if t[2] == 0:
|
||||
t = t[1], t[2], t[0]
|
||||
else: # assume quad
|
||||
if t[3] == 0 or t[2] == 0:
|
||||
t = t[2], t[3], t[0], t[1]
|
||||
|
||||
flat_ls[i:i + len(t)] = t
|
||||
i += 4
|
||||
return flat_ls
|
||||
|
||||
|
||||
def poll_file_object_drop(context):
|
||||
"""
|
||||
A default implementation for FileHandler poll_drop methods. Allows for both the 3D Viewport and
|
||||
the Outliner (in ViewLayer display mode) to be targets for file drag and drop.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: Whether the drop target is valid.
|
||||
:rtype: bool
|
||||
"""
|
||||
area = context.area
|
||||
if not area:
|
||||
return False
|
||||
is_v3d = area.type == 'VIEW_3D'
|
||||
is_outliner_view_layer = area.type == 'OUTLINER' and area.spaces.active.display_mode == 'VIEW_LAYER'
|
||||
return is_v3d or is_outliner_view_layer
|
||||
|
||||
|
||||
path_reference_mode = EnumProperty(
|
||||
name="Path Mode",
|
||||
description="Method used to reference paths",
|
||||
items=(
|
||||
('AUTO', "Auto", "Use relative paths with subdirectories only"),
|
||||
('ABSOLUTE', "Absolute", "Always write absolute paths"),
|
||||
('RELATIVE', "Relative", "Write relative paths where possible"),
|
||||
('MATCH', "Match", "Match absolute/relative "
|
||||
"setting with input path"),
|
||||
('STRIP', "Strip", "Filename only"),
|
||||
('COPY', "Copy", "Copy the file to the destination path "
|
||||
"(or subdirectory)"),
|
||||
),
|
||||
translation_context=i18n_contexts.editor_filebrowser,
|
||||
default='AUTO',
|
||||
)
|
||||
|
||||
|
||||
def path_reference(
|
||||
filepath,
|
||||
base_src,
|
||||
base_dst,
|
||||
mode='AUTO',
|
||||
copy_subdir="",
|
||||
copy_set=None,
|
||||
library=None,
|
||||
):
|
||||
"""
|
||||
Return a filepath relative to a destination directory, for use with
|
||||
exporters.
|
||||
|
||||
:param filepath: the file path to return,
|
||||
supporting blenders relative '//' prefix.
|
||||
:type filepath: str
|
||||
:param base_src: the directory the *filepath* is relative to
|
||||
(normally the blend file).
|
||||
:type base_src: str
|
||||
:param base_dst: the directory the *filepath* will be referenced from
|
||||
(normally the export path).
|
||||
:type base_dst: str
|
||||
:param mode: the method used to reference the path.
|
||||
:type mode: Literal['AUTO', 'ABSOLUTE', 'RELATIVE', 'MATCH', 'STRIP', 'COPY']
|
||||
:param copy_subdir: the subdirectory of *base_dst* to use when mode='COPY'.
|
||||
:type copy_subdir: str
|
||||
:param copy_set: collect from/to pairs when mode='COPY',
|
||||
pass to *path_reference_copy* when exporting is done.
|
||||
:type copy_set: set[tuple[str, str]] | None
|
||||
:param library: The library this path is relative to.
|
||||
:type library: :class:`bpy.types.Library` | None
|
||||
:return: the new filepath.
|
||||
:rtype: str
|
||||
"""
|
||||
import os
|
||||
is_relative = filepath.startswith("//")
|
||||
filepath_abs = bpy.path.abspath(filepath, start=base_src, library=library)
|
||||
filepath_abs = os.path.normpath(filepath_abs)
|
||||
|
||||
if mode in {'ABSOLUTE', 'RELATIVE', 'STRIP'}:
|
||||
pass
|
||||
elif mode == 'MATCH':
|
||||
mode = 'RELATIVE' if is_relative else 'ABSOLUTE'
|
||||
elif mode == 'AUTO':
|
||||
mode = (
|
||||
'RELATIVE' if bpy.path.is_subdir(filepath_abs, base_dst) else
|
||||
'ABSOLUTE'
|
||||
)
|
||||
elif mode == 'COPY':
|
||||
subdir_abs = os.path.normpath(base_dst)
|
||||
if copy_subdir:
|
||||
subdir_abs = os.path.join(subdir_abs, copy_subdir)
|
||||
|
||||
filepath_cpy = os.path.join(subdir_abs, os.path.basename(filepath_abs))
|
||||
|
||||
copy_set.add((filepath_abs, filepath_cpy))
|
||||
|
||||
filepath_abs = filepath_cpy
|
||||
mode = 'RELATIVE'
|
||||
else:
|
||||
raise Exception("invalid mode given {!r}".format(mode))
|
||||
|
||||
if mode == 'ABSOLUTE':
|
||||
return filepath_abs
|
||||
elif mode == 'RELATIVE':
|
||||
# can't always find the relative path
|
||||
# (between drive letters on windows)
|
||||
try:
|
||||
return os.path.relpath(filepath_abs, base_dst)
|
||||
except ValueError:
|
||||
return filepath_abs
|
||||
elif mode == 'STRIP':
|
||||
return os.path.basename(filepath_abs)
|
||||
|
||||
|
||||
def path_reference_copy(copy_set, report=print):
|
||||
"""
|
||||
Execute copying files of path_reference
|
||||
|
||||
:param copy_set: set of (from, to) pairs to copy.
|
||||
:type copy_set: set[tuple[str, str]]
|
||||
:param report: function used for reporting warnings, takes a string argument.
|
||||
:type report: Callable[[str], None]
|
||||
"""
|
||||
if not copy_set:
|
||||
return
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
for file_src, file_dst in copy_set:
|
||||
if not os.path.exists(file_src):
|
||||
report("missing {!r}, not copying".format(file_src))
|
||||
elif os.path.exists(file_dst) and os.path.samefile(file_src, file_dst):
|
||||
pass
|
||||
else:
|
||||
dir_to = os.path.dirname(file_dst)
|
||||
|
||||
try:
|
||||
os.makedirs(dir_to, exist_ok=True)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
shutil.copy(file_src, file_dst)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def unique_name(key, name, name_dict, name_max=-1, clean_func=None, sep="."):
|
||||
"""
|
||||
Helper function for storing unique names which may have special characters
|
||||
stripped and restricted to a maximum length.
|
||||
|
||||
:param key: Unique item this name belongs to, name_dict[key] will be reused
|
||||
when available.
|
||||
This can be the object, mesh, material, etc instance itself.
|
||||
Any hashable object associated with the *name*.
|
||||
:type key: Any
|
||||
:param name: The name used to create a unique value in *name_dict*.
|
||||
:type name: str
|
||||
:param name_dict: This is used to cache namespace to ensure no collisions
|
||||
occur, this should be an empty dict initially and only modified by this
|
||||
function.
|
||||
:type name_dict: dict[Any, str]
|
||||
:param name_max: Maximum length of the name. When ``-1`` the name is unlimited.
|
||||
:type name_max: int
|
||||
:param clean_func: Function to call on *name* before creating a unique value.
|
||||
:type clean_func: Callable[[str], str] | None
|
||||
:param sep: Separator to use when between the name and a number when a
|
||||
duplicate name is found.
|
||||
:type sep: str
|
||||
:return: A unique name.
|
||||
:rtype: str
|
||||
"""
|
||||
name_new = name_dict.get(key)
|
||||
if name_new is None:
|
||||
count = 1
|
||||
name_dict_values = name_dict.values()
|
||||
name_new = name_new_orig = (
|
||||
name if clean_func is None
|
||||
else clean_func(name)
|
||||
)
|
||||
|
||||
if name_max == -1:
|
||||
while name_new in name_dict_values:
|
||||
name_new = "{:s}{:s}{:03d}".format(
|
||||
name_new_orig,
|
||||
sep,
|
||||
count,
|
||||
)
|
||||
count += 1
|
||||
else:
|
||||
name_new = name_new[:name_max]
|
||||
while name_new in name_dict_values:
|
||||
count_str = "{:03d}".format(count)
|
||||
name_new = "{:.{:d}s}{:s}{:s}".format(
|
||||
name_new_orig,
|
||||
name_max - (len(count_str) + 1),
|
||||
sep,
|
||||
count_str,
|
||||
)
|
||||
count += 1
|
||||
|
||||
name_dict[key] = name_new
|
||||
|
||||
return name_new
|
||||
155
blender-5.2.0/scripts/modules/bpy_extras/keyconfig_utils.py
Normal file
155
blender-5.2.0/scripts/modules/bpy_extras/keyconfig_utils.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"addon_keymap_register",
|
||||
"addon_keymap_unregister",
|
||||
"keyconfig_test",
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Add-on helpers to properly (un)register their own keymaps.
|
||||
|
||||
def addon_keymap_register(keymap_data):
|
||||
"""
|
||||
Register a set of keymaps for addons using a list of keymaps.
|
||||
|
||||
See 'blender_default.py' for examples of the format this takes.
|
||||
|
||||
:param keymap_data: A list of keymap definitions to register.
|
||||
:type keymap_data: list[tuple[str, dict[str, Any], dict[str, Any]]]
|
||||
"""
|
||||
import bpy
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
from bl_keymap_utils.io import keymap_init_from_data
|
||||
|
||||
kconf = wm.keyconfigs.addon
|
||||
if not kconf:
|
||||
return # happens in background mode...
|
||||
for km_name, km_args, km_content in keymap_data:
|
||||
km_space_type = km_args["space_type"]
|
||||
km_region_type = km_args["region_type"]
|
||||
km_modal = km_args.get("modal", False)
|
||||
kmap = next(iter(
|
||||
k for k in kconf.keymaps
|
||||
if k.name == km_name and
|
||||
k.region_type == km_region_type and
|
||||
k.space_type == km_space_type and
|
||||
k.is_modal == km_modal
|
||||
), None)
|
||||
if kmap is None:
|
||||
kmap = kconf.keymaps.new(km_name, **km_args)
|
||||
keymap_init_from_data(kmap, km_content["items"], is_modal=km_modal)
|
||||
|
||||
|
||||
def addon_keymap_unregister(keymap_data):
|
||||
"""
|
||||
Unregister a set of keymaps for addons.
|
||||
|
||||
:param keymap_data: A list of keymap definitions to unregister.
|
||||
:type keymap_data: list[tuple[str, dict[str, Any], dict[str, Any]]]
|
||||
"""
|
||||
# NOTE: We must also clean up user keyconfig, else, if user has customized one of add-on's shortcut, this
|
||||
# customization remains in memory, and comes back when re-enabling the addon, causing a segfault... :/
|
||||
import bpy
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
kconfs = wm.keyconfigs
|
||||
for kconf in (kconfs.user, kconfs.addon):
|
||||
for km_name, km_args, km_content in keymap_data:
|
||||
km_space_type = km_args["space_type"]
|
||||
km_region_type = km_args["region_type"]
|
||||
km_modal = km_args.get("modal", False)
|
||||
kmaps = (
|
||||
k for k in kconf.keymaps
|
||||
if k.name == km_name and
|
||||
k.region_type == km_region_type and
|
||||
k.space_type == km_space_type and
|
||||
k.is_modal == km_modal
|
||||
)
|
||||
for kmap in kmaps:
|
||||
for kmi_idname, _, _ in km_content["items"]:
|
||||
for kmi in kmap.keymap_items:
|
||||
if kmi.idname == kmi_idname:
|
||||
kmap.keymap_items.remove(kmi)
|
||||
# NOTE: We won't remove addons keymaps themselves, other addons might also use them!
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utility Functions
|
||||
|
||||
def keyconfig_test(kc):
|
||||
"""
|
||||
Test a key configuration for duplicate key-map item assignments.
|
||||
|
||||
:param kc: The key configuration to test.
|
||||
:type kc: :class:`bpy.types.KeyConfig`
|
||||
:return: True if any duplicates were found.
|
||||
:rtype: bool
|
||||
"""
|
||||
from bl_keymap_utils.io import kmi_args_as_data
|
||||
|
||||
def _kmistr(kmi, is_modal):
|
||||
if is_modal:
|
||||
kmi_id = kmi.propvalue
|
||||
else:
|
||||
kmi_id = kmi.idname
|
||||
return "{:s}({:s})".format(kmi_id, kmi_args_as_data(kmi))
|
||||
|
||||
def testEntry(kc, entry, src=None, parent=None):
|
||||
result = False
|
||||
|
||||
idname, spaceid, regionid, children = entry
|
||||
|
||||
km = kc.keymaps.find(idname, space_type=spaceid, region_type=regionid)
|
||||
|
||||
if km:
|
||||
km = km.active()
|
||||
is_modal = km.is_modal
|
||||
|
||||
if src:
|
||||
for item in km.keymap_items:
|
||||
if src.compare(item):
|
||||
print("===========")
|
||||
print(parent.name, "[parent]")
|
||||
print(_kmistr(src, is_modal).strip())
|
||||
print(km.name, "[child]")
|
||||
print(_kmistr(item, is_modal).strip())
|
||||
result = True
|
||||
|
||||
for child in children:
|
||||
if testEntry(kc, child, src, parent):
|
||||
result = True
|
||||
else:
|
||||
for i, src in enumerate(km.keymap_items):
|
||||
for child in children:
|
||||
if testEntry(kc, child, src, km):
|
||||
result = True
|
||||
|
||||
for j in range(len(km.keymap_items) - i - 1):
|
||||
item = km.keymap_items[j + i + 1]
|
||||
if src.compare(item):
|
||||
print("===========")
|
||||
print(km.name, "[self conflict]")
|
||||
print(_kmistr(src, is_modal).strip())
|
||||
print(_kmistr(item, is_modal).strip())
|
||||
result = True
|
||||
|
||||
for child in children:
|
||||
if testEntry(kc, child):
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Function body
|
||||
|
||||
from bl_keymap_utils import keymap_hierarchy
|
||||
result = False
|
||||
for entry in keymap_hierarchy.generate():
|
||||
if testEntry(kc, entry):
|
||||
result = True
|
||||
return result
|
||||
475
blender-5.2.0/scripts/modules/bpy_extras/mesh_utils.py
Normal file
475
blender-5.2.0/scripts/modules/bpy_extras/mesh_utils.py
Normal file
@@ -0,0 +1,475 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"mesh_linked_uv_islands",
|
||||
"mesh_linked_triangles",
|
||||
"edge_face_count_dict",
|
||||
"edge_face_count",
|
||||
"edge_loops_from_edges",
|
||||
"ngon_tessellate",
|
||||
"triangle_random_points",
|
||||
)
|
||||
|
||||
|
||||
def mesh_linked_uv_islands(mesh):
|
||||
"""
|
||||
Returns lists of polygon indices connected by UV islands.
|
||||
|
||||
:param mesh: the mesh used to group with.
|
||||
:type mesh: :class:`bpy.types.Mesh`
|
||||
:return: list of lists containing polygon indices
|
||||
:rtype: list[list[int]]
|
||||
"""
|
||||
|
||||
if mesh.polygons and not mesh.uv_layers.active.data:
|
||||
# Currently, when in edit mode, UV Layer data will always be empty
|
||||
# when accessed though RNA. This may change in the future.
|
||||
raise ValueError(
|
||||
"UV Layers are not currently available from python in Edit Mode. "
|
||||
"Use bmesh and bpy_extras.bmesh_utils.bmesh_linked_uv_islands instead."
|
||||
)
|
||||
|
||||
uv_loops = [luv.uv[:] for luv in mesh.uv_layers.active.data]
|
||||
poly_loops = [poly.loop_indices for poly in mesh.polygons]
|
||||
luv_hash = {}
|
||||
luv_hash_get = luv_hash.get
|
||||
luv_hash_ls = [None] * len(uv_loops)
|
||||
for pi, poly_indices in enumerate(poly_loops):
|
||||
for li in poly_indices:
|
||||
uv = uv_loops[li]
|
||||
uv_hub = luv_hash_get(uv)
|
||||
if uv_hub is None:
|
||||
uv_hub = luv_hash[uv] = [pi]
|
||||
else:
|
||||
uv_hub.append(pi)
|
||||
luv_hash_ls[li] = uv_hub
|
||||
|
||||
poly_islands = []
|
||||
|
||||
# 0 = none, 1 = added, 2 = searched
|
||||
poly_tag = [0] * len(poly_loops)
|
||||
|
||||
while True:
|
||||
poly_index = -1
|
||||
for i in range(len(poly_loops)):
|
||||
if poly_tag[i] == 0:
|
||||
poly_index = i
|
||||
break
|
||||
|
||||
if poly_index != -1:
|
||||
island = [poly_index]
|
||||
poly_tag[poly_index] = 1
|
||||
poly_islands.append(island)
|
||||
else:
|
||||
break # we're done
|
||||
|
||||
added = True
|
||||
while added:
|
||||
added = False
|
||||
for poly_index in island[:]:
|
||||
if poly_tag[poly_index] == 1:
|
||||
for li in poly_loops[poly_index]:
|
||||
for poly_index_shared in luv_hash_ls[li]:
|
||||
if poly_tag[poly_index_shared] == 0:
|
||||
added = True
|
||||
poly_tag[poly_index_shared] = 1
|
||||
island.append(poly_index_shared)
|
||||
poly_tag[poly_index] = 2
|
||||
|
||||
return poly_islands
|
||||
|
||||
|
||||
def mesh_linked_triangles(mesh):
|
||||
"""
|
||||
Splits the mesh into connected triangles, use this for separating cubes from
|
||||
other mesh elements within 1 mesh data-block.
|
||||
|
||||
:param mesh: the mesh used to group with.
|
||||
:type mesh: :class:`bpy.types.Mesh`
|
||||
:return: Lists of lists containing triangles.
|
||||
:rtype: list[list[:class:`bpy.types.MeshLoopTriangle`]]
|
||||
"""
|
||||
|
||||
# Build vert face connectivity
|
||||
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 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 triangles iteratively
|
||||
ok = True
|
||||
while ok:
|
||||
ok = False
|
||||
|
||||
for t in mesh.loop_triangles:
|
||||
mapped_index = tri_mapping[t.index]
|
||||
mapped_group = tri_groups[mapped_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:
|
||||
ok = True
|
||||
|
||||
# Assign mapping to this group so they
|
||||
# all map to this group
|
||||
for grp_t in tri_groups[nxt_mapped_index]:
|
||||
tri_mapping[grp_t.index] = mapped_index
|
||||
|
||||
# Move triangles into this group
|
||||
mapped_group.extend(tri_groups[nxt_mapped_index])
|
||||
|
||||
# remove reference to the list
|
||||
tri_groups[nxt_mapped_index] = None
|
||||
|
||||
# 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):
|
||||
"""
|
||||
:param mesh: The mesh to count edges for.
|
||||
:type mesh: :class:`bpy.types.Mesh`
|
||||
:return: Dictionary of edge keys with their value set to the number of faces using each edge.
|
||||
:rtype: dict[tuple[int, int], int]
|
||||
"""
|
||||
|
||||
face_edge_count = {}
|
||||
loops = mesh.loops
|
||||
edges = mesh.edges
|
||||
for poly in mesh.polygons:
|
||||
for i in poly.loop_indices:
|
||||
key = edges[loops[i].edge_index].key
|
||||
try:
|
||||
face_edge_count[key] += 1
|
||||
except KeyError:
|
||||
face_edge_count[key] = 1
|
||||
|
||||
return face_edge_count
|
||||
|
||||
|
||||
def edge_face_count(mesh):
|
||||
"""
|
||||
:param mesh: The mesh to count edges for.
|
||||
:type mesh: :class:`bpy.types.Mesh`
|
||||
:return: list of face users for each item in mesh.edges.
|
||||
:rtype: list[int]
|
||||
"""
|
||||
edge_face_count = edge_face_count_dict(mesh)
|
||||
get = dict.get
|
||||
return [get(edge_face_count, ed.key, 0) for ed in mesh.edges]
|
||||
|
||||
|
||||
def edge_loops_from_edges(mesh, edges=None):
|
||||
"""
|
||||
Edge loops defined by edges.
|
||||
|
||||
Takes mesh.edges or a list of edges and returns the edge loops
|
||||
as a list of vertex indices.
|
||||
Closed loops have matching start and end values.
|
||||
|
||||
:param mesh: The mesh to extract edge loops from.
|
||||
:type mesh: :class:`bpy.types.Mesh`
|
||||
:param edges: Edges to use, or None to use all edges in the mesh.
|
||||
:type edges: list[:class:`bpy.types.MeshEdge`] | None
|
||||
:return: A list of edge loops, each a list of vertex indices.
|
||||
:rtype: list[list[int]]
|
||||
"""
|
||||
line_polys = []
|
||||
|
||||
# Get edges not used by a face
|
||||
if edges is None:
|
||||
edges = mesh.edges
|
||||
|
||||
if not hasattr(edges, "pop"):
|
||||
edges = edges[:]
|
||||
|
||||
while edges:
|
||||
current_edge = edges.pop()
|
||||
vert_end, vert_start = current_edge.vertices[:]
|
||||
line_poly = [vert_start, vert_end]
|
||||
|
||||
ok = True
|
||||
while ok:
|
||||
ok = False
|
||||
# for i, ed in enumerate(edges):
|
||||
i = len(edges)
|
||||
while i:
|
||||
i -= 1
|
||||
ed = edges[i]
|
||||
v1, v2 = ed.vertices
|
||||
if v1 == vert_end:
|
||||
line_poly.append(v2)
|
||||
vert_end = line_poly[-1]
|
||||
ok = 1
|
||||
del edges[i]
|
||||
# break
|
||||
elif v2 == vert_end:
|
||||
line_poly.append(v1)
|
||||
vert_end = line_poly[-1]
|
||||
ok = 1
|
||||
del edges[i]
|
||||
# break
|
||||
elif v1 == vert_start:
|
||||
line_poly.insert(0, v2)
|
||||
vert_start = line_poly[0]
|
||||
ok = 1
|
||||
del edges[i]
|
||||
# break
|
||||
elif v2 == vert_start:
|
||||
line_poly.insert(0, v1)
|
||||
vert_start = line_poly[0]
|
||||
ok = 1
|
||||
del edges[i]
|
||||
# break
|
||||
line_polys.append(line_poly)
|
||||
|
||||
return line_polys
|
||||
|
||||
|
||||
def ngon_tessellate(from_data, indices, fix_loops=True, debug_print=True):
|
||||
"""
|
||||
Takes a poly-line of indices (ngon) and returns a list of face
|
||||
index lists. Designed to be used for importers that need indices for an
|
||||
ngon to create from existing verts.
|
||||
|
||||
:param from_data: Either a mesh, or a list/tuple of 3D vectors.
|
||||
:type from_data: :class:`bpy.types.Mesh` | list[Sequence[float]] | tuple[Sequence[float]]
|
||||
:param indices: a list of indices to use.
|
||||
This list is the ordered closed poly-line to fill, and can be a subset of the data given.
|
||||
:type indices: list[int]
|
||||
:param fix_loops: If this is enabled poly-lines
|
||||
that use loops to make multiple
|
||||
poly-lines are dealt with correctly.
|
||||
:type fix_loops: bool
|
||||
:param debug_print: Print debug information to the console.
|
||||
:type debug_print: bool
|
||||
:return: Tessellated faces as a list of triangle index tuples.
|
||||
:rtype: list[tuple[int, int, int]]
|
||||
"""
|
||||
|
||||
from mathutils.geometry import tessellate_polygon
|
||||
from mathutils import Vector
|
||||
vector_to_tuple = Vector.to_tuple
|
||||
|
||||
if not indices:
|
||||
return []
|
||||
|
||||
def mlen(co):
|
||||
# Manhattan length of a vector, faster then length.
|
||||
return abs(co[0]) + abs(co[1]) + abs(co[2])
|
||||
|
||||
def vert_from_vector_with_extra_data(v, i):
|
||||
# Calculate data per-vector, for reuse.
|
||||
return v, vector_to_tuple(v, 6), i, mlen(v)
|
||||
|
||||
def ed_key_mlen(v1, v2):
|
||||
if v1[3] > v2[3]:
|
||||
return v2[1], v1[1]
|
||||
else:
|
||||
return v1[1], v2[1]
|
||||
|
||||
if not fix_loops:
|
||||
# Normal single concave loop filling.
|
||||
|
||||
if type(from_data) in {tuple, list}:
|
||||
verts = [Vector(from_data[i]) for ii, i in enumerate(indices)]
|
||||
else:
|
||||
verts = [from_data.vertices[i].co for ii, i in enumerate(indices)]
|
||||
|
||||
# same as reversed(range(1, len(verts))):
|
||||
for i in range(len(verts) - 1, 0, -1):
|
||||
if verts[i][1] == verts[i - 1][0]:
|
||||
verts.pop(i - 1)
|
||||
|
||||
fill = tessellate_polygon([verts])
|
||||
|
||||
else:
|
||||
# Separate this loop into multiple loops be finding edges that are
|
||||
# used twice. This is used by Light-Wave LWO files a lot.
|
||||
|
||||
if type(from_data) in {tuple, list}:
|
||||
verts = [
|
||||
vert_from_vector_with_extra_data(Vector(from_data[i]), ii)
|
||||
for ii, i in enumerate(indices)
|
||||
]
|
||||
else:
|
||||
verts = [
|
||||
vert_from_vector_with_extra_data(from_data.vertices[i].co, ii)
|
||||
for ii, i in enumerate(indices)
|
||||
]
|
||||
|
||||
edges = [(i, i - 1) for i in range(len(verts))]
|
||||
if edges:
|
||||
edges[0] = (0, len(verts) - 1)
|
||||
|
||||
if not verts:
|
||||
return []
|
||||
|
||||
edges_used = set()
|
||||
edges_doubles = set()
|
||||
# We need to check if any edges are used twice location based.
|
||||
for ed in edges:
|
||||
edkey = ed_key_mlen(verts[ed[0]], verts[ed[1]])
|
||||
if edkey in edges_used:
|
||||
edges_doubles.add(edkey)
|
||||
else:
|
||||
edges_used.add(edkey)
|
||||
|
||||
# Store a list of unconnected loop segments split by double edges.
|
||||
# will join later
|
||||
loop_segments = []
|
||||
|
||||
v_prev = verts[0]
|
||||
context_loop = [v_prev]
|
||||
loop_segments = [context_loop]
|
||||
|
||||
for v in verts:
|
||||
if v != v_prev:
|
||||
# Are we crossing an edge we removed?
|
||||
if ed_key_mlen(v, v_prev) in edges_doubles:
|
||||
context_loop = [v]
|
||||
loop_segments.append(context_loop)
|
||||
else:
|
||||
if context_loop and context_loop[-1][1] == v[1]:
|
||||
pass
|
||||
else:
|
||||
context_loop.append(v)
|
||||
|
||||
v_prev = v
|
||||
# Now join loop segments
|
||||
|
||||
def join_seg(s1, s2):
|
||||
if s2[-1][1] == s1[0][1]:
|
||||
s1, s2 = s2, s1
|
||||
elif s1[-1][1] == s2[0][1]:
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
|
||||
# If were still here s1 and s2 are 2 segments in the same poly-line.
|
||||
s1.pop() # remove the last vert from s1
|
||||
s1.extend(s2) # add segment 2 to segment 1
|
||||
|
||||
if s1[0][1] == s1[-1][1]: # remove endpoints double
|
||||
s1.pop()
|
||||
|
||||
del s2[:] # Empty this segment s2 so we don't use it again.
|
||||
return True
|
||||
|
||||
joining_segments = True
|
||||
while joining_segments:
|
||||
joining_segments = False
|
||||
segcount = len(loop_segments)
|
||||
|
||||
for j in range(segcount - 1, -1, -1): # reversed(range(segcount)):
|
||||
seg_j = loop_segments[j]
|
||||
if seg_j:
|
||||
for k in range(j - 1, -1, -1): # reversed(range(j)):
|
||||
if not seg_j:
|
||||
break
|
||||
seg_k = loop_segments[k]
|
||||
|
||||
if seg_k and join_seg(seg_j, seg_k):
|
||||
joining_segments = True
|
||||
|
||||
loop_list = loop_segments
|
||||
|
||||
for verts in loop_list:
|
||||
while verts and verts[0][1] == verts[-1][1]:
|
||||
verts.pop()
|
||||
|
||||
loop_list = [verts for verts in loop_list if len(verts) > 2]
|
||||
# DONE DEALING WITH LOOP FIXING
|
||||
|
||||
# vert mapping
|
||||
vert_map = [None] * len(indices)
|
||||
ii = 0
|
||||
for verts in loop_list:
|
||||
if len(verts) > 2:
|
||||
for i, vert in enumerate(verts):
|
||||
vert_map[i + ii] = vert[2]
|
||||
ii += len(verts)
|
||||
|
||||
fill = tessellate_polygon([[v[0] for v in loop] for loop in loop_list])
|
||||
# draw_loops(loop_list)
|
||||
# raise Exception("done loop")
|
||||
# map to original indices
|
||||
fill = [[vert_map[i] for i in f] for f in fill]
|
||||
|
||||
if not fill:
|
||||
if debug_print:
|
||||
print('Warning Cannot scan-fill, fallback on a triangle fan.')
|
||||
fill = [[0, i - 1, i] for i in range(2, len(indices))]
|
||||
else:
|
||||
# Use real scan-fill.
|
||||
# See if its flipped the wrong way.
|
||||
flip = None
|
||||
for fi in fill:
|
||||
if flip is not None:
|
||||
break
|
||||
for i, vi in enumerate(fi):
|
||||
if vi == 0 and fi[i - 1] == 1:
|
||||
flip = False
|
||||
break
|
||||
elif vi == 1 and fi[i - 1] == 0:
|
||||
flip = True
|
||||
break
|
||||
|
||||
if not flip:
|
||||
for i, fi in enumerate(fill):
|
||||
fill[i] = tuple(reversed(fi))
|
||||
|
||||
return fill
|
||||
|
||||
|
||||
def triangle_random_points(num_points, loop_triangles):
|
||||
"""
|
||||
Generates a list of random points over mesh loop triangles.
|
||||
|
||||
:param num_points: The number of random points to generate on each triangle.
|
||||
:type num_points: int
|
||||
:param loop_triangles: Sequence of the triangles to generate points on.
|
||||
:type loop_triangles: Sequence[:class:`bpy.types.MeshLoopTriangle`]
|
||||
:return: List of random points over all triangles.
|
||||
:rtype: list[:class:`mathutils.Vector`]
|
||||
"""
|
||||
|
||||
from random import random
|
||||
|
||||
# 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):
|
||||
u1 = random()
|
||||
u2 = random()
|
||||
u_tot = u1 + u2
|
||||
|
||||
if u_tot > 1:
|
||||
u1 = 1.0 - u1
|
||||
u2 = 1.0 - u2
|
||||
|
||||
side1 = tv[1] - tv[0]
|
||||
side2 = tv[2] - tv[0]
|
||||
|
||||
p = tv[0] + u1 * side1 + u2 * side2
|
||||
|
||||
sampled_points[num_points * i + k] = p
|
||||
|
||||
return sampled_points
|
||||
833
blender-5.2.0/scripts/modules/bpy_extras/node_shader_utils.py
Normal file
833
blender-5.2.0/scripts/modules/bpy_extras/node_shader_utils.py
Normal file
@@ -0,0 +1,833 @@
|
||||
# SPDX-FileCopyrightText: 2018-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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]))
|
||||
|
||||
|
||||
# All clamping value shall follow Blender's defined min/max (check relevant node definition .c file).
|
||||
def values_clamp(val, minv, maxv):
|
||||
if hasattr(val, "__iter__"):
|
||||
return tuple(max(minv, min(maxv, v)) for v in val)
|
||||
else:
|
||||
return max(minv, min(maxv, val))
|
||||
|
||||
# TODO: Consider moving node_input_value_set/node_input_value_get into a common utility module if
|
||||
# more usage merits doing so. If that is done, abstract out the validity check and make it usable
|
||||
# for node outputs as well. See PR #119354 for details.
|
||||
|
||||
|
||||
def node_input_value_set(node, input, value):
|
||||
if node is None or input not in node.inputs:
|
||||
return
|
||||
|
||||
node.inputs[input].default_value = value
|
||||
|
||||
|
||||
def node_input_value_get(node, input, default_value=None):
|
||||
if node is None or input not in node.inputs:
|
||||
return default_value
|
||||
|
||||
return node.inputs[input].default_value
|
||||
|
||||
|
||||
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):
|
||||
self.is_readonly = is_readonly
|
||||
self.material = material
|
||||
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 node_texcoords_get(self):
|
||||
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):
|
||||
super(PrincipledBSDFWrapper, self).__init__(material, is_readonly)
|
||||
|
||||
def update(self):
|
||||
super(PrincipledBSDFWrapper, self).update()
|
||||
|
||||
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 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"])
|
||||
self._node_normalmap = node_normalmap
|
||||
return self._node_normalmap
|
||||
|
||||
node_normalmap = property(node_normalmap_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Base Color.
|
||||
|
||||
def base_color_get(self):
|
||||
if 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):
|
||||
color = values_clamp(color, 0.0, 1.0)
|
||||
color = rgb_to_rgba(color)
|
||||
self.material.diffuse_color = color
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Base Color"].default_value = color
|
||||
|
||||
base_color = property(base_color_get, base_color_set)
|
||||
|
||||
def base_color_texture_get(self):
|
||||
if 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 self.node_principled_bsdf is None:
|
||||
return self.material.specular_intensity
|
||||
return self.node_principled_bsdf.inputs["Specular IOR Level"].default_value
|
||||
|
||||
@_set_check
|
||||
def specular_set(self, value):
|
||||
value = values_clamp(value, 0.0, 1.0)
|
||||
self.material.specular_intensity = value
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Specular IOR Level"].default_value = value
|
||||
|
||||
specular = property(specular_get, specular_set)
|
||||
|
||||
# Will only be used as gray-scale one...
|
||||
def specular_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Specular IOR Level"],
|
||||
grid_row_diff=0,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
specular_texture = property(specular_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Specular Tint.
|
||||
|
||||
def specular_tint_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return Color((0.0, 0.0, 0.0))
|
||||
return rgba_to_rgb(self.node_principled_bsdf.inputs["Specular Tint"].default_value)
|
||||
|
||||
@_set_check
|
||||
def specular_tint_set(self, color):
|
||||
color = values_clamp(color, 0.0, 1.0)
|
||||
color = rgb_to_rgba(color)
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Specular Tint"].default_value = color
|
||||
|
||||
specular_tint = property(specular_tint_get, specular_tint_set)
|
||||
|
||||
def specular_tint_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Specular Tint"],
|
||||
grid_row_diff=0,
|
||||
)
|
||||
|
||||
specular_tint_texture = property(specular_tint_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Roughness (also sort of inverse of specular hardness...).
|
||||
|
||||
def roughness_get(self):
|
||||
if 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):
|
||||
value = values_clamp(value, 0.0, 1.0)
|
||||
self.material.roughness = value
|
||||
if 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 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,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
roughness_texture = property(roughness_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Metallic (a.k.a reflection, mirror).
|
||||
|
||||
def metallic_get(self):
|
||||
if 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):
|
||||
value = values_clamp(value, 0.0, 1.0)
|
||||
self.material.metallic = value
|
||||
if 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 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,
|
||||
colorspace_name="Non-Color",
|
||||
)
|
||||
|
||||
metallic_texture = property(metallic_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Transparency settings.
|
||||
|
||||
def ior_get(self):
|
||||
if 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):
|
||||
value = values_clamp(value, 0.0, 1000.0)
|
||||
if 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 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,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
ior_texture = property(ior_texture_get)
|
||||
|
||||
def transmission_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return 0.0
|
||||
return self.node_principled_bsdf.inputs["Transmission Weight"].default_value
|
||||
|
||||
@_set_check
|
||||
def transmission_set(self, value):
|
||||
value = values_clamp(value, 0.0, 1.0)
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Transmission Weight"].default_value = value
|
||||
|
||||
transmission = property(transmission_get, transmission_set)
|
||||
|
||||
# Will only be used as gray-scale one...
|
||||
def transmission_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Transmission Weight"],
|
||||
grid_row_diff=-1,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
transmission_texture = property(transmission_texture_get)
|
||||
|
||||
def alpha_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return 1.0
|
||||
return self.node_principled_bsdf.inputs["Alpha"].default_value
|
||||
|
||||
@_set_check
|
||||
def alpha_set(self, value):
|
||||
value = values_clamp(value, 0.0, 1.0)
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Alpha"].default_value = value
|
||||
|
||||
alpha = property(alpha_get, alpha_set)
|
||||
|
||||
# Will only be used as gray-scale one...
|
||||
def alpha_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Alpha"],
|
||||
use_alpha=True,
|
||||
grid_row_diff=-1,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
alpha_texture = property(alpha_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Emission color.
|
||||
|
||||
def emission_color_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return Color((0.0, 0.0, 0.0))
|
||||
return rgba_to_rgb(self.node_principled_bsdf.inputs["Emission Color"].default_value)
|
||||
|
||||
@_set_check
|
||||
def emission_color_set(self, color):
|
||||
if self.node_principled_bsdf is not None:
|
||||
color = values_clamp(color, 0.0, 1000000.0)
|
||||
color = rgb_to_rgba(color)
|
||||
self.node_principled_bsdf.inputs["Emission Color"].default_value = color
|
||||
|
||||
emission_color = property(emission_color_get, emission_color_set)
|
||||
|
||||
def emission_color_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Emission Color"],
|
||||
grid_row_diff=1,
|
||||
)
|
||||
|
||||
emission_color_texture = property(emission_color_texture_get)
|
||||
|
||||
def emission_strength_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return 1.0
|
||||
return self.node_principled_bsdf.inputs["Emission Strength"].default_value
|
||||
|
||||
@_set_check
|
||||
def emission_strength_set(self, value):
|
||||
value = values_clamp(value, 0.0, 1000000.0)
|
||||
if self.node_principled_bsdf is not None:
|
||||
self.node_principled_bsdf.inputs["Emission Strength"].default_value = value
|
||||
|
||||
emission_strength = property(emission_strength_get, emission_strength_set)
|
||||
|
||||
def emission_strength_texture_get(self):
|
||||
if self.node_principled_bsdf is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_principled_bsdf,
|
||||
self.node_principled_bsdf.inputs["Emission Strength"],
|
||||
grid_row_diff=-1,
|
||||
colorspace_name='Non-Color',
|
||||
)
|
||||
|
||||
emission_strength_texture = property(emission_strength_texture_get)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Normal map.
|
||||
|
||||
def normalmap_strength_get(self):
|
||||
if self.node_normalmap is None:
|
||||
return 0.0
|
||||
return self.node_normalmap.inputs["Strength"].default_value
|
||||
|
||||
@_set_check
|
||||
def normalmap_strength_set(self, value):
|
||||
value = values_clamp(value, 0.0, 10.0)
|
||||
if 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 self.node_normalmap is None:
|
||||
return None
|
||||
return ShaderImageTextureWrapper(
|
||||
self, self.node_normalmap,
|
||||
self.node_normalmap.inputs["Color"],
|
||||
grid_row_diff=-2,
|
||||
colorspace_is_data=True,
|
||||
)
|
||||
|
||||
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",
|
||||
"colorspace_is_data",
|
||||
"colorspace_name",
|
||||
*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, colorspace_is_data=..., colorspace_name=...,
|
||||
):
|
||||
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.colorspace_is_data = colorspace_is_data
|
||||
self.colorspace_name = colorspace_name
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
if self.use_alpha:
|
||||
self.owner_shader.material.surface_render_method = 'DITHERED'
|
||||
self.owner_shader.material.use_transparency_overlap = False
|
||||
|
||||
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):
|
||||
if self.colorspace_is_data is not ...:
|
||||
if image.colorspace_settings.is_data != self.colorspace_is_data and image.users >= 1:
|
||||
image = image.copy()
|
||||
image.colorspace_settings.is_data = self.colorspace_is_data
|
||||
if self.colorspace_name is not ...:
|
||||
if image.colorspace_settings.name != self.colorspace_name and image.users >= 1:
|
||||
image = image.copy()
|
||||
image.colorspace_settings.name = self.colorspace_name
|
||||
if self.use_alpha:
|
||||
# Try to be smart, and only use image's alpha output if image actually has alpha data.
|
||||
tree = self.owner_shader.material.node_tree
|
||||
if image.channels < 4 or image.depth in {24, 8}:
|
||||
tree.links.new(self.node_image.outputs["Color"], self.socket_dst)
|
||||
else:
|
||||
tree.links.new(self.node_image.outputs["Alpha"], self.socket_dst)
|
||||
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 node_input_value_get(self.node_mapping, "Location", Vector((0.0, 0.0, 0.0)))
|
||||
|
||||
@_set_check
|
||||
def translation_set(self, translation):
|
||||
node_input_value_set(self.node_mapping, "Location", translation)
|
||||
|
||||
translation = property(translation_get, translation_set)
|
||||
|
||||
def rotation_get(self):
|
||||
if self.node_mapping is None:
|
||||
return Vector((0.0, 0.0, 0.0))
|
||||
return self.node_mapping.inputs["Rotation"].default_value
|
||||
|
||||
@_set_check
|
||||
def rotation_set(self, rotation):
|
||||
self.node_mapping.inputs["Rotation"].default_value = rotation
|
||||
|
||||
rotation = property(rotation_get, rotation_set)
|
||||
|
||||
def scale_get(self):
|
||||
if self.node_mapping is None:
|
||||
return Vector((1.0, 1.0, 1.0))
|
||||
return self.node_mapping.inputs["Scale"].default_value
|
||||
|
||||
@_set_check
|
||||
def scale_set(self, scale):
|
||||
self.node_mapping.inputs["Scale"].default_value = scale
|
||||
|
||||
scale = property(scale_get, scale_set)
|
||||
111
blender-5.2.0/scripts/modules/bpy_extras/node_utils.py
Normal file
111
blender-5.2.0/scripts/modules/bpy_extras/node_utils.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"connect_sockets",
|
||||
"find_base_socket_type",
|
||||
"find_node_input",
|
||||
)
|
||||
|
||||
|
||||
def find_base_socket_type(socket):
|
||||
"""
|
||||
Find the base class of the socket.
|
||||
|
||||
Sockets can have a subtype such as NodeSocketFloatFactor,
|
||||
but only the base type is allowed, e.g. NodeSocketFloat
|
||||
|
||||
:param socket: The socket to find the base type for.
|
||||
:type socket: :class:`bpy.types.NodeSocket`
|
||||
:return: The base socket type identifier.
|
||||
:rtype: str
|
||||
"""
|
||||
if socket.type == 'CUSTOM':
|
||||
# Custom socket types are used directly
|
||||
return socket.bl_idname
|
||||
if socket.type == 'VALUE':
|
||||
return 'NodeSocketFloat'
|
||||
if socket.type == 'INT':
|
||||
return 'NodeSocketInt'
|
||||
if socket.type == 'BOOLEAN':
|
||||
return 'NodeSocketBool'
|
||||
if socket.type == 'VECTOR':
|
||||
return 'NodeSocketVector'
|
||||
if socket.type == 'ROTATION':
|
||||
return 'NodeSocketRotation'
|
||||
if socket.type == 'STRING':
|
||||
return 'NodeSocketString'
|
||||
if socket.type == 'RGBA':
|
||||
return 'NodeSocketColor'
|
||||
if socket.type == 'SHADER':
|
||||
return 'NodeSocketShader'
|
||||
if socket.type == 'OBJECT':
|
||||
return 'NodeSocketObject'
|
||||
if socket.type == 'IMAGE':
|
||||
return 'NodeSocketImage'
|
||||
if socket.type == 'GEOMETRY':
|
||||
return 'NodeSocketGeometry'
|
||||
if socket.type == 'COLLECTION':
|
||||
return 'NodeSocketCollection'
|
||||
if socket.type == 'TEXTURE':
|
||||
return 'NodeSocketTexture'
|
||||
if socket.type == 'MATERIAL':
|
||||
return 'NodeSocketMaterial'
|
||||
|
||||
|
||||
def connect_sockets(input, output):
|
||||
"""
|
||||
Connect sockets in a node tree.
|
||||
|
||||
This is useful because the links created through the normal Python API are
|
||||
invalid when one of the sockets is a virtual socket (grayed out sockets in
|
||||
Group Input and Group Output nodes).
|
||||
|
||||
It replaces node_tree.links.new(input, output)
|
||||
|
||||
:param input: The input socket.
|
||||
:type input: :class:`bpy.types.NodeSocket`
|
||||
:param output: The output socket.
|
||||
:type output: :class:`bpy.types.NodeSocket`
|
||||
:return: The created link, or ``None`` when the sockets cannot be connected.
|
||||
:rtype: :class:`bpy.types.NodeLink` | None
|
||||
"""
|
||||
import bpy
|
||||
|
||||
# Swap sockets if they are not passed in the proper order
|
||||
if input.is_output and not output.is_output:
|
||||
input, output = output, input
|
||||
|
||||
input_node = output.node
|
||||
output_node = input.node
|
||||
|
||||
if input_node.id_data is not output_node.id_data:
|
||||
print("Sockets do not belong to the same node tree")
|
||||
return
|
||||
|
||||
if type(input) == type(output) == bpy.types.NodeSocketVirtual:
|
||||
print("Cannot connect two virtual sockets together")
|
||||
return
|
||||
|
||||
return input_node.id_data.links.new(input, output, handle_dynamic_sockets=True)
|
||||
|
||||
|
||||
def find_node_input(node, name):
|
||||
"""
|
||||
Find a node input socket by name.
|
||||
|
||||
Note that names are not unique, returns the first match.
|
||||
|
||||
:param node: The node to search.
|
||||
:type node: :class:`bpy.types.Node`
|
||||
:param name: The name of the input socket.
|
||||
:type name: str
|
||||
:return: The input socket or None if not found.
|
||||
:rtype: :class:`bpy.types.NodeSocket` | None
|
||||
"""
|
||||
for input in node.inputs:
|
||||
if input.name == name:
|
||||
return input
|
||||
|
||||
return None
|
||||
319
blender-5.2.0/scripts/modules/bpy_extras/object_utils.py
Normal file
319
blender-5.2.0/scripts/modules/bpy_extras/object_utils.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"add_object_align_init",
|
||||
"object_data_add",
|
||||
"AddObjectHelper",
|
||||
"object_add_grid_scale",
|
||||
"object_add_grid_scale_apply_operator",
|
||||
"world_to_camera_view",
|
||||
"object_report_if_active_shape_key_is_locked",
|
||||
)
|
||||
|
||||
|
||||
import bpy
|
||||
|
||||
from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
def add_object_align_init(context, operator):
|
||||
"""
|
||||
Return a matrix using the operator settings and view context.
|
||||
|
||||
:param context: The context to use.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:param operator: The operator, checked for location and rotation properties.
|
||||
:type operator: :class:`bpy.types.Operator` | None
|
||||
:return: the matrix from the context and settings.
|
||||
:rtype: :class:`mathutils.Matrix`
|
||||
"""
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
properties = operator.properties if operator is not None else None
|
||||
|
||||
space_data = context.space_data
|
||||
if space_data and space_data.type != 'VIEW_3D':
|
||||
space_data = None
|
||||
|
||||
# location
|
||||
if operator and properties.is_property_set("location"):
|
||||
location = Matrix.Translation(Vector(properties.location))
|
||||
else:
|
||||
location = Matrix.Translation(context.scene.cursor.location)
|
||||
|
||||
if operator:
|
||||
properties.location = location.to_translation()
|
||||
|
||||
# rotation
|
||||
add_align_preference = context.preferences.edit.object_align
|
||||
if operator:
|
||||
if not properties.is_property_set("rotation"):
|
||||
# So one of "align" and "rotation" will be set
|
||||
properties.align = add_align_preference
|
||||
|
||||
if properties.align == 'WORLD':
|
||||
rotation = properties.rotation.to_matrix().to_4x4()
|
||||
elif properties.align == 'VIEW':
|
||||
rotation = space_data.region_3d.view_matrix.to_3x3().inverted()
|
||||
rotation.resize_4x4()
|
||||
properties.rotation = rotation.to_euler()
|
||||
elif properties.align == 'CURSOR':
|
||||
rotation = context.scene.cursor.matrix
|
||||
rotation.col[3][0:3] = 0.0, 0.0, 0.0
|
||||
properties.rotation = rotation.to_euler()
|
||||
else:
|
||||
rotation = properties.rotation.to_matrix().to_4x4()
|
||||
else:
|
||||
if (add_align_preference == 'VIEW') and space_data:
|
||||
rotation = space_data.region_3d.view_matrix.to_3x3().inverted()
|
||||
rotation.resize_4x4()
|
||||
elif add_align_preference == 'CURSOR':
|
||||
rotation = context.scene.cursor.rotation_euler.to_matrix().to_4x4()
|
||||
else:
|
||||
rotation = Matrix()
|
||||
|
||||
return location @ rotation
|
||||
|
||||
|
||||
def object_data_add(context, obdata, operator=None, name=None):
|
||||
"""
|
||||
Add an object using the view context and preference to initialize the
|
||||
location, rotation and layer.
|
||||
|
||||
:param context: The context to use.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:param obdata: Valid object data to be used for the new object or None.
|
||||
:type obdata: :class:`bpy.types.ID` | None
|
||||
:param operator: The operator, checked for location and rotation properties.
|
||||
:type operator: :class:`bpy.types.Operator` | None
|
||||
:param name: Optional name
|
||||
:type name: str | None
|
||||
:return: the newly created object in the scene.
|
||||
:rtype: :class:`bpy.types.Object`
|
||||
"""
|
||||
layer = context.view_layer
|
||||
layer_collection = context.layer_collection or layer.active_layer_collection
|
||||
scene_collection = layer_collection.collection
|
||||
|
||||
for ob in layer.objects:
|
||||
ob.select_set(False)
|
||||
|
||||
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)
|
||||
scene_collection.objects.link(obj_new)
|
||||
obj_new.select_set(True)
|
||||
obj_new.matrix_world = add_object_align_init(context, operator)
|
||||
|
||||
space_data = context.space_data
|
||||
if space_data and space_data.type != 'VIEW_3D':
|
||||
space_data = None
|
||||
|
||||
if space_data:
|
||||
if space_data.local_view:
|
||||
obj_new.local_view_set(space_data, True)
|
||||
|
||||
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(True)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
obj_act.select_set(True)
|
||||
layer.update() # apply location
|
||||
# layer.objects.active = obj_new
|
||||
|
||||
# Match up UV layers, this is needed so adding an object with UVs
|
||||
# doesn't create new layers when there happens to be a naming mismatch.
|
||||
uv_new = obdata.uv_layers.active
|
||||
if uv_new is not None:
|
||||
uv_act = obj_act.data.uv_layers.active
|
||||
if uv_act is not None:
|
||||
uv_new.name = uv_act.name
|
||||
|
||||
# Copy the active object's active material into the primitive with no materials.
|
||||
if len(obj_act.data.materials) > 0 and len(obdata.materials) == 0:
|
||||
obdata.materials.append(obj_act.active_material)
|
||||
|
||||
bpy.ops.object.join() # join into the active.
|
||||
if obdata:
|
||||
bpy.data.meshes.remove(obdata)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
else:
|
||||
layer.objects.active = obj_new
|
||||
if context.preferences.edit.use_enter_edit_mode:
|
||||
if obdata and obdata.library is None:
|
||||
obtype = obj_new.type
|
||||
mode = None
|
||||
if obtype in {'ARMATURE', 'CURVE', 'CURVES', 'FONT', 'LATTICE', 'MESH', 'META', 'SURFACE'}:
|
||||
mode = 'EDIT'
|
||||
elif obtype == 'GPENCIL':
|
||||
mode = 'EDIT_GPENCIL'
|
||||
|
||||
if mode is not None:
|
||||
bpy.ops.object.mode_set(mode=mode)
|
||||
|
||||
return obj_new
|
||||
|
||||
|
||||
class AddObjectHelper:
|
||||
def align_update_callback(self, context):
|
||||
"""
|
||||
Update callback for the align property, resets rotation for world alignment.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
"""
|
||||
del context
|
||||
if self.align == 'WORLD':
|
||||
self.rotation.zero()
|
||||
|
||||
align: EnumProperty(
|
||||
name="Align",
|
||||
items=(
|
||||
('WORLD', "World", "Align the new object to the world"),
|
||||
('VIEW', "View", "Align the new object to the view"),
|
||||
('CURSOR', "3D Cursor", "Use the 3D cursor orientation for the new object"),
|
||||
),
|
||||
default='WORLD',
|
||||
update=AddObjectHelper.align_update_callback,
|
||||
)
|
||||
location: FloatVectorProperty(
|
||||
name="Location",
|
||||
subtype='TRANSLATION',
|
||||
)
|
||||
rotation: FloatVectorProperty(
|
||||
name="Rotation",
|
||||
subtype='EULER',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""
|
||||
Check the scene is not linked from a library.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: True when the scene is local (not linked from a library).
|
||||
:rtype: bool
|
||||
"""
|
||||
return context.scene.library is None
|
||||
|
||||
|
||||
def object_add_grid_scale(context):
|
||||
"""
|
||||
Return scale which should be applied on object
|
||||
data to align it to grid scale.
|
||||
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
:return: The grid scale.
|
||||
:rtype: float
|
||||
"""
|
||||
|
||||
space_data = context.space_data
|
||||
|
||||
if space_data and space_data.type == 'VIEW_3D':
|
||||
return space_data.overlay.grid_scale_unit
|
||||
|
||||
return 1.0
|
||||
|
||||
|
||||
def object_add_grid_scale_apply_operator(operator, context):
|
||||
"""
|
||||
Scale an operator's distance values by the grid size.
|
||||
|
||||
:param operator: The operator to scale.
|
||||
:type operator: :class:`bpy.types.Operator`
|
||||
:param context: The context.
|
||||
:type context: :class:`bpy.types.Context`
|
||||
"""
|
||||
# This is a Python version of the C++ function `WM_operator_view3d_unit_defaults`.
|
||||
grid_scale = object_add_grid_scale(context)
|
||||
|
||||
properties = operator.properties
|
||||
properties_def = properties.bl_rna.properties
|
||||
for prop_id in properties_def.keys():
|
||||
if not properties.is_property_set(prop_id, ghost=False):
|
||||
prop_def = properties_def[prop_id]
|
||||
if prop_def.unit == 'LENGTH' and prop_def.subtype == 'DISTANCE':
|
||||
setattr(operator, prop_id,
|
||||
getattr(operator, prop_id) * grid_scale)
|
||||
|
||||
|
||||
def world_to_camera_view(scene, obj, coord):
|
||||
"""
|
||||
Returns the camera space coords for a 3d point.
|
||||
(also known as: normalized device coordinates - NDC).
|
||||
|
||||
Where (0, 0) is the bottom left and (1, 1)
|
||||
is the top right of the camera frame.
|
||||
values outside 0-1 are also supported.
|
||||
A negative 'z' value means the point is behind the camera.
|
||||
|
||||
Takes shift-x/y, lens angle and sensor size into account
|
||||
as well as perspective/ortho projections.
|
||||
|
||||
:param scene: Scene to use for frame size.
|
||||
:type scene: :class:`bpy.types.Scene`
|
||||
:param obj: Camera object.
|
||||
:type obj: :class:`bpy.types.Object`
|
||||
:param coord: World space location.
|
||||
:type coord: :class:`mathutils.Vector`
|
||||
:return: a vector where X and Y map to the view plane and
|
||||
Z is the depth on the view axis.
|
||||
:rtype: :class:`mathutils.Vector`
|
||||
"""
|
||||
from mathutils import Vector
|
||||
|
||||
co_local = obj.matrix_world.normalized().inverted() @ coord
|
||||
z = -co_local.z
|
||||
|
||||
camera = obj.data
|
||||
frame = [v for v in camera.view_frame(scene=scene)[:3]]
|
||||
if camera.type != 'ORTHO':
|
||||
if z == 0.0:
|
||||
return Vector((0.5, 0.5, 0.0))
|
||||
else:
|
||||
frame = [-(v / (v.z / z)) for v in frame]
|
||||
|
||||
min_x, max_x = frame[2].x, frame[1].x
|
||||
min_y, max_y = frame[1].y, frame[0].y
|
||||
|
||||
x = (co_local.x - min_x) / (max_x - min_x)
|
||||
y = (co_local.y - min_y) / (max_y - min_y)
|
||||
|
||||
return Vector((x, y, z))
|
||||
|
||||
|
||||
def object_report_if_active_shape_key_is_locked(obj, operator):
|
||||
"""
|
||||
Checks if the active shape key of the specified object is locked, and reports an error if so.
|
||||
|
||||
If the object has no shape keys, there is nothing to lock, and the function returns False.
|
||||
|
||||
:param obj: Object to check.
|
||||
:type obj: :class:`bpy.types.Object`
|
||||
:param operator: Currently running operator to report the error through. Use None to suppress emitting the message.
|
||||
:type operator: :class:`bpy.types.Operator` | None
|
||||
:return: True if the shape key was locked.
|
||||
:rtype: bool
|
||||
"""
|
||||
key = obj.active_shape_key
|
||||
|
||||
if key and key.lock_shape:
|
||||
if operator:
|
||||
operator.report({'ERROR'}, "The active shape key of {:s} is locked".format(obj.name))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
182
blender-5.2.0/scripts/modules/bpy_extras/view3d_utils.py
Normal file
182
blender-5.2.0/scripts/modules/bpy_extras/view3d_utils.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"region_2d_to_vector_3d",
|
||||
"region_2d_to_origin_3d",
|
||||
"region_2d_to_location_3d",
|
||||
"location_3d_to_region_2d",
|
||||
)
|
||||
|
||||
|
||||
def region_2d_to_vector_3d(region, rv3d, coord):
|
||||
"""
|
||||
Return a direction vector from the viewport at the specific 2D region
|
||||
coordinate.
|
||||
|
||||
:param region: region of the 3D viewport, typically bpy.context.region.
|
||||
:type region: :class:`bpy.types.Region`
|
||||
:param rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
:type rv3d: :class:`bpy.types.RegionView3D`
|
||||
:param coord: 2D coordinates relative to the region:
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
:type coord: Sequence[float]
|
||||
:return: normalized 3D vector.
|
||||
:rtype: :class:`mathutils.Vector`
|
||||
"""
|
||||
from mathutils import Vector
|
||||
|
||||
viewinv = rv3d.view_matrix.inverted()
|
||||
if rv3d.is_perspective:
|
||||
persinv = rv3d.perspective_matrix.inverted()
|
||||
|
||||
out = Vector((
|
||||
(2.0 * coord[0] / region.width) - 1.0,
|
||||
(2.0 * coord[1] / region.height) - 1.0,
|
||||
-0.5
|
||||
))
|
||||
|
||||
w = out.dot(persinv[3].xyz) + persinv[3][3]
|
||||
|
||||
view_vector = ((persinv @ out) / w) - viewinv.translation
|
||||
else:
|
||||
view_vector = -viewinv.col[2].xyz
|
||||
|
||||
view_vector.normalize()
|
||||
|
||||
return view_vector
|
||||
|
||||
|
||||
def region_2d_to_origin_3d(region, rv3d, coord, *, clamp=None):
|
||||
"""
|
||||
Return the 3D view origin from the region relative 2D coords.
|
||||
|
||||
.. note::
|
||||
|
||||
Orthographic views have a less obvious origin,
|
||||
the far clip is used to define the viewport near/far extents.
|
||||
Since far clip can be a very large value,
|
||||
the result may have numeric precision issues.
|
||||
|
||||
To avoid this problem, you can optionally clamp the far clip to a
|
||||
smaller value based on the data you're operating on.
|
||||
|
||||
:param region: region of the 3D viewport, typically bpy.context.region.
|
||||
:type region: :class:`bpy.types.Region`
|
||||
:param rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
:type rv3d: :class:`bpy.types.RegionView3D`
|
||||
:param coord: 2D coordinates relative to the region;
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
:type coord: Sequence[float]
|
||||
:param clamp: Clamp the maximum far-clip value used.
|
||||
(negative value will move the offset away from the view_location)
|
||||
:type clamp: float | None
|
||||
:return: The origin of the viewpoint in 3D space.
|
||||
:rtype: :class:`mathutils.Vector`
|
||||
"""
|
||||
viewinv = rv3d.view_matrix.inverted()
|
||||
|
||||
if rv3d.is_perspective:
|
||||
origin_start = viewinv.translation.copy()
|
||||
else:
|
||||
persmat = rv3d.perspective_matrix.copy()
|
||||
dx = (2.0 * coord[0] / region.width) - 1.0
|
||||
dy = (2.0 * coord[1] / region.height) - 1.0
|
||||
persinv = persmat.inverted()
|
||||
origin_start = (
|
||||
(persinv.col[0].xyz * dx) +
|
||||
(persinv.col[1].xyz * dy) +
|
||||
persinv.translation
|
||||
)
|
||||
|
||||
if clamp != 0.0:
|
||||
if rv3d.view_perspective != 'CAMERA':
|
||||
# this value is scaled to the far clip already
|
||||
origin_offset = persinv.col[2].xyz
|
||||
if clamp is not None:
|
||||
if clamp < 0.0:
|
||||
origin_offset.negate()
|
||||
clamp = -clamp
|
||||
if origin_offset.length > clamp:
|
||||
origin_offset.length = clamp
|
||||
|
||||
origin_start -= origin_offset
|
||||
|
||||
return origin_start
|
||||
|
||||
|
||||
def region_2d_to_location_3d(region, rv3d, coord, depth_location):
|
||||
"""
|
||||
Return a 3D location from the region relative 2D coords, aligned with
|
||||
*depth_location*.
|
||||
|
||||
:param region: region of the 3D viewport, typically bpy.context.region.
|
||||
:type region: :class:`bpy.types.Region`
|
||||
:param rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
:type rv3d: :class:`bpy.types.RegionView3D`
|
||||
:param coord: 2D coordinates relative to the region;
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
:type coord: Sequence[float]
|
||||
:param depth_location: the returned vectors depth is aligned with this since
|
||||
there is no defined depth with a 2D region input.
|
||||
:type depth_location: :class:`mathutils.Vector`
|
||||
:return: normalized 3D vector.
|
||||
:rtype: :class:`mathutils.Vector`
|
||||
"""
|
||||
from mathutils import Vector
|
||||
|
||||
coord_vec = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
depth_location = Vector(depth_location)
|
||||
|
||||
origin_start = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
origin_end = origin_start + coord_vec
|
||||
|
||||
if rv3d.is_perspective:
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
viewinv = rv3d.view_matrix.inverted()
|
||||
view_vec = viewinv.col[2].copy()
|
||||
return intersect_line_plane(
|
||||
origin_start,
|
||||
origin_end,
|
||||
depth_location,
|
||||
view_vec, 1,
|
||||
)
|
||||
else:
|
||||
from mathutils.geometry import intersect_point_line
|
||||
return intersect_point_line(
|
||||
depth_location,
|
||||
origin_start,
|
||||
origin_end,
|
||||
)[0]
|
||||
|
||||
|
||||
def location_3d_to_region_2d(region, rv3d, coord, *, default=None):
|
||||
"""
|
||||
Return the *region* relative 2D location of a 3D position.
|
||||
|
||||
:param region: region of the 3D viewport, typically bpy.context.region.
|
||||
:type region: :class:`bpy.types.Region`
|
||||
:param rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
:type rv3d: :class:`bpy.types.RegionView3D`
|
||||
:param coord: 3D world-space location.
|
||||
:type coord: :class:`mathutils.Vector`
|
||||
:param default: Return this value if ``coord``
|
||||
is behind the origin of a perspective view.
|
||||
:type default: Any
|
||||
:return: 2D location
|
||||
:rtype: :class:`mathutils.Vector` | Any
|
||||
"""
|
||||
from mathutils import Vector
|
||||
|
||||
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
|
||||
|
||||
return Vector((
|
||||
width_half + width_half * (prj.x / prj.w),
|
||||
height_half + height_half * (prj.y / prj.w),
|
||||
))
|
||||
else:
|
||||
return default
|
||||
@@ -0,0 +1,160 @@
|
||||
# SPDX-FileCopyrightText: 2015-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"ProgressReport",
|
||||
"ProgressReportSubstep",
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ProgressReport:
|
||||
"""
|
||||
A basic 'progress report' using either simple prints in console, or WindowManager's 'progress' API.
|
||||
|
||||
This object can be used as a context manager.
|
||||
|
||||
It supports multiple levels of 'sub-steps' - you shall always enter at least one sub-step (because level 0
|
||||
has only one single step, representing the whole 'area' of the progress stuff).
|
||||
|
||||
You should give the expected number of sub-steps each time you enter a new one (you may then step more or less then
|
||||
given number, but this will give incoherent progression).
|
||||
|
||||
Leaving a sub-step automatically steps by one the parent level.
|
||||
|
||||
with ProgressReport() as progress: # Not giving a WindowManager here will default to console printing.
|
||||
progress.enter_substeps(10)
|
||||
for i in range(10):
|
||||
progress.enter_substeps(100)
|
||||
for j in range(100):
|
||||
progress.step()
|
||||
progress.leave_substeps() # No need to step here, this implicitly does it.
|
||||
progress.leave_substeps("Finished!") # You may pass some message too.
|
||||
"""
|
||||
__slots__ = ("wm", "running", "steps", "curr_step", "start_time")
|
||||
|
||||
def __init__(self, wm=None):
|
||||
self_wm = getattr(self, "wm", None)
|
||||
if self_wm:
|
||||
self.finalize()
|
||||
self.running = False
|
||||
|
||||
self.wm = wm
|
||||
self.steps = [100000]
|
||||
self.curr_step = [0]
|
||||
|
||||
initialize = __init__
|
||||
|
||||
def __enter__(self):
|
||||
self.start_time = [time.time()]
|
||||
if self.wm:
|
||||
self.wm.progress_begin(0, self.steps[0])
|
||||
self.update()
|
||||
self.running = True
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type=None, _exc_value=None, _traceback=None):
|
||||
self.running = False
|
||||
if self.wm:
|
||||
self.wm.progress_end()
|
||||
self.wm = None
|
||||
print("\n")
|
||||
self.steps = [100000]
|
||||
self.curr_step = [0]
|
||||
self.start_time = [time.time()]
|
||||
|
||||
def start(self):
|
||||
self.__enter__()
|
||||
|
||||
def finalize(self):
|
||||
self.__exit__()
|
||||
|
||||
def update(self, msg=""):
|
||||
steps = sum(s * cs for (s, cs) in zip(self.steps, self.curr_step))
|
||||
steps_percent = steps / self.steps[0] * 100.0
|
||||
tm = time.time()
|
||||
loc_tm = tm - self.start_time[-1]
|
||||
tm -= self.start_time[0]
|
||||
if self.wm and self.running:
|
||||
self.wm.progress_update(steps)
|
||||
if msg:
|
||||
prefix = " " * (len(self.steps) - 1)
|
||||
print(
|
||||
prefix + "({:8.4f} sec | {:8.4f} sec) {:s}\nProgress: {:6.2f}%\r".format(
|
||||
tm, loc_tm, msg, steps_percent,
|
||||
),
|
||||
end="",
|
||||
)
|
||||
else:
|
||||
print("Progress: {:6.2f}%\r".format(steps_percent,), end="")
|
||||
|
||||
def enter_substeps(self, nbr, msg=""):
|
||||
if msg:
|
||||
self.update(msg)
|
||||
self.steps.append(self.steps[-1] / max(nbr, 1))
|
||||
self.curr_step.append(0)
|
||||
self.start_time.append(time.time())
|
||||
|
||||
def step(self, msg="", nbr=1):
|
||||
self.curr_step[-1] += nbr
|
||||
self.update(msg)
|
||||
|
||||
def leave_substeps(self, msg=""):
|
||||
if (msg):
|
||||
self.update(msg)
|
||||
assert len(self.steps) > 1
|
||||
del self.steps[-1]
|
||||
del self.curr_step[-1]
|
||||
del self.start_time[-1]
|
||||
self.step()
|
||||
|
||||
|
||||
class ProgressReportSubstep:
|
||||
"""
|
||||
A sub-step context manager for ProgressReport.
|
||||
|
||||
It can be used to generate other sub-step contexts too, and can act as a (limited) proxy of its real ProgressReport.
|
||||
|
||||
Its exit method always ensure ProgressReport is back on 'level' it was before entering this context.
|
||||
This means it is especially useful to ensure a coherent behavior around code that could return/continue/break
|
||||
from many places, without having to bother to explicitly leave sub-step in each and every possible place!
|
||||
|
||||
with ProgressReport() as progress: # Not giving a WindowManager here will default to console printing.
|
||||
with ProgressReportSubstep(progress, 10, final_msg="Finished!") as subprogress1:
|
||||
for i in range(10):
|
||||
with ProgressReportSubstep(subprogress1, 100) as subprogress2:
|
||||
for j in range(100):
|
||||
subprogress2.step()
|
||||
"""
|
||||
__slots__ = ("progress", "nbr", "msg", "final_msg", "level")
|
||||
|
||||
def __init__(self, progress, nbr, msg="", final_msg=""):
|
||||
# Allows to generate a sub-progress context handler from another one.
|
||||
progress = getattr(progress, "progress", progress)
|
||||
|
||||
self.progress = progress
|
||||
self.nbr = nbr
|
||||
self.msg = msg
|
||||
self.final_msg = final_msg
|
||||
|
||||
def __enter__(self):
|
||||
self.level = len(self.progress.steps)
|
||||
self.progress.enter_substeps(self.nbr, self.msg)
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
assert len(self.progress.steps) > self.level
|
||||
while len(self.progress.steps) > self.level + 1:
|
||||
self.progress.leave_substeps()
|
||||
self.progress.leave_substeps(self.final_msg)
|
||||
|
||||
def enter_substeps(self, nbr, msg=""):
|
||||
self.progress.enter_substeps(nbr, msg)
|
||||
|
||||
def step(self, msg="", nbr=1):
|
||||
self.progress.step(msg, nbr)
|
||||
|
||||
def leave_substeps(self, msg=""):
|
||||
self.progress.leave_substeps(msg)
|
||||
Reference in New Issue
Block a user