Add Chromium-only Blender WebEngine parity work
This commit is contained in:
89
blender-5.2.0/scripts/startup/bl_operators/__init__.py
Normal file
89
blender-5.2.0/scripts/startup/bl_operators/__init__.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# support reloading sub-modules
|
||||
if "bpy" in locals():
|
||||
from importlib import reload
|
||||
_modules_loaded[:] = [reload(val) for val in _modules_loaded]
|
||||
del reload
|
||||
|
||||
_modules = [
|
||||
"add_mesh_torus",
|
||||
"anim",
|
||||
"assets",
|
||||
"bone_selection_sets",
|
||||
"clip",
|
||||
"connect_to_output",
|
||||
"console",
|
||||
"constraint",
|
||||
"copy_global_transform",
|
||||
"file",
|
||||
"geometry_nodes",
|
||||
"grease_pencil",
|
||||
"image",
|
||||
"image_as_planes",
|
||||
"mesh",
|
||||
"node",
|
||||
"object",
|
||||
"object_align",
|
||||
"object_quick_effects",
|
||||
"object_randomize_transform",
|
||||
"presets",
|
||||
"render",
|
||||
"rigidbody",
|
||||
"screen_play_rendered_anim",
|
||||
"sequencer",
|
||||
"spreadsheet",
|
||||
"userpref",
|
||||
"uvcalc_follow_active",
|
||||
"uvcalc_lightmap",
|
||||
"uvcalc_transform",
|
||||
"vertexpaint_dirt",
|
||||
"view3d",
|
||||
"world",
|
||||
"wm",
|
||||
]
|
||||
|
||||
import bpy
|
||||
|
||||
if bpy.app.build_options.freestyle:
|
||||
_modules.append("freestyle")
|
||||
|
||||
__import__(name=__name__, fromlist=_modules)
|
||||
_namespace = globals()
|
||||
_modules_loaded = [_namespace[name] for name in _modules]
|
||||
del _namespace
|
||||
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
from . import (
|
||||
bone_selection_sets,
|
||||
copy_global_transform,
|
||||
)
|
||||
|
||||
for mod in _modules_loaded:
|
||||
for cls in mod.classes:
|
||||
register_class(cls)
|
||||
|
||||
bone_selection_sets.register()
|
||||
copy_global_transform.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
from . import (
|
||||
bone_selection_sets,
|
||||
copy_global_transform,
|
||||
)
|
||||
|
||||
bone_selection_sets.unregister()
|
||||
copy_global_transform.unregister()
|
||||
|
||||
for mod in reversed(_modules_loaded):
|
||||
for cls in reversed(mod.classes):
|
||||
if cls.is_registered:
|
||||
unregister_class(cls)
|
||||
262
blender-5.2.0/scripts/startup/bl_operators/add_mesh_torus.py
Normal file
262
blender-5.2.0/scripts/startup/bl_operators/add_mesh_torus.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
)
|
||||
from bpy.app.translations import pgettext_data as data_
|
||||
|
||||
from bpy_extras import object_utils
|
||||
|
||||
|
||||
def add_torus(major_rad, minor_rad, major_seg, minor_seg):
|
||||
from math import cos, sin, pi
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
pi_2 = pi * 2.0
|
||||
|
||||
verts = []
|
||||
faces = []
|
||||
i1 = 0
|
||||
tot_verts = major_seg * minor_seg
|
||||
for major_index in range(major_seg):
|
||||
matrix = Matrix.Rotation((major_index / major_seg) * pi_2, 3, 'Z')
|
||||
|
||||
for minor_index in range(minor_seg):
|
||||
angle = pi_2 * minor_index / minor_seg
|
||||
|
||||
vec = matrix @ Vector((
|
||||
major_rad + (cos(angle) * minor_rad),
|
||||
0.0,
|
||||
sin(angle) * minor_rad,
|
||||
))
|
||||
|
||||
verts.extend(vec[:])
|
||||
|
||||
if minor_index + 1 == minor_seg:
|
||||
i2 = (major_index) * minor_seg
|
||||
i3 = i1 + minor_seg
|
||||
i4 = i2 + minor_seg
|
||||
else:
|
||||
i2 = i1 + 1
|
||||
i3 = i1 + minor_seg
|
||||
i4 = i3 + 1
|
||||
|
||||
if i2 >= tot_verts:
|
||||
i2 = i2 - tot_verts
|
||||
if i3 >= tot_verts:
|
||||
i3 = i3 - tot_verts
|
||||
if i4 >= tot_verts:
|
||||
i4 = i4 - tot_verts
|
||||
|
||||
faces.extend([i1, i3, i4, i2])
|
||||
|
||||
i1 += 1
|
||||
|
||||
return verts, faces
|
||||
|
||||
|
||||
def add_uvs(mesh, minor_seg, major_seg):
|
||||
from math import fmod
|
||||
|
||||
mesh.uv_layers.new()
|
||||
uv_data = mesh.uv_layers.active.data
|
||||
polygons = mesh.polygons
|
||||
u_step = 1.0 / major_seg
|
||||
v_step = 1.0 / minor_seg
|
||||
|
||||
# Round UVs, needed when segments aren't divisible by 4.
|
||||
u_init = 0.5 + fmod(0.5, u_step)
|
||||
v_init = 0.5 + fmod(0.5, v_step)
|
||||
|
||||
# Calculate wrapping value under 1.0 to prevent
|
||||
# float precision errors wrapping at the wrong step.
|
||||
u_wrap = 1.0 - (u_step / 2.0)
|
||||
v_wrap = 1.0 - (v_step / 2.0)
|
||||
|
||||
vertex_index = 0
|
||||
|
||||
u_prev = u_init
|
||||
u_next = u_prev + u_step
|
||||
for _major_index in range(major_seg):
|
||||
v_prev = v_init
|
||||
v_next = v_prev + v_step
|
||||
for _minor_index in range(minor_seg):
|
||||
loops = polygons[vertex_index].loop_indices
|
||||
uv_data[loops[0]].uv = u_prev, v_prev
|
||||
uv_data[loops[1]].uv = u_next, v_prev
|
||||
uv_data[loops[3]].uv = u_prev, v_next
|
||||
uv_data[loops[2]].uv = u_next, v_next
|
||||
|
||||
if v_next > v_wrap:
|
||||
v_prev = v_next - 1.0
|
||||
else:
|
||||
v_prev = v_next
|
||||
v_next = v_prev + v_step
|
||||
|
||||
vertex_index += 1
|
||||
|
||||
if u_next > u_wrap:
|
||||
u_prev = u_next - 1.0
|
||||
else:
|
||||
u_prev = u_next
|
||||
u_next = u_prev + u_step
|
||||
|
||||
|
||||
class AddTorus(Operator, object_utils.AddObjectHelper):
|
||||
"""Construct a torus mesh"""
|
||||
bl_idname = "mesh.primitive_torus_add"
|
||||
bl_label = "Add Torus"
|
||||
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
|
||||
|
||||
def mode_update_callback(self, _context):
|
||||
if self.mode == 'EXT_INT':
|
||||
self.abso_major_rad = self.major_radius + self.minor_radius
|
||||
self.abso_minor_rad = self.major_radius - self.minor_radius
|
||||
|
||||
major_segments: IntProperty(
|
||||
name="Major Segments",
|
||||
description="Number of segments for the main ring of the torus",
|
||||
min=3, max=256,
|
||||
default=48,
|
||||
)
|
||||
minor_segments: IntProperty(
|
||||
name="Minor Segments",
|
||||
description="Number of segments for the minor ring of the torus",
|
||||
min=3, max=256,
|
||||
default=12,
|
||||
)
|
||||
mode: EnumProperty(
|
||||
name="Dimensions Mode",
|
||||
items=(
|
||||
('MAJOR_MINOR', "Major/Minor",
|
||||
"Use the major/minor radii for torus dimensions"),
|
||||
('EXT_INT', "Exterior/Interior",
|
||||
"Use the exterior/interior radii for torus dimensions"),
|
||||
),
|
||||
update=AddTorus.mode_update_callback,
|
||||
)
|
||||
major_radius: FloatProperty(
|
||||
name="Major Radius",
|
||||
description="Radius from the origin to the center of the cross sections",
|
||||
soft_min=0.0, soft_max=100.0,
|
||||
min=0.0, max=10_000.0,
|
||||
default=1.0,
|
||||
subtype='DISTANCE',
|
||||
unit='LENGTH',
|
||||
)
|
||||
minor_radius: FloatProperty(
|
||||
name="Minor Radius",
|
||||
description="Radius of the torus's cross section",
|
||||
soft_min=0.0, soft_max=100.0,
|
||||
min=0.0, max=10_000.0,
|
||||
default=0.25,
|
||||
subtype='DISTANCE',
|
||||
unit='LENGTH',
|
||||
)
|
||||
abso_major_rad: FloatProperty(
|
||||
name="Exterior Radius",
|
||||
description="Total Exterior Radius of the torus",
|
||||
soft_min=0.0, soft_max=100.0,
|
||||
min=0.0, max=10_000.0,
|
||||
default=1.25,
|
||||
subtype='DISTANCE',
|
||||
unit='LENGTH',
|
||||
)
|
||||
abso_minor_rad: FloatProperty(
|
||||
name="Interior Radius",
|
||||
description="Total Interior Radius of the torus",
|
||||
soft_min=0.0, soft_max=100.0,
|
||||
min=0.0, max=10_000.0,
|
||||
default=0.75,
|
||||
subtype='DISTANCE',
|
||||
unit='LENGTH',
|
||||
)
|
||||
generate_uvs: BoolProperty(
|
||||
name="Generate UVs",
|
||||
description="Generate a default UV map",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.prop(self, "major_segments")
|
||||
layout.prop(self, "minor_segments")
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.prop(self, "mode")
|
||||
if self.mode == 'MAJOR_MINOR':
|
||||
layout.prop(self, "major_radius")
|
||||
layout.prop(self, "minor_radius")
|
||||
else:
|
||||
layout.prop(self, "abso_major_rad")
|
||||
layout.prop(self, "abso_minor_rad")
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.prop(self, "generate_uvs")
|
||||
layout.prop(self, "align")
|
||||
layout.prop(self, "location")
|
||||
layout.prop(self, "rotation")
|
||||
|
||||
def invoke(self, context, _event):
|
||||
object_utils.object_add_grid_scale_apply_operator(self, context)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if self.mode == 'EXT_INT':
|
||||
extra_helper = (self.abso_major_rad - self.abso_minor_rad) * 0.5
|
||||
self.major_radius = self.abso_minor_rad + extra_helper
|
||||
self.minor_radius = extra_helper
|
||||
|
||||
verts_loc, faces = add_torus(
|
||||
self.major_radius,
|
||||
self.minor_radius,
|
||||
self.major_segments,
|
||||
self.minor_segments,
|
||||
)
|
||||
|
||||
mesh = bpy.data.meshes.new(data_("Torus"))
|
||||
|
||||
mesh.vertices.add(len(verts_loc) // 3)
|
||||
|
||||
nbr_loops = len(faces)
|
||||
nbr_polys = nbr_loops // 4
|
||||
mesh.loops.add(nbr_loops)
|
||||
mesh.polygons.add(nbr_polys)
|
||||
|
||||
mesh.vertices.foreach_set("co", verts_loc)
|
||||
mesh.polygons.foreach_set("loop_start", range(0, nbr_loops, 4))
|
||||
mesh.loops.foreach_set("vertex_index", faces)
|
||||
mesh.shade_flat()
|
||||
|
||||
if self.generate_uvs:
|
||||
add_uvs(mesh, self.minor_segments, self.major_segments)
|
||||
|
||||
mesh.update()
|
||||
|
||||
object_utils.object_data_add(context, mesh, operator=self)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
AddTorus,
|
||||
)
|
||||
923
blender-5.2.0/scripts/startup/bl_operators/anim.py
Normal file
923
blender-5.2.0/scripts/startup/bl_operators/anim.py
Normal file
@@ -0,0 +1,923 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.app.translations import (
|
||||
pgettext_rpt as rpt_,
|
||||
contexts as i18n_contexts,
|
||||
)
|
||||
|
||||
|
||||
class ANIM_OT_keying_set_export(Operator):
|
||||
"""Export Keying Set to a Python script"""
|
||||
bl_idname = "anim.keying_set_export"
|
||||
bl_label = "Export Keying Set..."
|
||||
|
||||
filepath: StringProperty(
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
filter_folder: BoolProperty(
|
||||
name="Filter folders",
|
||||
default=True,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
filter_text: BoolProperty(
|
||||
name="Filter text",
|
||||
default=True,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
filter_python: BoolProperty(
|
||||
name="Filter Python",
|
||||
default=True,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
from bpy.utils import escape_identifier
|
||||
|
||||
if not self.filepath:
|
||||
raise Exception("Filepath not set")
|
||||
|
||||
f = open(self.filepath, "w", encoding="utf8")
|
||||
if not f:
|
||||
raise Exception("Could not open file")
|
||||
|
||||
scene = context.scene
|
||||
ks = scene.keying_sets.active
|
||||
|
||||
f.write("# Keying Set: {:s}\n".format(ks.bl_idname))
|
||||
|
||||
f.write("import bpy\n\n")
|
||||
f.write("scene = bpy.context.scene\n\n")
|
||||
|
||||
# Add KeyingSet and set general settings
|
||||
f.write("# Keying Set Level declarations\n")
|
||||
f.write("ks = scene.keying_sets.new(idname={!r}, name={!r})\n".format(ks.bl_idname, ks.bl_label))
|
||||
f.write("ks.bl_description = {!r}\n".format(ks.bl_description))
|
||||
|
||||
# TODO: this isn't editable, it should be possible to set this flag for `scene.keying_sets.new`.
|
||||
# if not ks.is_path_absolute:
|
||||
# f.write("ks.is_path_absolute = False\n")
|
||||
f.write("\n")
|
||||
|
||||
f.write("ks.use_insertkey_needed = {!r}\n".format(ks.use_insertkey_needed))
|
||||
f.write("ks.use_insertkey_visual = {!r}\n".format(ks.use_insertkey_visual))
|
||||
f.write("\n")
|
||||
|
||||
# --------------------------------------------------------
|
||||
# generate and write set of lookups for id's used in paths
|
||||
|
||||
# cache for syncing ID-blocks to bpy paths + shorthand's
|
||||
id_to_paths_cache = {}
|
||||
|
||||
for ksp in ks.paths:
|
||||
if ksp.id is None:
|
||||
continue
|
||||
if ksp.id in id_to_paths_cache:
|
||||
continue
|
||||
|
||||
# - `idtype_list` is used to get the list of ID-data-blocks from
|
||||
# `bpy.data.*` since this info isn't available elsewhere.
|
||||
# - `id.bl_rna.name` gives a name suitable for UI,
|
||||
# with a capitalized first letter, but we need
|
||||
# the plural form that's all lower case.
|
||||
# - special handling is needed for "nested" ID-blocks
|
||||
# (e.g. node-tree in Material).
|
||||
if ksp.id.bl_rna.identifier.startswith("ShaderNodeTree"):
|
||||
# Find material or light using this node tree...
|
||||
id_bpy_path = "bpy.data.nodes[\"{:s}\"]"
|
||||
found = False
|
||||
|
||||
for mat in bpy.data.materials:
|
||||
if mat.node_tree == ksp.id:
|
||||
id_bpy_path = "bpy.data.materials[\"{:s}\"].node_tree".format(escape_identifier(mat.name))
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
for light in bpy.data.lights:
|
||||
if light.node_tree == ksp.id:
|
||||
id_bpy_path = "bpy.data.lights[\"{:s}\"].node_tree".format(escape_identifier(light.name))
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
rpt_("Could not find material or light using Shader Node Tree - {:s}").format(str(ksp.id)),
|
||||
)
|
||||
elif ksp.id.bl_rna.identifier.startswith("CompositorNodeTree"):
|
||||
# Find compositor node-tree using this node tree.
|
||||
for scene in bpy.data.scenes:
|
||||
if scene.compositing_node_group == ksp.id:
|
||||
id_bpy_path = "bpy.data.scenes[\"{:s}\"].compositing_node_group".format(
|
||||
escape_identifier(scene.name))
|
||||
break
|
||||
else:
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
rpt_("Could not find scene using Compositor Node Tree - {:s}").format(str(ksp.id)),
|
||||
)
|
||||
elif ksp.id.bl_rna.name == "Key":
|
||||
# "keys" conflicts with a Python keyword, hence the simple solution won't work
|
||||
id_bpy_path = "bpy.data.shape_keys[\"{:s}\"]".format(escape_identifier(ksp.id.name))
|
||||
else:
|
||||
idtype_list = ksp.id.bl_rna.name.lower() + "s"
|
||||
id_bpy_path = "bpy.data.{:s}[\"{:s}\"]".format(idtype_list, escape_identifier(ksp.id.name))
|
||||
|
||||
# shorthand ID for the ID-block (as used in the script)
|
||||
short_id = "id_{:d}".format(len(id_to_paths_cache))
|
||||
|
||||
# store this in the cache now
|
||||
id_to_paths_cache[ksp.id] = [short_id, id_bpy_path]
|
||||
|
||||
f.write("# ID's that are commonly used\n")
|
||||
for id_pair in id_to_paths_cache.values():
|
||||
f.write("{:s} = {:s}\n".format(id_pair[0], id_pair[1]))
|
||||
f.write("\n")
|
||||
|
||||
# write paths
|
||||
f.write("# Path Definitions\n")
|
||||
for ksp in ks.paths:
|
||||
f.write("ksp = ks.paths.add(")
|
||||
|
||||
# id-block + data_path
|
||||
if ksp.id:
|
||||
# find the relevant shorthand from the cache
|
||||
id_bpy_path = id_to_paths_cache[ksp.id][0]
|
||||
else:
|
||||
id_bpy_path = "None" # XXX...
|
||||
f.write("{:s}, {!r}".format(id_bpy_path, ksp.data_path))
|
||||
|
||||
# array index settings (if applicable)
|
||||
if ksp.use_entire_array:
|
||||
f.write(", index=-1")
|
||||
else:
|
||||
f.write(", index={:d}".format(ksp.array_index))
|
||||
|
||||
# grouping settings (if applicable)
|
||||
# NOTE: the current default is KEYINGSET, but if this changes,
|
||||
# change this code too
|
||||
if ksp.group_method == 'NAMED':
|
||||
f.write(", group_method={!r}, group_name={!r}".format(ksp.group_method, ksp.group))
|
||||
elif ksp.group_method != 'KEYINGSET':
|
||||
f.write(", group_method={!r}".format(ksp.group_method))
|
||||
|
||||
# finish off
|
||||
f.write(")\n")
|
||||
|
||||
f.write("\n")
|
||||
f.close()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
wm = context.window_manager
|
||||
wm.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class NLA_OT_bake(Operator):
|
||||
"""Bake all selected objects location/scale/rotation animation to an action"""
|
||||
bl_idname = "nla.bake"
|
||||
bl_label = "Bake Action"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
frame_start: IntProperty(
|
||||
name="Start Frame",
|
||||
description="Start frame for baking",
|
||||
min=0, max=300000,
|
||||
default=1,
|
||||
)
|
||||
frame_end: IntProperty(
|
||||
name="End Frame",
|
||||
description="End frame for baking",
|
||||
min=1, max=300000,
|
||||
default=250,
|
||||
)
|
||||
step: IntProperty(
|
||||
name="Frame Step",
|
||||
description="Number of frames to skip forward while baking each frame",
|
||||
min=1, max=120,
|
||||
default=1,
|
||||
)
|
||||
only_selected: BoolProperty(
|
||||
name="Only Selected Bones",
|
||||
description="Only key selected bones (Pose baking only)",
|
||||
default=True,
|
||||
)
|
||||
visual_keying: BoolProperty(
|
||||
name="Visual Keying",
|
||||
description="Keyframe from the final transformations (with constraints applied)",
|
||||
default=False,
|
||||
)
|
||||
clear_constraints: BoolProperty(
|
||||
name="Clear Local Constraints",
|
||||
description=(
|
||||
"Remove all constraints from keyed object/bones. "
|
||||
"To get a correct bake with this setting Visual Keying should be enabled"
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
clear_parents: BoolProperty(
|
||||
name="Clear Parents",
|
||||
description="Bake animation onto the object then clear parents (objects only)",
|
||||
default=False,
|
||||
)
|
||||
use_current_action: BoolProperty(
|
||||
name="Overwrite Current Action",
|
||||
description="Bake animation into current action, instead of creating a new one "
|
||||
"(useful for baking only part of bones in an armature)",
|
||||
default=False,
|
||||
)
|
||||
clean_curves: BoolProperty(
|
||||
name="Clean Curves",
|
||||
description="After baking curves, remove redundant keys",
|
||||
default=False,
|
||||
)
|
||||
bake_types: EnumProperty(
|
||||
name="Bake Data",
|
||||
translation_context=i18n_contexts.id_action,
|
||||
description="Which data's transformations to bake",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(
|
||||
('POSE', "Pose", "Bake bones transformations"),
|
||||
('OBJECT', "Object", "Bake object transformations"),
|
||||
),
|
||||
default={'POSE'},
|
||||
)
|
||||
channel_types: EnumProperty(
|
||||
name="Channels",
|
||||
description="Which channels to bake",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(
|
||||
('LOCATION', "Location", "Bake location channels"),
|
||||
('ROTATION', "Rotation", "Bake rotation channels"),
|
||||
('SCALE', "Scale", "Bake scale channels"),
|
||||
('BBONE', "B-Bone", "Bake B-Bone channels"),
|
||||
('PROPS', "Custom Properties", "Bake custom properties"),
|
||||
),
|
||||
default={'LOCATION', 'ROTATION', 'SCALE', 'BBONE', 'PROPS'},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras import anim_utils
|
||||
|
||||
bake_options = anim_utils.BakeOptions(
|
||||
only_selected=self.only_selected,
|
||||
do_pose='POSE' in self.bake_types,
|
||||
do_object='OBJECT' in self.bake_types,
|
||||
do_visual_keying=self.visual_keying,
|
||||
do_constraint_clear=self.clear_constraints,
|
||||
do_parents_clear=self.clear_parents,
|
||||
do_clean=self.clean_curves,
|
||||
do_location='LOCATION' in self.channel_types,
|
||||
do_rotation='ROTATION' in self.channel_types,
|
||||
do_scale='SCALE' in self.channel_types,
|
||||
do_bbone='BBONE' in self.channel_types,
|
||||
do_custom_props='PROPS' in self.channel_types,
|
||||
)
|
||||
|
||||
if bake_options.do_pose and self.only_selected:
|
||||
pose_bones = context.selected_pose_bones or []
|
||||
armatures = {pose_bone.id_data for pose_bone in pose_bones}
|
||||
objects = list(armatures)
|
||||
else:
|
||||
objects = context.selected_editable_objects
|
||||
if bake_options.do_pose and not bake_options.do_object:
|
||||
pose_object = getattr(context, "pose_object", None)
|
||||
if pose_object and pose_object not in objects:
|
||||
# The active object might not be selected, but it is the one in pose mode.
|
||||
# It can be assumed this pose needs baking.
|
||||
objects.append(pose_object)
|
||||
objects = [obj for obj in objects if obj.pose is not None]
|
||||
|
||||
object_action_pairs = (
|
||||
[(obj, getattr(obj.animation_data, "action", None)) for obj in objects]
|
||||
if self.use_current_action else
|
||||
[(obj, None) for obj in objects]
|
||||
)
|
||||
|
||||
actions = anim_utils.bake_action_objects(
|
||||
object_action_pairs,
|
||||
frames=range(self.frame_start, self.frame_end + 1, self.step),
|
||||
bake_options=bake_options,
|
||||
)
|
||||
|
||||
if not any(actions):
|
||||
self.report({'INFO'}, "Nothing to bake")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
scene = context.scene
|
||||
if scene.use_preview_range:
|
||||
self.frame_start = scene.frame_preview_start
|
||||
self.frame_end = scene.frame_preview_end
|
||||
else:
|
||||
self.frame_start = scene.frame_start
|
||||
self.frame_end = scene.frame_end
|
||||
self.bake_types = {'POSE'} if context.mode == 'POSE' else {'OBJECT'}
|
||||
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class ClearUselessActions(Operator):
|
||||
"""Mark actions with no F-Curves for deletion after save and reload of """ \
|
||||
"""file preserving \"action libraries\""""
|
||||
bl_idname = "anim.clear_useless_actions"
|
||||
bl_label = "Clear Useless Actions"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
only_unused: BoolProperty(
|
||||
name="Only Unused",
|
||||
description="Only unused (Fake User only) actions get considered",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, _context):
|
||||
return bool(bpy.data.actions)
|
||||
|
||||
@staticmethod
|
||||
def has_fcurves(action: bpy.types.Action) -> bool:
|
||||
for layer in action.layers:
|
||||
for strip in layer.strips:
|
||||
assert strip.type == 'KEYFRAME'
|
||||
for channelbag in strip.channelbags:
|
||||
if channelbag.fcurves:
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, _context):
|
||||
removed = 0
|
||||
|
||||
for action in bpy.data.actions:
|
||||
# if only user is "fake" user...
|
||||
if (
|
||||
(self.only_unused is False) or
|
||||
(action.use_fake_user and action.users == 1)
|
||||
):
|
||||
|
||||
# if it has F-Curves, then it's a "action library"
|
||||
# (i.e. walk, wave, jump, etc.)
|
||||
# and should be left alone as that's what fake users are for!
|
||||
if not self.has_fcurves(action):
|
||||
# mark action for deletion
|
||||
action.user_clear()
|
||||
removed += 1
|
||||
|
||||
self.report({'INFO'}, rpt_("Removed {:d} empty and/or fake-user only Actions").format(removed))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UpdateAnimatedTransformConstraint(Operator):
|
||||
"""Update f-curves/drivers affecting Transform constraints (use it with files from 2.70 and earlier)"""
|
||||
bl_idname = "anim.update_animated_transform_constraints"
|
||||
bl_label = "Update Animated Transform Constraints"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
use_convert_to_radians: BoolProperty(
|
||||
name="Convert to Radians",
|
||||
description=(
|
||||
"Convert f-curves/drivers affecting rotations to radians.\n"
|
||||
"Warning: Use this only once"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
import _animsys_refactor as animsys_refactor
|
||||
from math import radians
|
||||
import io
|
||||
|
||||
from_paths = {"from_max_x", "from_max_y", "from_max_z", "from_min_x", "from_min_y", "from_min_z"}
|
||||
to_paths = {"to_max_x", "to_max_y", "to_max_z", "to_min_x", "to_min_y", "to_min_z"}
|
||||
paths = from_paths | to_paths
|
||||
|
||||
def update_cb(base, _class_name, old_path, fcurve, options):
|
||||
# print(options)
|
||||
|
||||
def handle_deg2rad(fcurve):
|
||||
if fcurve is not None:
|
||||
if hasattr(fcurve, "keyframes"):
|
||||
for k in fcurve.keyframes:
|
||||
k.co.y = radians(k.co.y)
|
||||
for mod in fcurve.modifiers:
|
||||
if mod.type == 'GENERATOR':
|
||||
if mod.mode == 'POLYNOMIAL':
|
||||
mod.coefficients[:] = [radians(c) for c in mod.coefficients]
|
||||
else: # if mod.type == 'POLYNOMIAL_FACTORISED':
|
||||
mod.coefficients[:2] = [radians(c) for c in mod.coefficients[:2]]
|
||||
elif mod.type == 'FNGENERATOR':
|
||||
mod.amplitude = radians(mod.amplitude)
|
||||
fcurve.update()
|
||||
|
||||
data = ...
|
||||
try:
|
||||
data = eval("base." + old_path)
|
||||
except Exception:
|
||||
pass
|
||||
ret = (data, old_path)
|
||||
if isinstance(base, bpy.types.TransformConstraint) and data is not ...:
|
||||
new_path = None
|
||||
map_info = base.map_from if old_path in from_paths else base.map_to
|
||||
if map_info == 'ROTATION':
|
||||
new_path = old_path + "_rot"
|
||||
if options is not None and options["use_convert_to_radians"]:
|
||||
handle_deg2rad(fcurve)
|
||||
elif map_info == 'SCALE':
|
||||
new_path = old_path + "_scale"
|
||||
|
||||
if new_path is not None:
|
||||
data = ...
|
||||
try:
|
||||
data = eval("base." + new_path)
|
||||
except Exception:
|
||||
pass
|
||||
ret = (data, new_path)
|
||||
# print(ret)
|
||||
|
||||
return ret
|
||||
|
||||
options = {"use_convert_to_radians": self.use_convert_to_radians}
|
||||
replace_ls = [("TransformConstraint", p, update_cb, options) for p in paths]
|
||||
log = io.StringIO()
|
||||
|
||||
animsys_refactor.update_data_paths(replace_ls, log)
|
||||
|
||||
context.scene.frame_set(context.scene.frame_current)
|
||||
|
||||
log = log.getvalue()
|
||||
if log:
|
||||
print(log)
|
||||
text = bpy.data.texts.new("UpdateAnimatedTransformConstraint Report")
|
||||
text.from_string(log)
|
||||
self.report({'INFO'}, rpt_("Complete report available on '{:s}' text data-block").format(text.name))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ARMATURE_OT_copy_bone_color_to_selected(Operator):
|
||||
"""Copy the bone color of the active bone to all selected bones"""
|
||||
bl_idname = "armature.copy_bone_color_to_selected"
|
||||
bl_label = "Copy Colors to Selected"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
_bone_type_enum = [
|
||||
('EDIT', "Bone", "Copy Bone colors from the active bone to all selected bones"),
|
||||
('POSE', "Pose Bone", "Copy Pose Bone colors from the active pose bone to all selected pose bones"),
|
||||
]
|
||||
|
||||
bone_type: EnumProperty(
|
||||
name="Type",
|
||||
items=_bone_type_enum,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode in {'EDIT_ARMATURE', 'POSE'}
|
||||
|
||||
def execute(self, context):
|
||||
match(self.bone_type, context.mode):
|
||||
# Armature in edit mode:
|
||||
case('POSE', 'EDIT_ARMATURE'):
|
||||
self.report({'ERROR'}, "Go to pose mode to copy pose bone colors")
|
||||
return {'OPERATOR_CANCELLED'}
|
||||
case('EDIT', 'EDIT_ARMATURE'):
|
||||
bone_source = context.active_bone
|
||||
bones_dest = context.selected_bones
|
||||
pose_bones_to_check = []
|
||||
|
||||
# Armature in pose mode:
|
||||
case('POSE', 'POSE'):
|
||||
bone_source = context.active_pose_bone
|
||||
bones_dest = context.selected_pose_bones
|
||||
pose_bones_to_check = []
|
||||
case('EDIT', 'POSE'):
|
||||
bone_source = context.active_bone
|
||||
pose_bones_to_check = context.selected_pose_bones
|
||||
bones_dest = [posebone.bone for posebone in pose_bones_to_check]
|
||||
|
||||
# Anything else:
|
||||
case _:
|
||||
self.report({'ERROR'}, rpt_("Cannot do anything in mode {!r}").format(context.mode))
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not bone_source:
|
||||
self.report({'ERROR'}, "No active bone to copy from")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not bones_dest:
|
||||
self.report({'ERROR'}, "No selected bones to copy to")
|
||||
return {'CANCELLED'}
|
||||
|
||||
num_pose_color_overrides = 0
|
||||
for index, bone_dest in enumerate(bones_dest):
|
||||
bone_dest.color.palette = bone_source.color.palette
|
||||
for custom_field in ("normal", "select", "active"):
|
||||
color = getattr(bone_source.color.custom, custom_field)
|
||||
setattr(bone_dest.color.custom, custom_field, color)
|
||||
|
||||
if self.bone_type == 'EDIT' and pose_bones_to_check:
|
||||
pose_bone = pose_bones_to_check[index]
|
||||
if pose_bone.color.palette != 'DEFAULT':
|
||||
# A pose color has been set, and we're now syncing edit bone
|
||||
# colors. This means that the synced color will not be
|
||||
# visible. Better to let the user know about this.
|
||||
num_pose_color_overrides += 1
|
||||
|
||||
if num_pose_color_overrides:
|
||||
self.report(
|
||||
{'INFO'},
|
||||
rpt_("Bone colors were synced; "
|
||||
"for {:d} bones this will not be visible due to pose bone color overrides").format(
|
||||
num_pose_color_overrides,
|
||||
),
|
||||
)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def _armature_from_context(context):
|
||||
pin_armature = getattr(context, "armature", None)
|
||||
if pin_armature:
|
||||
return pin_armature
|
||||
ob = context.object
|
||||
if ob and ob.type == 'ARMATURE':
|
||||
return ob.data
|
||||
return None
|
||||
|
||||
|
||||
class ARMATURE_OT_collection_show_all(Operator):
|
||||
"""Show all bone collections"""
|
||||
bl_idname = "armature.collection_show_all"
|
||||
bl_label = "Show All"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _armature_from_context(context) is not None
|
||||
|
||||
def execute(self, context):
|
||||
arm = _armature_from_context(context)
|
||||
for bcoll in arm.collections_all:
|
||||
bcoll.is_visible = True
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ARMATURE_OT_collection_unsolo_all(Operator):
|
||||
"""Clear the 'solo' setting on all bone collections"""
|
||||
bl_idname = "armature.collection_unsolo_all"
|
||||
bl_label = "Un-solo All"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = _armature_from_context(context)
|
||||
if not armature:
|
||||
return False
|
||||
if not armature.collections.is_solo_active:
|
||||
cls.poll_message_set("None of the bone collections is marked 'solo'")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
arm = _armature_from_context(context)
|
||||
for bcoll in arm.collections_all:
|
||||
bcoll.is_solo = False
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ARMATURE_OT_collection_remove_unused(Operator):
|
||||
"""Remove all bone collections that have neither bones nor children. """ \
|
||||
"""This is done recursively, so bone collections that only have unused children are also removed"""
|
||||
|
||||
bl_idname = "armature.collection_remove_unused"
|
||||
bl_label = "Remove Unused Bone Collections"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = _armature_from_context(context)
|
||||
if not armature:
|
||||
return False
|
||||
return len(armature.collections) > 0
|
||||
|
||||
def execute(self, context):
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
return self.execute_edit_mode(context)
|
||||
|
||||
armature = _armature_from_context(context)
|
||||
|
||||
# Build a set of bone collections that don't contain any bones, and
|
||||
# whose children also don't contain any bones.
|
||||
bcolls_to_remove = {
|
||||
bcoll
|
||||
for bcoll in armature.collections_all
|
||||
if len(bcoll.bones_recursive) == 0}
|
||||
|
||||
if not bcolls_to_remove:
|
||||
self.report({'INFO'}, "All bone collections are in use")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.remove_bcolls(armature, bcolls_to_remove)
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute_edit_mode(self, context):
|
||||
# BoneCollection.bones_recursive or .bones are not available in armature
|
||||
# edit mode, because that has a completely separate list of edit bones.
|
||||
# This is why edit mode needs separate handling.
|
||||
|
||||
armature = _armature_from_context(context)
|
||||
bcolls_with_bones = {
|
||||
bcoll
|
||||
for ebone in armature.edit_bones
|
||||
for bcoll in ebone.collections
|
||||
}
|
||||
|
||||
bcolls_to_remove = []
|
||||
for root in armature.collections:
|
||||
self.visit(root, bcolls_with_bones, bcolls_to_remove)
|
||||
|
||||
if not bcolls_to_remove:
|
||||
self.report({'INFO'}, "All bone collections are in use")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.remove_bcolls(armature, bcolls_to_remove)
|
||||
return {'FINISHED'}
|
||||
|
||||
def visit(self, bcoll, bcolls_with_bones, bcolls_to_remove):
|
||||
has_bones = bcoll in bcolls_with_bones
|
||||
|
||||
for child in bcoll.children:
|
||||
child_has_bones = self.visit(child, bcolls_with_bones, bcolls_to_remove)
|
||||
has_bones = has_bones or child_has_bones
|
||||
|
||||
if not has_bones:
|
||||
bcolls_to_remove.append(bcoll)
|
||||
|
||||
return has_bones
|
||||
|
||||
def remove_bcolls(self, armature, bcolls_to_remove):
|
||||
# Count things before they get removed.
|
||||
num_bcolls_before_removal = len(armature.collections_all)
|
||||
num_bcolls_to_remove = len(bcolls_to_remove)
|
||||
|
||||
# Create a copy of bcolls_to_remove so that it doesn't change when we
|
||||
# remove bone collections.
|
||||
for bcoll in reversed(list(bcolls_to_remove)):
|
||||
armature.collections.remove(bcoll)
|
||||
|
||||
self.report(
|
||||
{'INFO'},
|
||||
rpt_("Removed {:d} of {:d} bone collections").format(
|
||||
num_bcolls_to_remove,
|
||||
num_bcolls_before_removal),
|
||||
)
|
||||
|
||||
|
||||
class ANIM_OT_slot_new_for_id(Operator):
|
||||
"""Create a new Action Slot for an ID.
|
||||
|
||||
Note that _which_ ID should get this slot must be set in the 'animated_id' context pointer, using:
|
||||
|
||||
>>> layout.context_pointer_set("animated_id", animated_id)
|
||||
|
||||
When the ID already has a slot assigned, the newly-created slot will be
|
||||
named after it (ensuring uniqueness with a numerical suffix) and any
|
||||
animation data of the assigned slot will be duplicated for the new slot.
|
||||
"""
|
||||
bl_idname = "anim.slot_new_for_id"
|
||||
bl_label = "New Slot"
|
||||
bl_description = "Create a new action slot for this data-block, to hold its animation"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
animated_id = getattr(context, "animated_id", None)
|
||||
if not animated_id:
|
||||
return False
|
||||
if not animated_id.animation_data or not animated_id.animation_data.action:
|
||||
cls.poll_message_set("An action slot can only be created when an action is assigned")
|
||||
return False
|
||||
if not animated_id.animation_data.action.is_editable:
|
||||
cls.poll_message_set("Creating a new Slot is not possible on a linked Action")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
animated_id = context.animated_id
|
||||
adt = animated_id.animation_data
|
||||
|
||||
if adt.action_slot:
|
||||
slot = adt.action_slot.duplicate()
|
||||
else:
|
||||
slot_name = adt.last_slot_identifier[2:] or animated_id.name
|
||||
slot = adt.action.slots.new(animated_id.id_type, slot_name)
|
||||
|
||||
adt.action_slot = slot
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ANIM_OT_slot_unassign_from_id(Operator):
|
||||
"""Un-assign the assigned Action Slot from an ID.
|
||||
|
||||
Note that _which_ ID should get this slot unassigned must be set in the
|
||||
"animated_id" context pointer, using:
|
||||
|
||||
>>> layout.context_pointer_set("animated_id", animated_id)
|
||||
"""
|
||||
bl_idname = "anim.slot_unassign_from_id"
|
||||
bl_label = "Unassign Slot"
|
||||
bl_description = "Un-assign the action slot, effectively making this data-block non-animated"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
animated_id = getattr(context, "animated_id", None)
|
||||
if not animated_id:
|
||||
return False
|
||||
if not animated_id.animation_data or not animated_id.animation_data.action_slot:
|
||||
cls.poll_message_set("This data-block has no Action slot assigned")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
animated_id = context.animated_id
|
||||
animated_id.animation_data.action_slot = None
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class generic_slot_unassign_mixin:
|
||||
context_property_name = ""
|
||||
"""Which context attribute to use to get the to-be-manipulated data-block."""
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
slot_user = getattr(context, cls.context_property_name, None)
|
||||
if not slot_user:
|
||||
return False
|
||||
|
||||
if not slot_user.action_slot:
|
||||
cls.poll_message_set("No Action slot is assigned, so there is nothing to un-assign")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
slot_user = getattr(context, self.context_property_name, None)
|
||||
slot_user.action_slot = None
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ANIM_OT_slot_unassign_from_nla_strip(generic_slot_unassign_mixin, Operator):
|
||||
"""Un-assign the assigned Action Slot from an NLA strip.
|
||||
|
||||
Note that _which_ NLA strip should get this slot unassigned must be set in
|
||||
the "nla_strip" context pointer, using:
|
||||
|
||||
>>> layout.context_pointer_set("nla_strip", nla_strip)
|
||||
"""
|
||||
bl_idname = "anim.slot_unassign_from_nla_strip"
|
||||
bl_label = "Unassign Slot"
|
||||
bl_description = "Un-assign the action slot from this NLA strip, effectively making it non-animated"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
context_property_name = "nla_strip"
|
||||
|
||||
|
||||
class ANIM_OT_slot_unassign_from_constraint(generic_slot_unassign_mixin, Operator):
|
||||
"""Un-assign the assigned Action Slot from an Action constraint.
|
||||
|
||||
Note that _which_ constraint should get this slot unassigned must be set in
|
||||
the "constraint" context pointer, using:
|
||||
|
||||
>>> layout.context_pointer_set("constraint", constraint)
|
||||
"""
|
||||
bl_idname = "anim.slot_unassign_from_constraint"
|
||||
bl_label = "Unassign Slot"
|
||||
bl_description = "Un-assign the action slot from this constraint"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
context_property_name = "constraint"
|
||||
|
||||
|
||||
# This is for the versioning from 4.5 to 5.0 and can be removed in 6.0.
|
||||
class ANIM_OT_version_bone_hide_property(Operator):
|
||||
bl_idname = "anim.version_bone_hide_property"
|
||||
bl_label = "Version Bone Hide Property"
|
||||
bl_description = "Moves any F-Curves for the `hide` property of selected armatures " \
|
||||
"into the action of the object. This will only operate on the first layer " \
|
||||
"and strip of the action"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
if len(context.selected_objects) == 0:
|
||||
cls.poll_message_set("No objects selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def find_property_fcurves(channelbag):
|
||||
fcurves = []
|
||||
for fcurve in channelbag.fcurves:
|
||||
if fcurve.data_path.startswith("bones[") and fcurve.data_path.endswith("].hide"):
|
||||
fcurves.append(fcurve)
|
||||
return fcurves
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras import anim_utils
|
||||
selected_armatures = []
|
||||
for arm_ob in context.selected_objects:
|
||||
if arm_ob.type != 'ARMATURE' or not arm_ob.data:
|
||||
continue
|
||||
armature = arm_ob.data
|
||||
assigned_channelbag = anim_utils.animdata_get_channelbag_for_assigned_slot(armature.animation_data)
|
||||
if not assigned_channelbag:
|
||||
# Armature not animated. Cannot have the FCurve we need.
|
||||
continue
|
||||
selected_armatures.append(arm_ob)
|
||||
|
||||
if not selected_armatures:
|
||||
self.report({'WARNING'}, rpt_("No animated armatures selected"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
warn = True
|
||||
modified_armatures = []
|
||||
# The objects also have to be animated -> have an assigned action + slot.
|
||||
# This means we know with certainty which action to move the data into.
|
||||
for arm_ob in selected_armatures:
|
||||
ob_adt = arm_ob.animation_data
|
||||
arm_adt = arm_ob.data.animation_data
|
||||
if warn and (not ob_adt or not ob_adt.action or not ob_adt.action_slot):
|
||||
self.report({'WARNING'}, rpt_("Not all armature objects have an action and slot assigned"))
|
||||
# Only warn once.
|
||||
warn = False
|
||||
continue
|
||||
|
||||
# Only armatures with an action and slot are added to `selected_armatures`.
|
||||
assert arm_adt is not None
|
||||
armature_channelbag = anim_utils.action_get_channelbag_for_slot(arm_adt.action, arm_adt.action_slot)
|
||||
if not armature_channelbag:
|
||||
continue
|
||||
|
||||
fcurves = self.find_property_fcurves(armature_channelbag)
|
||||
|
||||
if not fcurves:
|
||||
# No FCurves for the hide property found.
|
||||
continue
|
||||
|
||||
# An action + slot is assigned, but that doesn't mean there is a layer and a strip.
|
||||
ob_channelbag = anim_utils.action_ensure_channelbag_for_slot(ob_adt.action, ob_adt.action_slot)
|
||||
|
||||
for fcurve in fcurves:
|
||||
new_path = "pose." + fcurve.data_path
|
||||
if ob_channelbag.fcurves.find(new_path):
|
||||
# FCurve for that property already exists.
|
||||
continue
|
||||
|
||||
ob_channelbag.fcurves.new_from_fcurve(fcurve, data_path=new_path)
|
||||
|
||||
modified_armatures.append(arm_ob)
|
||||
|
||||
if not modified_armatures:
|
||||
self.report({'WARNING'}, rpt_("No armature animation was modified"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, rpt_("Modified the animation of {:d} armatures").format(len(modified_armatures)))
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
ANIM_OT_keying_set_export,
|
||||
NLA_OT_bake,
|
||||
ClearUselessActions,
|
||||
UpdateAnimatedTransformConstraint,
|
||||
ARMATURE_OT_copy_bone_color_to_selected,
|
||||
ARMATURE_OT_collection_show_all,
|
||||
ARMATURE_OT_collection_unsolo_all,
|
||||
ARMATURE_OT_collection_remove_unused,
|
||||
ANIM_OT_slot_new_for_id,
|
||||
ANIM_OT_slot_unassign_from_id,
|
||||
ANIM_OT_slot_unassign_from_nla_strip,
|
||||
ANIM_OT_slot_unassign_from_constraint,
|
||||
ANIM_OT_version_bone_hide_property,
|
||||
)
|
||||
200
blender-5.2.0/scripts/startup/bl_operators/assets.py
Normal file
200
blender-5.2.0/scripts/startup/bl_operators/assets.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.app.translations import (
|
||||
pgettext_data as data_,
|
||||
pgettext_rpt as rpt_,
|
||||
)
|
||||
|
||||
|
||||
from bpy_extras.asset_utils import (
|
||||
SpaceAssetInfo,
|
||||
)
|
||||
|
||||
|
||||
class AssetBrowserMetadataOperator:
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not SpaceAssetInfo.is_asset_browser_poll(context) or not context.asset:
|
||||
return False
|
||||
|
||||
if not context.asset.local_id:
|
||||
Operator.poll_message_set(
|
||||
"Asset metadata from external asset libraries cannot be "
|
||||
"edited, only assets stored in the current file can"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ASSET_OT_tag_add(AssetBrowserMetadataOperator, Operator):
|
||||
"""Add a new keyword tag to the active asset"""
|
||||
|
||||
bl_idname = "asset.tag_add"
|
||||
bl_label = "Add Asset Tag"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
active_asset = context.asset
|
||||
active_asset.metadata.tags.new(data_("Tag"))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ASSET_OT_tag_remove(AssetBrowserMetadataOperator, Operator):
|
||||
"""Remove an existing keyword tag from the active asset"""
|
||||
|
||||
bl_idname = "asset.tag_remove"
|
||||
bl_label = "Remove Asset Tag"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
|
||||
active_asset = context.asset
|
||||
asset_metadata = active_asset.metadata
|
||||
return asset_metadata.active_tag in range(len(asset_metadata.tags))
|
||||
|
||||
def execute(self, context):
|
||||
active_asset = context.asset
|
||||
asset_metadata = active_asset.metadata
|
||||
tag = asset_metadata.tags[asset_metadata.active_tag]
|
||||
|
||||
asset_metadata.tags.remove(tag)
|
||||
asset_metadata.active_tag -= 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ASSET_OT_open_containing_blend_file(Operator):
|
||||
"""Open the blend file that contains the active asset"""
|
||||
|
||||
bl_idname = "asset.open_containing_blend_file"
|
||||
bl_label = "Open Blend File"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
_process = None # Optional[subprocess.Popen]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
asset = getattr(context, "asset", None)
|
||||
|
||||
if not asset:
|
||||
cls.poll_message_set("No asset selected")
|
||||
return False
|
||||
if asset.local_id:
|
||||
cls.poll_message_set("Selected asset is contained in the current file")
|
||||
return False
|
||||
if asset.is_online:
|
||||
cls.poll_message_set("Selected asset is stored online")
|
||||
return False
|
||||
if not asset.owner_asset_library.is_editable:
|
||||
cls.poll_message_set(
|
||||
"The asset library this asset belongs to is not editable"
|
||||
)
|
||||
return False
|
||||
# This could become a built-in query, for now this is good enough.
|
||||
if asset.full_library_path.endswith(".asset.blend"):
|
||||
cls.poll_message_set(
|
||||
"Selected asset is contained in a file managed by the asset system, manual edits should be avoided",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
asset = context.asset
|
||||
|
||||
if asset.local_id:
|
||||
self.report({'WARNING'}, "This asset is stored in the current blend file")
|
||||
return {'CANCELLED'}
|
||||
|
||||
asset_lib_path = asset.full_library_path
|
||||
self.open_in_new_blender(asset_lib_path)
|
||||
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(0.1, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type != 'TIMER':
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
if self._process is None:
|
||||
self.report({'ERROR'}, "Unable to find any running process")
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
returncode = self._process.poll()
|
||||
if returncode is None:
|
||||
# Process is still running.
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
if returncode:
|
||||
self.report({'WARNING'}, rpt_("Blender sub-process exited with error code {:d}").format(returncode))
|
||||
|
||||
if bpy.ops.asset.library_refresh.poll():
|
||||
bpy.ops.asset.library_refresh()
|
||||
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
def cancel(self, context):
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
|
||||
def open_in_new_blender(self, filepath):
|
||||
import subprocess
|
||||
|
||||
cli_args = [bpy.app.binary_path, str(filepath)]
|
||||
self._process = subprocess.Popen(cli_args)
|
||||
|
||||
|
||||
class ASSET_OT_browse_containing_blend_file(Operator):
|
||||
"""Open the system's file browser with the blend file that contains the active asset"""
|
||||
|
||||
bl_idname = "asset.browse_containing_blend_file"
|
||||
bl_label = "Open File Location"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
asset = getattr(context, "asset", None)
|
||||
|
||||
if not asset:
|
||||
cls.poll_message_set("No asset selected")
|
||||
return False
|
||||
if asset.local_id and not bpy.data.filepath:
|
||||
cls.poll_message_set("Asset local to the current file, which is not saved anywhere")
|
||||
return False
|
||||
if asset.is_online:
|
||||
cls.poll_message_set("Selected asset is stored online")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
from pathlib import Path
|
||||
|
||||
asset = context.asset
|
||||
|
||||
if asset.local_id:
|
||||
asset_path = Path(bpy.data.filepath)
|
||||
else:
|
||||
asset_path = Path(asset.full_library_path)
|
||||
return bpy.ops.wm.path_open(filepath=str(asset_path.parent))
|
||||
|
||||
|
||||
classes = (
|
||||
ASSET_OT_tag_add,
|
||||
ASSET_OT_tag_remove,
|
||||
ASSET_OT_open_containing_blend_file,
|
||||
ASSET_OT_browse_containing_blend_file,
|
||||
)
|
||||
@@ -0,0 +1,341 @@
|
||||
# SPDX-FileCopyrightText: 2016-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Utilities to detect the next matching element (vert/edge/face)
|
||||
# based on an existing pair of elements.
|
||||
|
||||
import bmesh
|
||||
|
||||
__all__ = (
|
||||
"select_prev",
|
||||
"select_next",
|
||||
)
|
||||
|
||||
|
||||
def other_edges_over_face(e):
|
||||
# Can yield same edge multiple times, its fine.
|
||||
for l in e.link_loops:
|
||||
yield l.link_loop_next.edge
|
||||
yield l.link_loop_prev.edge
|
||||
|
||||
|
||||
def other_edges_over_edge(e):
|
||||
# Can yield same edge multiple times, its fine.
|
||||
for v in e.verts:
|
||||
for e_other in v.link_edges:
|
||||
if e_other is not e:
|
||||
if not e.is_wire:
|
||||
yield e_other
|
||||
|
||||
|
||||
def verts_from_elem(ele):
|
||||
ele_type = type(ele)
|
||||
if ele_type is bmesh.types.BMFace:
|
||||
return [l.vert for l in ele.loops]
|
||||
elif ele_type is bmesh.types.BMEdge:
|
||||
return [v for v in ele.verts]
|
||||
elif ele_type is bmesh.types.BMVert:
|
||||
return [ele]
|
||||
else:
|
||||
raise TypeError("wrong type")
|
||||
|
||||
|
||||
def edges_from_elem(ele):
|
||||
ele_type = type(ele)
|
||||
if ele_type is bmesh.types.BMFace:
|
||||
return [l.edge for l in ele.loops]
|
||||
elif ele_type is bmesh.types.BMEdge:
|
||||
return [ele]
|
||||
elif ele_type is bmesh.types.BMVert:
|
||||
return [e for e in ele.link_edges]
|
||||
else:
|
||||
raise TypeError("wrong type")
|
||||
|
||||
|
||||
def elems_depth_search(ele_init, depths, other_edges_over_cb, results_init=None):
|
||||
"""
|
||||
List of depths -> List of elems that match those depths.
|
||||
"""
|
||||
|
||||
depth_max = max(depths)
|
||||
depth_min = min(depths)
|
||||
depths_sorted = tuple(sorted(depths))
|
||||
|
||||
stack_old = edges_from_elem(ele_init)
|
||||
stack_new = []
|
||||
|
||||
stack_visit = set(stack_old)
|
||||
|
||||
vert_depths = {}
|
||||
vert_depths_setdefault = vert_depths.setdefault
|
||||
|
||||
depth = 0
|
||||
while stack_old and depth <= depth_max:
|
||||
for ele in stack_old:
|
||||
for v in verts_from_elem(ele):
|
||||
vert_depths_setdefault(v, depth)
|
||||
for ele_other in other_edges_over_cb(ele):
|
||||
stack_visit_len = len(stack_visit)
|
||||
stack_visit.add(ele_other)
|
||||
if stack_visit_len != len(stack_visit):
|
||||
stack_new.append(ele_other)
|
||||
stack_new, stack_old = stack_old, stack_new
|
||||
stack_new[:] = []
|
||||
depth += 1
|
||||
|
||||
# now we have many verts in vert_depths which are attached to elements
|
||||
# which are candidates for matching with depths
|
||||
if type(ele_init) is bmesh.types.BMFace:
|
||||
test_ele = {
|
||||
l.face for v, depth in vert_depths.items()
|
||||
if depth >= depth_min for l in v.link_loops
|
||||
}
|
||||
elif type(ele_init) is bmesh.types.BMEdge:
|
||||
test_ele = {
|
||||
e for v, depth in vert_depths.items()
|
||||
if depth >= depth_min for e in v.link_edges if not e.is_wire
|
||||
}
|
||||
else:
|
||||
test_ele = {
|
||||
v for v, depth in vert_depths.items()
|
||||
if depth >= depth_min
|
||||
}
|
||||
|
||||
result_ele = set()
|
||||
|
||||
vert_depths_get = vert_depths.get
|
||||
# re-used each time, will always be the same length
|
||||
depths_test = [None] * len(depths)
|
||||
|
||||
for ele in test_ele:
|
||||
verts_test = verts_from_elem(ele)
|
||||
if len(verts_test) != len(depths):
|
||||
continue
|
||||
if results_init is not None and ele not in results_init:
|
||||
continue
|
||||
if ele in result_ele:
|
||||
continue
|
||||
|
||||
ok = True
|
||||
for i, v in enumerate(verts_test):
|
||||
depth = vert_depths_get(v)
|
||||
if depth is not None:
|
||||
depths_test[i] = depth
|
||||
else:
|
||||
ok = False
|
||||
break
|
||||
|
||||
if ok:
|
||||
if depths_sorted == tuple(sorted(depths_test)):
|
||||
# Note, its possible the order of sorted items moves the values out-of-order.
|
||||
# for this we could do a circular list comparison,
|
||||
# however - this is such a rare case that we're ignoring it.
|
||||
result_ele.add(ele)
|
||||
|
||||
return result_ele
|
||||
|
||||
|
||||
def elems_depth_measure(ele_dst, ele_src, other_edges_over_cb):
|
||||
"""
|
||||
Returns·ele_dst vert depths from ele_src, aligned with ele_dst verts.
|
||||
"""
|
||||
|
||||
stack_old = edges_from_elem(ele_src)
|
||||
stack_new = []
|
||||
|
||||
stack_visit = set(stack_old)
|
||||
|
||||
# continue until we've reached all verts in the destination
|
||||
ele_dst_verts = verts_from_elem(ele_dst)
|
||||
all_dst = set(ele_dst_verts)
|
||||
all_dst_discard = all_dst.discard
|
||||
|
||||
vert_depths = {}
|
||||
|
||||
depth = 0
|
||||
while stack_old and all_dst:
|
||||
for ele in stack_old:
|
||||
for v in verts_from_elem(ele):
|
||||
len_prev = len(all_dst)
|
||||
all_dst_discard(v)
|
||||
if len_prev != len(all_dst):
|
||||
vert_depths[v] = depth
|
||||
|
||||
for ele_other in other_edges_over_cb(ele):
|
||||
stack_visit_len = len(stack_visit)
|
||||
stack_visit.add(ele_other)
|
||||
if stack_visit_len != len(stack_visit):
|
||||
stack_new.append(ele_other)
|
||||
stack_new, stack_old = stack_old, stack_new
|
||||
stack_new[:] = []
|
||||
depth += 1
|
||||
|
||||
if not all_dst:
|
||||
return [vert_depths[v] for v in ele_dst_verts]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def find_next(ele_dst, ele_src):
|
||||
depth_src_a = elems_depth_measure(ele_dst, ele_src, other_edges_over_edge)
|
||||
depth_src_b = elems_depth_measure(ele_dst, ele_src, other_edges_over_face)
|
||||
|
||||
# path not found
|
||||
if depth_src_a is None or depth_src_b is None:
|
||||
return []
|
||||
|
||||
depth_src = tuple(zip(depth_src_a, depth_src_b))
|
||||
|
||||
candidates = elems_depth_search(ele_dst, depth_src_a, other_edges_over_edge)
|
||||
candidates = elems_depth_search(ele_dst, depth_src_b, other_edges_over_face, candidates)
|
||||
candidates.discard(ele_src)
|
||||
candidates.discard(ele_dst)
|
||||
if not candidates:
|
||||
return []
|
||||
|
||||
# Now we have to pick which is the best next-element,
|
||||
# do this by calculating the element with the largest
|
||||
# variation in depth from the relationship to the source.
|
||||
# ... So we have the highest chance of stepping onto the opposite element.
|
||||
diff_best = 0
|
||||
ele_best = None
|
||||
ele_best_ls = []
|
||||
for ele_test in candidates:
|
||||
depth_test_a = elems_depth_measure(ele_dst, ele_test, other_edges_over_edge)
|
||||
depth_test_b = elems_depth_measure(ele_dst, ele_test, other_edges_over_face)
|
||||
if depth_test_a is None or depth_test_b is None:
|
||||
continue
|
||||
depth_test = tuple(zip(depth_test_a, depth_test_b))
|
||||
# square so a few high values win over many small ones
|
||||
diff_test = sum(
|
||||
(abs(a[0] - b[0]) ** 2) +
|
||||
(abs(a[1] - b[1]) ** 2) for a, b in zip(depth_src, depth_test)
|
||||
)
|
||||
if diff_test > diff_best:
|
||||
diff_best = diff_test
|
||||
ele_best = ele_test
|
||||
ele_best_ls[:] = [ele_best]
|
||||
elif diff_test == diff_best:
|
||||
if ele_best is None:
|
||||
ele_best = ele_test
|
||||
ele_best_ls.append(ele_test)
|
||||
|
||||
if len(ele_best_ls) > 1:
|
||||
ele_best_ls_init = ele_best_ls
|
||||
ele_best_ls = []
|
||||
depth_accum_max = -1
|
||||
for ele_test in ele_best_ls_init:
|
||||
depth_test_a = elems_depth_measure(ele_src, ele_test, other_edges_over_edge)
|
||||
depth_test_b = elems_depth_measure(ele_src, ele_test, other_edges_over_face)
|
||||
if depth_test_a is None or depth_test_b is None:
|
||||
continue
|
||||
depth_accum_test = (
|
||||
sum(depth_test_a) + sum(depth_test_b))
|
||||
|
||||
if depth_accum_test > depth_accum_max:
|
||||
depth_accum_max = depth_accum_test
|
||||
ele_best = ele_test
|
||||
ele_best_ls[:] = [ele_best]
|
||||
elif depth_accum_test == depth_accum_max:
|
||||
# we have multiple bests, don't return any
|
||||
ele_best_ls.append(ele_test)
|
||||
|
||||
return ele_best_ls
|
||||
|
||||
|
||||
# expose for operators
|
||||
def select_next(bm, report):
|
||||
ele_pair = [None, None]
|
||||
for i, ele in enumerate(reversed(bm.select_history)):
|
||||
ele_pair[i] = ele
|
||||
if i == 1:
|
||||
break
|
||||
|
||||
if ele_pair[-1] is None:
|
||||
report({'INFO'}, "Selection pair not found")
|
||||
return False
|
||||
|
||||
ele_pair_next = find_next(*ele_pair)
|
||||
|
||||
if len(ele_pair_next) > 1:
|
||||
# We have multiple options,
|
||||
# check topology around the element and find the closest match
|
||||
# (allow for sloppy comparison if exact checks fail).
|
||||
|
||||
def ele_uuid(ele):
|
||||
ele_type = type(ele)
|
||||
if ele_type is bmesh.types.BMFace:
|
||||
ret = [len(f.verts) for l in ele.loops for f in l.edge.link_faces if f is not ele]
|
||||
elif ele_type is bmesh.types.BMEdge:
|
||||
ret = [len(l.face.verts) for l in ele.link_loops]
|
||||
elif ele_type is bmesh.types.BMVert:
|
||||
ret = [len(l.face.verts) for l in ele.link_loops]
|
||||
else:
|
||||
raise TypeError("wrong type")
|
||||
return tuple(sorted(ret))
|
||||
|
||||
def ele_uuid_filter():
|
||||
|
||||
def pass_fn(seq):
|
||||
return seq
|
||||
|
||||
def sum_set(seq):
|
||||
return sum(set(seq))
|
||||
|
||||
uuid_cmp = ele_uuid(ele_pair[0])
|
||||
ele_pair_next_uuid = [(ele, ele_uuid(ele)) for ele in ele_pair_next]
|
||||
|
||||
# Attempt to find the closest match,
|
||||
# start specific, use increasingly more approximate comparisons.
|
||||
for fn in (pass_fn, set, sum_set, len):
|
||||
uuid_cmp_test = fn(uuid_cmp)
|
||||
ele_pair_next_uuid_test = [
|
||||
(ele, uuid) for (ele, uuid) in ele_pair_next_uuid
|
||||
if uuid_cmp_test == fn(uuid)
|
||||
]
|
||||
if len(ele_pair_next_uuid_test) > 1:
|
||||
ele_pair_next_uuid = ele_pair_next_uuid_test
|
||||
elif len(ele_pair_next_uuid_test) == 1:
|
||||
return [ele for (ele, uuid) in ele_pair_next_uuid_test]
|
||||
return []
|
||||
|
||||
ele_pair_next[:] = ele_uuid_filter()
|
||||
|
||||
del ele_uuid, ele_uuid_filter
|
||||
|
||||
if len(ele_pair_next) != 1:
|
||||
report({'INFO'}, "No single next item found")
|
||||
return False
|
||||
|
||||
ele = ele_pair_next[0]
|
||||
if ele.hide:
|
||||
report({'INFO'}, "Next element is hidden")
|
||||
return False
|
||||
|
||||
ele.select_set(False)
|
||||
ele.select_set(True)
|
||||
bm.select_history.discard(ele)
|
||||
bm.select_history.add(ele)
|
||||
if type(ele) is bmesh.types.BMFace:
|
||||
bm.faces.active = ele
|
||||
return True
|
||||
|
||||
|
||||
def select_prev(bm, report):
|
||||
import bmesh
|
||||
for ele in reversed(bm.select_history):
|
||||
break
|
||||
else:
|
||||
report({'INFO'}, "Last selected not found")
|
||||
return False
|
||||
|
||||
ele.select_set(False)
|
||||
|
||||
for i, ele in enumerate(reversed(bm.select_history)):
|
||||
if i == 1:
|
||||
if type(ele) is bmesh.types.BMFace:
|
||||
bm.faces.active = ele
|
||||
break
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,443 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
PropertyGroup,
|
||||
)
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
IntProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
# Note: The UI classes are stored in bl_ui/properties_data_armature.py
|
||||
|
||||
# Data Structure ##############################################################
|
||||
|
||||
# Note: bones are stored by name, this means that if the bone is renamed,
|
||||
# there can be problems. However, bone renaming is unlikely during animation.
|
||||
|
||||
|
||||
class SelectionEntry(PropertyGroup):
|
||||
__slots__ = ()
|
||||
|
||||
name: StringProperty(name="Bone Name", override={'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
|
||||
class SelectionSet(PropertyGroup):
|
||||
__slots__ = ()
|
||||
|
||||
name: StringProperty(name="Set Name", override={'LIBRARY_OVERRIDABLE'})
|
||||
bone_ids: CollectionProperty(
|
||||
type=SelectionEntry,
|
||||
override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}
|
||||
)
|
||||
is_selected: BoolProperty(
|
||||
name="Include this selection set when copying to the clipboard. "
|
||||
"If none are specified, all sets will be copied.",
|
||||
override={'LIBRARY_OVERRIDABLE'},
|
||||
)
|
||||
|
||||
|
||||
# Operators ##############################################################
|
||||
|
||||
class _PoseModeOnlyMixin:
|
||||
"""Operator only available for objects of type armature in pose mode."""
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.object and
|
||||
context.object.type == 'ARMATURE' and
|
||||
context.mode == 'POSE'
|
||||
)
|
||||
|
||||
|
||||
class _NeedSelSetMixin(_PoseModeOnlyMixin):
|
||||
"""Operator only available if the armature has a selected selection set."""
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
arm = context.object
|
||||
return 0 <= arm.active_selection_set < len(arm.selection_sets)
|
||||
|
||||
|
||||
class POSE_OT_selection_set_delete_all(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_delete_all"
|
||||
bl_label = "Delete All Sets"
|
||||
bl_description = "Remove all Selection Sets from this Armature"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
arm.selection_sets.clear()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_remove_bones(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_remove_bones"
|
||||
bl_label = "Remove Selected Bones from All Sets"
|
||||
bl_description = "Remove the selected bones from all Selection Sets"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
|
||||
# Iterate only the selected bones in current pose that are not hidden.
|
||||
for bone in context.selected_pose_bones:
|
||||
for selset in arm.selection_sets:
|
||||
if bone.name in selset.bone_ids:
|
||||
idx = selset.bone_ids.find(bone.name)
|
||||
selset.bone_ids.remove(idx)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_move(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_move"
|
||||
bl_label = "Move Selection Set in List"
|
||||
bl_description = "Move the active Selection Set up/down the list of sets"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
direction: EnumProperty(
|
||||
name="Move Direction",
|
||||
description="Direction to move the active Selection Set: UP (default) or DOWN",
|
||||
items=[
|
||||
('UP', "Up", "", -1),
|
||||
('DOWN', "Down", "", 1),
|
||||
],
|
||||
default='UP',
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
arm = context.object
|
||||
return len(arm.selection_sets) > 1
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
|
||||
active_idx = arm.active_selection_set
|
||||
new_idx = active_idx + (-1 if self.direction == 'UP' else 1)
|
||||
|
||||
if new_idx < 0 or new_idx >= len(arm.selection_sets):
|
||||
return {'FINISHED'}
|
||||
|
||||
arm.selection_sets.move(active_idx, new_idx)
|
||||
arm.active_selection_set = new_idx
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_add(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_add"
|
||||
bl_label = "Create Selection Set"
|
||||
bl_description = "Create a new empty Selection Set"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
sel_sets = arm.selection_sets
|
||||
new_sel_set = sel_sets.add()
|
||||
new_sel_set.name = _uniqify("SelectionSet", sel_sets.keys())
|
||||
|
||||
# Select newly created set.
|
||||
arm.active_selection_set = len(sel_sets) - 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_remove(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_remove"
|
||||
bl_label = "Delete Selection Set"
|
||||
bl_description = "Remove a Selection Set from this Armature"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
|
||||
arm.selection_sets.remove(arm.active_selection_set)
|
||||
|
||||
# Change currently active selection set.
|
||||
numsets = len(arm.selection_sets)
|
||||
if (arm.active_selection_set > (numsets - 1) and numsets > 0):
|
||||
arm.active_selection_set = len(arm.selection_sets) - 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_assign(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_assign"
|
||||
bl_label = "Add Bones to Selection Set"
|
||||
bl_description = "Add selected bones to Selection Set"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
arm = context.object
|
||||
|
||||
if not (arm.active_selection_set < len(arm.selection_sets)):
|
||||
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name="POSE_MT_selection_set_create")
|
||||
else:
|
||||
bpy.ops.pose.selection_set_assign('EXEC_DEFAULT')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
act_sel_set = arm.selection_sets[arm.active_selection_set]
|
||||
|
||||
# Iterate only the selected bones in current pose that are not hidden.
|
||||
for bone in context.selected_pose_bones:
|
||||
if bone.name not in act_sel_set.bone_ids:
|
||||
bone_id = act_sel_set.bone_ids.add()
|
||||
bone_id.name = bone.name
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_unassign(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_unassign"
|
||||
bl_label = "Remove Bones from Selection Set"
|
||||
bl_description = "Remove selected bones from Selection Set"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
act_sel_set = arm.selection_sets[arm.active_selection_set]
|
||||
|
||||
# Iterate only the selected bones in current pose that are not hidden.
|
||||
for bone in context.selected_pose_bones:
|
||||
if bone.name in act_sel_set.bone_ids:
|
||||
idx = act_sel_set.bone_ids.find(bone.name)
|
||||
act_sel_set.bone_ids.remove(idx)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_select(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_select"
|
||||
bl_label = "Select Selection Set"
|
||||
bl_description = "Select the bones from this Selection Set"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
selection_set_index: IntProperty(
|
||||
name="Selection Set Index",
|
||||
default=-1,
|
||||
description="Which Selection Set to select; -1 uses the active Selection Set",
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
|
||||
if self.selection_set_index == -1:
|
||||
idx = arm.active_selection_set
|
||||
else:
|
||||
idx = self.selection_set_index
|
||||
sel_set = arm.selection_sets[idx]
|
||||
|
||||
for bone in context.visible_pose_bones:
|
||||
if bone.name in sel_set.bone_ids:
|
||||
bone.select = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_deselect(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_deselect"
|
||||
bl_label = "Deselect Selection Set"
|
||||
bl_description = "Remove Selection Set bones from current selection"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
arm = context.object
|
||||
act_sel_set = arm.selection_sets[arm.active_selection_set]
|
||||
|
||||
for bone in context.selected_pose_bones:
|
||||
if bone.name in act_sel_set.bone_ids:
|
||||
bone.select = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_add_and_assign(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_add_and_assign"
|
||||
bl_label = "Create and Add Bones to Selection Set"
|
||||
bl_description = "Create a new Selection Set with the currently selected bones"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.pose.selection_set_add('EXEC_DEFAULT')
|
||||
bpy.ops.pose.selection_set_assign('EXEC_DEFAULT')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_copy(_NeedSelSetMixin, Operator):
|
||||
bl_idname = "pose.selection_set_copy"
|
||||
bl_label = "Copy Selection Set(s)"
|
||||
bl_description = "Copy the selected Selection Set(s) to the clipboard"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
context.window_manager.clipboard = _to_json(context)
|
||||
self.report({'INFO'}, "Copied Selection Set(s) to clipboard")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_selection_set_paste(_PoseModeOnlyMixin, Operator):
|
||||
bl_idname = "pose.selection_set_paste"
|
||||
bl_label = "Paste Selection Set(s)"
|
||||
bl_description = "Add new Selection Set(s) from the clipboard"
|
||||
bl_options = {'UNDO', 'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
import json
|
||||
|
||||
try:
|
||||
_from_json(context, context.window_manager.clipboard)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
self.report({'ERROR'}, "The clipboard does not contain a Selection Set")
|
||||
else:
|
||||
# Select the pasted Selection Set.
|
||||
context.object.active_selection_set = len(context.object.selection_sets) - 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def _uniqify(name, other_names):
|
||||
# :param name: The name to make unique.
|
||||
# :type name: str
|
||||
# :param other_names: The name to make unique.
|
||||
# :type other_names: str
|
||||
# :return: Return a unique name with ``.xxx`` suffix if necessary.
|
||||
# :rtype: str
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# >>> _uniqify('hey', ['there'])
|
||||
# 'hey'
|
||||
# >>> _uniqify('hey', ['hey.001', 'hey.005'])
|
||||
# 'hey'
|
||||
# >>> _uniqify('hey', ['hey', 'hey.001', 'hey.005'])
|
||||
# 'hey.002'
|
||||
# >>> _uniqify('hey', ['hey', 'hey.005', 'hey.001'])
|
||||
# 'hey.002'
|
||||
# >>> _uniqify('hey', ['hey', 'hey.005', 'hey.001', 'hey.left'])
|
||||
# 'hey.002'
|
||||
# >>> _uniqify('hey', ['hey', 'hey.001', 'hey.002'])
|
||||
# 'hey.003'
|
||||
#
|
||||
# It also works with a dict_keys object:
|
||||
# >>> _uniqify('hey', {'hey': 1, 'hey.005': 1, 'hey.001': 1}.keys())
|
||||
# 'hey.002'
|
||||
|
||||
if name not in other_names:
|
||||
return name
|
||||
|
||||
# Construct the list of numbers already in use.
|
||||
offset = len(name) + 1
|
||||
others = (
|
||||
n[offset:] for n in other_names
|
||||
if n.startswith(name + '.')
|
||||
)
|
||||
numbers = sorted(
|
||||
int(suffix) for suffix in others
|
||||
if suffix.isdigit()
|
||||
)
|
||||
|
||||
# Find the first unused number.
|
||||
min_index = 1
|
||||
for num in numbers:
|
||||
if min_index < num:
|
||||
break
|
||||
min_index = num + 1
|
||||
return "{:s}.{:03d}".format(name, min_index)
|
||||
|
||||
|
||||
def _to_json(context):
|
||||
# Convert the selected Selection Sets of the current rig to JSON.
|
||||
#
|
||||
# Selected Sets are the active_selection_set determined by the UIList
|
||||
# plus any with the is_selected checkbox on.
|
||||
#
|
||||
# :return: The selection as JSON data.
|
||||
# :rtype: str
|
||||
import json
|
||||
|
||||
arm = context.object
|
||||
active_idx = arm.active_selection_set
|
||||
|
||||
json_obj = {}
|
||||
for idx, sel_set in enumerate(context.object.selection_sets):
|
||||
if idx == active_idx or sel_set.is_selected:
|
||||
bones = [bone_id.name for bone_id in sel_set.bone_ids]
|
||||
json_obj[sel_set.name] = bones
|
||||
|
||||
return json.dumps(json_obj)
|
||||
|
||||
|
||||
def _from_json(context, as_json):
|
||||
# Add the selection sets (one or more) from JSON to the current rig.
|
||||
#
|
||||
# :param as_json: The JSON contents to load.
|
||||
# :type as_json: str
|
||||
import json
|
||||
|
||||
json_obj = json.loads(as_json)
|
||||
arm_sel_sets = context.object.selection_sets
|
||||
|
||||
for name, bones in json_obj.items():
|
||||
new_sel_set = arm_sel_sets.add()
|
||||
new_sel_set.name = _uniqify(name, arm_sel_sets.keys())
|
||||
for bone_name in bones:
|
||||
bone_id = new_sel_set.bone_ids.add()
|
||||
bone_id.name = bone_name
|
||||
|
||||
|
||||
# Registry ####################################################################
|
||||
|
||||
classes = (
|
||||
SelectionEntry,
|
||||
SelectionSet,
|
||||
POSE_OT_selection_set_delete_all,
|
||||
POSE_OT_selection_set_remove_bones,
|
||||
POSE_OT_selection_set_move,
|
||||
POSE_OT_selection_set_add,
|
||||
POSE_OT_selection_set_remove,
|
||||
POSE_OT_selection_set_assign,
|
||||
POSE_OT_selection_set_unassign,
|
||||
POSE_OT_selection_set_select,
|
||||
POSE_OT_selection_set_deselect,
|
||||
POSE_OT_selection_set_add_and_assign,
|
||||
POSE_OT_selection_set_copy,
|
||||
POSE_OT_selection_set_paste,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.selection_sets = CollectionProperty(
|
||||
type=SelectionSet,
|
||||
name="Selection Sets",
|
||||
description="List of groups of bones for easy selection",
|
||||
override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}
|
||||
)
|
||||
bpy.types.Object.active_selection_set = IntProperty(
|
||||
name="Active Selection Set",
|
||||
description="Index of the currently active selection set",
|
||||
default=0,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Object.selection_sets
|
||||
del bpy.types.Object.active_selection_set
|
||||
1034
blender-5.2.0/scripts/startup/bl_operators/clip.py
Normal file
1034
blender-5.2.0/scripts/startup/bl_operators/clip.py
Normal file
File diff suppressed because it is too large
Load Diff
360
blender-5.2.0/scripts/startup/bl_operators/connect_to_output.py
Normal file
360
blender-5.2.0/scripts/startup/bl_operators/connect_to_output.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# SPDX-FileCopyrightText: 2013-2024 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.app.translations import pgettext_data as data_
|
||||
|
||||
from .node_editor.node_functions import (
|
||||
NodeEditorBase,
|
||||
node_editor_poll,
|
||||
node_space_type_poll,
|
||||
get_group_output_node,
|
||||
get_output_location,
|
||||
get_internal_socket,
|
||||
is_visible_socket,
|
||||
is_viewer_link,
|
||||
force_update,
|
||||
)
|
||||
|
||||
|
||||
class NODE_OT_connect_to_output(Operator, NodeEditorBase):
|
||||
bl_idname = "node.connect_to_output"
|
||||
bl_label = "Connect to Output"
|
||||
bl_description = "Connect active node to the active output node of the node tree"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
# If false, the operator is not executed if the current node group happens to be a geometry nodes group.
|
||||
# This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
|
||||
run_in_geometry_nodes: BoolProperty(
|
||||
name="Run in Geometry Nodes Editor",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.shader_output_idname = ""
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Already implemented natively for compositing nodes."""
|
||||
return (node_editor_poll(cls, context) and
|
||||
node_space_type_poll(cls, context, {'ShaderNodeTree', 'GeometryNodeTree'}))
|
||||
|
||||
@staticmethod
|
||||
def get_output_sockets(node_tree):
|
||||
return [item for item in node_tree.interface.items_tree
|
||||
if item.item_type == 'SOCKET' and item.in_out == 'OUTPUT']
|
||||
|
||||
def init_shader_variables(self, space, shader_type):
|
||||
"""Get correct output node in shader editor"""
|
||||
if shader_type == 'OBJECT':
|
||||
if space.id in bpy.data.lights.values():
|
||||
self.shader_output_idname = 'ShaderNodeOutputLight'
|
||||
else:
|
||||
self.shader_output_idname = 'ShaderNodeOutputMaterial'
|
||||
elif shader_type == 'WORLD':
|
||||
self.shader_output_idname = 'ShaderNodeOutputWorld'
|
||||
|
||||
def ensure_viewer_socket(self, node_tree, socket_type, connect_socket=None):
|
||||
"""Check if a viewer output already exists in a node group, otherwise create it"""
|
||||
viewer_socket = None
|
||||
output_sockets = self.get_output_sockets(node_tree)
|
||||
if len(output_sockets):
|
||||
for i, socket in enumerate(output_sockets):
|
||||
if socket.is_inspect_output:
|
||||
# If viewer output is already used but leads to the same socket we can still use it.
|
||||
is_used = self.has_socket_other_users(socket)
|
||||
if is_used:
|
||||
if connect_socket is None:
|
||||
continue
|
||||
groupout = get_group_output_node(node_tree)
|
||||
groupout_input = groupout.inputs[i]
|
||||
links = groupout_input.links
|
||||
if connect_socket not in [link.from_socket for link in links]:
|
||||
continue
|
||||
viewer_socket = socket
|
||||
break
|
||||
|
||||
if viewer_socket is None:
|
||||
# Create viewer socket.
|
||||
viewer_socket = node_tree.interface.new_socket(
|
||||
data_("(Viewer)"), in_out='OUTPUT', socket_type=socket_type)
|
||||
viewer_socket.is_inspect_output = True
|
||||
return viewer_socket
|
||||
|
||||
@staticmethod
|
||||
def ensure_group_output(node_tree):
|
||||
"""Check if a group output node exists, otherwise create it"""
|
||||
groupout = get_group_output_node(node_tree)
|
||||
if groupout is None:
|
||||
groupout = node_tree.nodes.new('NodeGroupOutput')
|
||||
loc_x, loc_y = get_output_location(node_tree)
|
||||
groupout.location.x = loc_x
|
||||
groupout.location.y = loc_y
|
||||
groupout.select = False
|
||||
# So that we don't keep on adding new group outputs.
|
||||
groupout.is_active_output = True
|
||||
return groupout
|
||||
|
||||
@classmethod
|
||||
def search_connected_viewer_sockets(cls, output_node, r_sockets, index=None):
|
||||
"""From an output node, recursively scan node tree for connected viewer sockets"""
|
||||
for i, input_socket in enumerate(output_node.inputs):
|
||||
if index and i != index:
|
||||
continue
|
||||
if len(input_socket.links):
|
||||
link = input_socket.links[0]
|
||||
next_node = link.from_node
|
||||
external_socket = link.from_socket
|
||||
if hasattr(next_node, "node_tree"):
|
||||
for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
|
||||
# Find inside socket matching outside one.
|
||||
if socket.identifier == external_socket.identifier:
|
||||
break
|
||||
if socket.is_inspect_output and socket not in r_sockets:
|
||||
r_sockets.append(socket)
|
||||
# Continue search inside of node group but restrict socket to where we came from.
|
||||
groupout = get_group_output_node(next_node.node_tree)
|
||||
cls.search_connected_viewer_sockets(groupout, r_sockets, index=socket_index)
|
||||
|
||||
@classmethod
|
||||
def search_viewer_sockets_in_tree(cls, tree, r_sockets):
|
||||
"""Recursively get all viewer sockets in a node tree"""
|
||||
for node in tree.nodes:
|
||||
if hasattr(node, "node_tree"):
|
||||
if node.node_tree is None:
|
||||
continue
|
||||
for socket in cls.get_output_sockets(node.node_tree):
|
||||
if socket.is_inspect_output and (socket not in r_sockets):
|
||||
r_sockets.append(socket)
|
||||
cls.search_viewer_sockets_in_tree(node.node_tree, r_sockets)
|
||||
|
||||
@staticmethod
|
||||
def remove_socket(tree, socket):
|
||||
interface = tree.interface
|
||||
interface.remove(socket)
|
||||
interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
|
||||
|
||||
def link_leads_to_used_socket(self, link):
|
||||
"""Return True if link leads to a socket that is already used in this node"""
|
||||
socket = get_internal_socket(link.to_socket)
|
||||
return socket and self.is_socket_used_active_tree(socket)
|
||||
|
||||
def is_socket_used_active_tree(self, socket):
|
||||
"""Ensure used sockets in active node tree is calculated and check given socket"""
|
||||
if not hasattr(self, "used_viewer_sockets_active_mat"):
|
||||
self.used_viewer_sockets_active_mat = []
|
||||
|
||||
node_tree = bpy.context.space_data.node_tree
|
||||
output_node = None
|
||||
if node_tree.type == 'GEOMETRY':
|
||||
output_node = get_group_output_node(node_tree)
|
||||
elif node_tree.type == 'SHADER':
|
||||
output_node = get_group_output_node(node_tree, output_node_idname=self.shader_output_idname)
|
||||
|
||||
if output_node is not None:
|
||||
self.search_connected_viewer_sockets(output_node, self.used_viewer_sockets_active_mat)
|
||||
return socket in self.used_viewer_sockets_active_mat
|
||||
|
||||
def has_socket_other_users(self, socket):
|
||||
"""List the other users for this socket (other materials or geometry nodes groups)"""
|
||||
if not hasattr(self, "other_viewer_sockets_users"):
|
||||
self.other_viewer_sockets_users = []
|
||||
if socket.socket_type == 'NodeSocketGeometry':
|
||||
# This operator can only preview Geometry sockets for geometry nodes,
|
||||
# so the rest of them are shader nodes.
|
||||
for obj in bpy.data.objects:
|
||||
for mod in obj.modifiers:
|
||||
if mod.type != 'NODES' or mod.node_group == bpy.context.space_data.node_tree:
|
||||
continue
|
||||
# Get viewer node.
|
||||
output_node = get_group_output_node(mod.node_group)
|
||||
if output_node is not None:
|
||||
self.search_connected_viewer_sockets(output_node, self.other_viewer_sockets_users)
|
||||
else:
|
||||
for mat in bpy.data.materials:
|
||||
if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
|
||||
continue
|
||||
# Get viewer node.
|
||||
output_node = get_group_output_node(
|
||||
mat.node_tree,
|
||||
output_node_idname=self.shader_output_idname,
|
||||
)
|
||||
if output_node is not None:
|
||||
self.search_connected_viewer_sockets(output_node, self.other_viewer_sockets_users)
|
||||
return socket in self.other_viewer_sockets_users
|
||||
|
||||
def get_output_index(self, node, output_node, is_base_node_tree, socket_type, check_type=False):
|
||||
"""Get the next available output socket in the active node"""
|
||||
out_i = None
|
||||
valid_outputs = []
|
||||
for i, out in enumerate(node.outputs):
|
||||
if out.select:
|
||||
return i
|
||||
if is_visible_socket(out) and (not check_type or out.type == socket_type):
|
||||
valid_outputs.append(i)
|
||||
if valid_outputs:
|
||||
out_i = valid_outputs[0] # Start index of node's outputs.
|
||||
for i, valid_i in enumerate(valid_outputs):
|
||||
for out_link in node.outputs[valid_i].links:
|
||||
if is_viewer_link(out_link, output_node):
|
||||
if is_base_node_tree or self.link_leads_to_used_socket(out_link):
|
||||
if i < len(valid_outputs) - 1:
|
||||
out_i = valid_outputs[i + 1]
|
||||
else:
|
||||
out_i = valid_outputs[0]
|
||||
return out_i
|
||||
|
||||
def create_links(self, path, node, active_node_socket_id, socket_type):
|
||||
"""Create links at each step in the node group path."""
|
||||
from bpy_extras.node_utils import connect_sockets
|
||||
|
||||
path = list(reversed(path))
|
||||
# Starting from the level of the active node.
|
||||
for path_index, path_element in enumerate(path[:-1]):
|
||||
# Ensure there is a viewer node and it has an input.
|
||||
tree = path_element.node_tree
|
||||
viewer_socket = self.ensure_viewer_socket(
|
||||
tree, socket_type,
|
||||
connect_socket=node.outputs[active_node_socket_id]
|
||||
if path_index == 0 else None,
|
||||
)
|
||||
if viewer_socket in self.delete_sockets:
|
||||
self.delete_sockets.remove(viewer_socket)
|
||||
|
||||
# Connect the current to its viewer.
|
||||
link_start = node.outputs[active_node_socket_id]
|
||||
link_end = self.ensure_group_output(tree).inputs[viewer_socket.identifier]
|
||||
connect_sockets(link_start, link_end)
|
||||
|
||||
# Go up in the node group hierarchy.
|
||||
next_tree = path[path_index + 1].node_tree
|
||||
node = next(
|
||||
n for n in next_tree.nodes
|
||||
if n.type == 'GROUP' and
|
||||
n.node_tree == tree
|
||||
)
|
||||
tree = next_tree
|
||||
active_node_socket_id = viewer_socket.identifier
|
||||
return node.outputs[active_node_socket_id]
|
||||
|
||||
def cleanup(self):
|
||||
# Delete sockets.
|
||||
for socket in self.delete_sockets:
|
||||
if not self.has_socket_other_users(socket):
|
||||
tree = socket.id_data
|
||||
self.remove_socket(tree, socket)
|
||||
|
||||
def invoke(self, context, event):
|
||||
from bpy_extras.node_utils import (
|
||||
find_base_socket_type,
|
||||
connect_sockets,
|
||||
)
|
||||
|
||||
space = context.space_data
|
||||
# Ignore operator when running in wrong context.
|
||||
if self.run_in_geometry_nodes != (space.tree_type == 'GeometryNodeTree'):
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
mlocx = event.mouse_region_x
|
||||
mlocy = event.mouse_region_y
|
||||
select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False, socket_select=True)
|
||||
if 'FINISHED' not in select_node: # only run if mouse click is on a node.
|
||||
return {'CANCELLED'}
|
||||
|
||||
base_node_tree = space.node_tree
|
||||
active_tree = context.space_data.edit_tree
|
||||
path = context.space_data.path
|
||||
nodes = active_tree.nodes
|
||||
active = nodes.active
|
||||
|
||||
if not active and not any(is_visible_socket(out) for out in active.outputs):
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Scan through all nodes in tree including nodes inside of groups to find viewer sockets.
|
||||
self.delete_sockets = []
|
||||
self.search_viewer_sockets_in_tree(base_node_tree, self.delete_sockets)
|
||||
|
||||
if not active.outputs:
|
||||
self.cleanup()
|
||||
return {'CANCELLED'}
|
||||
|
||||
# For geometry node trees, we just connect to the group output.
|
||||
if space.tree_type == 'GeometryNodeTree':
|
||||
|
||||
# Find (or create if needed) the output of this node tree.
|
||||
output_node = self.ensure_group_output(base_node_tree)
|
||||
|
||||
active_node_socket_index = self.get_output_index(
|
||||
active, output_node, base_node_tree == active_tree, 'GEOMETRY', check_type=True
|
||||
)
|
||||
# If there is no 'GEOMETRY' output type - We can't preview the node.
|
||||
if active_node_socket_index is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Find an input socket of the output of type geometry.
|
||||
output_node_socket_index = None
|
||||
for i, inp in enumerate(output_node.inputs):
|
||||
if inp.type == 'GEOMETRY':
|
||||
output_node_socket_index = i
|
||||
break
|
||||
|
||||
node_output = active.outputs[active_node_socket_index]
|
||||
socket_type = find_base_socket_type(node_output)
|
||||
if output_node_socket_index is None:
|
||||
output_node_socket_index = self.ensure_viewer_socket(
|
||||
base_node_tree, socket_type, connect_socket=None,
|
||||
)
|
||||
|
||||
# For shader node trees, we connect to a material output.
|
||||
elif space.tree_type == 'ShaderNodeTree':
|
||||
self.init_shader_variables(space, space.shader_type)
|
||||
|
||||
# Get or create material_output node.
|
||||
output_node = get_group_output_node(
|
||||
base_node_tree,
|
||||
output_node_idname=self.shader_output_idname,
|
||||
)
|
||||
if not output_node:
|
||||
output_node = base_node_tree.nodes.new(self.shader_output_idname)
|
||||
output_node.location = get_output_location(base_node_tree)
|
||||
output_node.select = False
|
||||
|
||||
active_node_socket_index = self.get_output_index(
|
||||
active, output_node, base_node_tree == active_tree, 'SHADER'
|
||||
)
|
||||
|
||||
# Cancel if no socket was found. This can happen for group input
|
||||
# nodes with only a virtual socket output.
|
||||
if active_node_socket_index is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
node_output = active.outputs[active_node_socket_index]
|
||||
socket_type = find_base_socket_type(node_output)
|
||||
if node_output.name == "Volume":
|
||||
output_node_socket_index = 1
|
||||
else:
|
||||
output_node_socket_index = 0
|
||||
|
||||
# If there are no nested node groups, the link starts at the active node.
|
||||
if len(path) > 1:
|
||||
# Recursively connect inside nested node groups and get the one from base level.
|
||||
node_output = self.create_links(path, active, active_node_socket_index, socket_type)
|
||||
output_node_input = output_node.inputs[output_node_socket_index]
|
||||
|
||||
# Connect at base level.
|
||||
connect_sockets(node_output, output_node_input)
|
||||
|
||||
self.cleanup()
|
||||
nodes.active = active
|
||||
active.select = True
|
||||
force_update(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
NODE_OT_connect_to_output,
|
||||
)
|
||||
156
blender-5.2.0/scripts/startup/bl_operators/console.py
Normal file
156
blender-5.2.0/scripts/startup/bl_operators/console.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.app.translations import contexts as i18n_contexts
|
||||
|
||||
|
||||
def _lang_module_get(sc):
|
||||
return __import__(
|
||||
"_console_" + sc.language,
|
||||
# for python 3.3, maybe a bug???
|
||||
level=0,
|
||||
)
|
||||
|
||||
|
||||
class ConsoleExec(Operator):
|
||||
"""Execute the current console line as a Python expression"""
|
||||
bl_idname = "console.execute"
|
||||
bl_label = "Console Execute"
|
||||
bl_options = {'UNDO_GROUPED'}
|
||||
|
||||
interactive: BoolProperty(
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.area and context.area.type == 'CONSOLE')
|
||||
|
||||
def execute(self, context):
|
||||
sc = context.space_data
|
||||
|
||||
module = _lang_module_get(sc)
|
||||
execute = getattr(module, "execute", None)
|
||||
|
||||
if execute is not None:
|
||||
return execute(context, self.interactive)
|
||||
else:
|
||||
print("Error: bpy.ops.console.execute_{:s} - not found".format(sc.language))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ConsoleAutocomplete(Operator):
|
||||
"""Evaluate the namespace up until the cursor and give a list of """ \
|
||||
"""options or complete the name if there is only one"""
|
||||
bl_idname = "console.autocomplete"
|
||||
bl_label = "Console Autocomplete"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.area and context.area.type == 'CONSOLE')
|
||||
|
||||
def execute(self, context):
|
||||
sc = context.space_data
|
||||
module = _lang_module_get(sc)
|
||||
autocomplete = getattr(module, "autocomplete", None)
|
||||
|
||||
if autocomplete:
|
||||
return autocomplete(context)
|
||||
else:
|
||||
print("Error: bpy.ops.console.autocomplete_{:s} - not found".format(sc.language))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ConsoleCopyAsScript(Operator):
|
||||
"""Copy the console contents for use in a script"""
|
||||
bl_idname = "console.copy_as_script"
|
||||
bl_label = "Copy to Clipboard (as Script)"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.area and context.area.type == 'CONSOLE')
|
||||
|
||||
def execute(self, context):
|
||||
sc = context.space_data
|
||||
|
||||
module = _lang_module_get(sc)
|
||||
copy_as_script = getattr(module, "copy_as_script", None)
|
||||
|
||||
if copy_as_script:
|
||||
return copy_as_script(context)
|
||||
else:
|
||||
print("Error: copy_as_script - not found for {!r}".format(sc.language))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ConsoleBanner(Operator):
|
||||
"""Print a message when the terminal initializes"""
|
||||
bl_idname = "console.banner"
|
||||
bl_label = "Console Banner"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.area and context.area.type == 'CONSOLE')
|
||||
|
||||
def execute(self, context):
|
||||
sc = context.space_data
|
||||
|
||||
# default to python
|
||||
if not sc.language:
|
||||
sc.language = "python"
|
||||
|
||||
module = _lang_module_get(sc)
|
||||
banner = getattr(module, "banner", None)
|
||||
|
||||
if banner:
|
||||
return banner(context)
|
||||
else:
|
||||
print("Error: bpy.ops.console.banner_{:s} - not found".format(sc.language))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ConsoleLanguage(Operator):
|
||||
"""Set the current language for this console"""
|
||||
bl_idname = "console.language"
|
||||
bl_label = "Console Language"
|
||||
|
||||
language: StringProperty(
|
||||
name="Language",
|
||||
translation_context=i18n_contexts.editor_python_console,
|
||||
maxlen=32,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.area and context.area.type == 'CONSOLE')
|
||||
|
||||
def execute(self, context):
|
||||
sc = context.space_data
|
||||
|
||||
# default to python
|
||||
sc.language = self.language
|
||||
|
||||
bpy.ops.console.banner()
|
||||
|
||||
# insert a new blank line
|
||||
bpy.ops.console.history_append(text="", current_character=0, remove_duplicates=True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
ConsoleAutocomplete,
|
||||
ConsoleBanner,
|
||||
ConsoleCopyAsScript,
|
||||
ConsoleExec,
|
||||
ConsoleLanguage,
|
||||
)
|
||||
121
blender-5.2.0/scripts/startup/bl_operators/constraint.py
Normal file
121
blender-5.2.0/scripts/startup/bl_operators/constraint.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# SPDX-FileCopyrightText: 2018-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
)
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
)
|
||||
|
||||
|
||||
class CONSTRAINT_OT_add_target(Operator):
|
||||
"""Add a target to the constraint"""
|
||||
bl_idname = "constraint.add_target"
|
||||
bl_label = "Add Target"
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
constraint = getattr(context, "constraint", None)
|
||||
return constraint
|
||||
|
||||
def execute(self, context):
|
||||
context.constraint.targets.new()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CONSTRAINT_OT_remove_target(Operator):
|
||||
"""Remove the target from the constraint"""
|
||||
bl_idname = "constraint.remove_target"
|
||||
bl_label = "Remove Target"
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
index: IntProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
constraint = getattr(context, "constraint", None)
|
||||
return constraint
|
||||
|
||||
def execute(self, context):
|
||||
tgts = context.constraint.targets
|
||||
tgts.remove(tgts[self.index])
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CONSTRAINT_OT_normalize_target_weights(Operator):
|
||||
"""Normalize weights of all target bones"""
|
||||
bl_idname = "constraint.normalize_target_weights"
|
||||
bl_label = "Normalize Weights"
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
constraint = getattr(context, "constraint", None)
|
||||
return constraint
|
||||
|
||||
def execute(self, context):
|
||||
tgts = context.constraint.targets
|
||||
total = sum(t.weight for t in tgts)
|
||||
|
||||
if total > 0:
|
||||
for t in tgts:
|
||||
t.weight = t.weight / total
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CONSTRAINT_OT_disable_keep_transform(Operator):
|
||||
"""Set the influence of this constraint to zero while """ \
|
||||
"""trying to maintain the object's transformation. Other active """ \
|
||||
"""constraints can still influence the final transformation"""
|
||||
|
||||
bl_idname = "constraint.disable_keep_transform"
|
||||
bl_label = "Disable and Keep Transform"
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
constraint = getattr(context, "constraint", None)
|
||||
return constraint and constraint.influence > 0.0
|
||||
|
||||
def execute(self, context):
|
||||
"""Disable constraint while maintaining the visual transform"""
|
||||
|
||||
# This works most of the time, but when there are multiple constraints active
|
||||
# there could still be one that overrides the visual transform.
|
||||
#
|
||||
# Note that executing this operator and then increasing the constraint
|
||||
# influence may move the object; this happens when the constraint is
|
||||
# additive rather than replacing the transform entirely.
|
||||
|
||||
# Get the matrix in world space.
|
||||
is_bone_constraint = context.space_data.context == 'BONE_CONSTRAINT'
|
||||
ob = context.object
|
||||
if is_bone_constraint:
|
||||
bone = context.pose_bone
|
||||
mat = ob.matrix_world @ bone.matrix
|
||||
else:
|
||||
mat = ob.matrix_world
|
||||
|
||||
context.constraint.influence = 0.0
|
||||
|
||||
# Set the matrix.
|
||||
if is_bone_constraint:
|
||||
bone.matrix = ob.matrix_world.inverted() @ mat
|
||||
else:
|
||||
ob.matrix_world = mat
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
CONSTRAINT_OT_add_target,
|
||||
CONSTRAINT_OT_remove_target,
|
||||
CONSTRAINT_OT_normalize_target_weights,
|
||||
CONSTRAINT_OT_disable_keep_transform,
|
||||
)
|
||||
@@ -0,0 +1,823 @@
|
||||
# SPDX-FileCopyrightText: 2021-2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Copy Global Transform
|
||||
|
||||
Simple operators for copying world-space transforms.
|
||||
|
||||
It's called "global" to avoid confusion with the Blender World data-block.
|
||||
"""
|
||||
|
||||
import abc
|
||||
from typing import Iterable, Optional, Any, TypeAlias
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Context, Object, Operator, PoseBone,
|
||||
Camera, ID, ActionChannelbag,
|
||||
)
|
||||
from mathutils import Matrix
|
||||
|
||||
|
||||
_axis_enum_items = [
|
||||
("x", "X", "", 1),
|
||||
("y", "Y", "", 2),
|
||||
("z", "Z", "", 3),
|
||||
]
|
||||
|
||||
# Mapping from frame number to the dominant (in terms of genetics) key type.
|
||||
# GENERATED is the only recessive key type, others are dominant.
|
||||
KeyInfo: TypeAlias = dict[float, str]
|
||||
|
||||
|
||||
def get_matrix(context: Context) -> Matrix:
|
||||
bone = context.active_pose_bone
|
||||
if bone:
|
||||
# Convert matrix to world space
|
||||
arm = context.active_object
|
||||
mat = arm.matrix_world @ bone.matrix
|
||||
else:
|
||||
mat = context.active_object.matrix_world
|
||||
|
||||
return mat
|
||||
|
||||
|
||||
def set_matrix(context: Context, mat: Matrix) -> None:
|
||||
from bpy_extras.anim_utils import AutoKeying
|
||||
bone = context.active_pose_bone
|
||||
if bone:
|
||||
# Convert matrix to local space
|
||||
arm_eval = context.active_object.evaluated_get(context.view_layer.depsgraph)
|
||||
bone.matrix = arm_eval.matrix_world.inverted() @ mat
|
||||
AutoKeying.autokey_transformation(context, bone)
|
||||
else:
|
||||
context.active_object.matrix_world = mat
|
||||
AutoKeying.autokey_transformation(context, context.active_object)
|
||||
|
||||
|
||||
def _channelbag_for_id(animated_id: ID) -> ActionChannelbag | None:
|
||||
# This is on purpose limited to the first layer and strip. To support more
|
||||
# than 1 layer, a rewrite of the caller is needed.
|
||||
|
||||
adt = animated_id.animation_data
|
||||
action = adt and adt.action
|
||||
if action is None:
|
||||
return None
|
||||
|
||||
slot = adt.action_slot
|
||||
|
||||
for layer in action.layers:
|
||||
for strip in layer.strips:
|
||||
assert strip.type == 'KEYFRAME'
|
||||
channelbag = strip.channelbag(slot)
|
||||
return channelbag
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _selected_keyframes(context: Context) -> list[float]:
|
||||
"""Return the list of frame numbers that have a selected key.
|
||||
|
||||
Only keys on the active bone/object are considered.
|
||||
"""
|
||||
|
||||
bone = context.active_pose_bone
|
||||
if bone:
|
||||
return _selected_keyframes_for_bone(context.active_object, bone)
|
||||
return _selected_keyframes_for_object(context.active_object)
|
||||
|
||||
|
||||
def _selected_keyframes_for_bone(object: Object, bone: PoseBone) -> list[float]:
|
||||
"""Return the list of frame numbers that have a selected key.
|
||||
|
||||
Only keys on the given pose bone are considered.
|
||||
"""
|
||||
name = bpy.utils.escape_identifier(bone.name)
|
||||
return _selected_keyframes_for_action_slot(object, "pose.bones[\"{:s}\"].".format(name))
|
||||
|
||||
|
||||
def _selected_keyframes_for_object(object: Object) -> list[float]:
|
||||
"""Return the list of frame numbers that have a selected key.
|
||||
|
||||
Only keys on the given object are considered.
|
||||
"""
|
||||
return _selected_keyframes_for_action_slot(object, "")
|
||||
|
||||
|
||||
def _selected_keyframes_for_action_slot(object: Object, rna_path_prefix: str) -> list[float]:
|
||||
"""Return the list of frame numbers that have a selected key.
|
||||
|
||||
Only keys on the given object's Action Slot on FCurves starting with rna_path_prefix are considered.
|
||||
"""
|
||||
|
||||
cbag = _channelbag_for_id(object)
|
||||
if not cbag:
|
||||
return []
|
||||
|
||||
keyframes = set()
|
||||
for fcurve in cbag.fcurves:
|
||||
if not fcurve.data_path.startswith(rna_path_prefix):
|
||||
continue
|
||||
|
||||
for kp in fcurve.keyframe_points:
|
||||
if not kp.select_control_point:
|
||||
continue
|
||||
keyframes.add(kp.co.x)
|
||||
return sorted(keyframes)
|
||||
|
||||
|
||||
def _copy_matrix_to_clipboard(window_manager: bpy.types.WindowManager, matrix: Matrix) -> None:
|
||||
rows = [" {!r},".format(tuple(row)) for row in matrix]
|
||||
as_string = "\n".join(rows)
|
||||
window_manager.clipboard = "Matrix((\n{:s}\n))".format(as_string)
|
||||
|
||||
|
||||
class OBJECT_OT_copy_global_transform(Operator):
|
||||
bl_idname = "object.copy_global_transform"
|
||||
bl_label = "Copy Global Transform"
|
||||
bl_description = (
|
||||
"Copies the matrix of the currently active object or pose bone to the clipboard. Uses world-space matrices"
|
||||
)
|
||||
# This operator cannot be un-done because it manipulates data outside Blender.
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.active_pose_bone) or bool(context.active_object)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
mat = get_matrix(context)
|
||||
_copy_matrix_to_clipboard(context.window_manager, mat)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_relative_ob(context: Context) -> Optional[Object]:
|
||||
"""Get the 'relative' object.
|
||||
|
||||
This is the object that's configured, or if that's empty, the active scene camera.
|
||||
"""
|
||||
rel_ob = context.scene.tool_settings.anim_relative_object
|
||||
return rel_ob or context.scene.camera
|
||||
|
||||
|
||||
class OBJECT_OT_copy_relative_transform(Operator):
|
||||
bl_idname = "object.copy_relative_transform"
|
||||
bl_label = "Copy Relative Transform"
|
||||
bl_description = "Copies the matrix of the currently active object or pose bone to the clipboard. " \
|
||||
"Uses matrices relative to a specific object or the active scene camera"
|
||||
# This operator cannot be un-done because it manipulates data outside Blender.
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
rel_ob = get_relative_ob(context)
|
||||
if not rel_ob:
|
||||
return False
|
||||
return bool(context.active_pose_bone) or bool(context.active_object)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
rel_ob = get_relative_ob(context)
|
||||
if not rel_ob:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"No 'Relative To' object found, set one explicitly or make sure there is an active object")
|
||||
return {'CANCELLED'}
|
||||
mat = rel_ob.matrix_world.inverted() @ get_matrix(context)
|
||||
_copy_matrix_to_clipboard(context.window_manager, mat)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UnableToMirrorError(Exception):
|
||||
"""Raised when mirroring is enabled but no mirror object/bone is set."""
|
||||
|
||||
|
||||
class OBJECT_OT_paste_transform(Operator):
|
||||
bl_idname = "object.paste_transform"
|
||||
bl_label = "Paste Global Transform"
|
||||
bl_description = (
|
||||
"Pastes the matrix from the clipboard to the currently active pose bone or object. Uses world-space matrices"
|
||||
)
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
_method_items = [
|
||||
(
|
||||
'CURRENT',
|
||||
"Current Transform",
|
||||
"Paste onto the current values only, only manipulating the animation data if auto-keying is enabled",
|
||||
),
|
||||
(
|
||||
'EXISTING_KEYS',
|
||||
"Selected Keys",
|
||||
"Paste onto frames that have a selected key, potentially creating new keys on those frames",
|
||||
),
|
||||
(
|
||||
'BAKE',
|
||||
"Bake on Key Range",
|
||||
"Paste onto all frames between the first and last selected key, creating new keyframes if necessary",
|
||||
),
|
||||
]
|
||||
method: bpy.props.EnumProperty( # type: ignore
|
||||
items=_method_items,
|
||||
name="Paste Method",
|
||||
description="Update the current transform, selected keyframes, or even create new keys",
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
bake_step: bpy.props.IntProperty( # type: ignore
|
||||
name="Frame Step",
|
||||
description="Only used for baking. Step=1 creates a key on every frame, step=2 bakes on 2s, etc",
|
||||
min=1,
|
||||
soft_min=1,
|
||||
soft_max=5,
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
use_mirror: bpy.props.BoolProperty( # type: ignore
|
||||
name="Mirror Transform",
|
||||
description="When pasting, mirror the transform relative to a specific object or bone",
|
||||
default=False,
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
mirror_axis_loc: bpy.props.EnumProperty( # type: ignore
|
||||
items=_axis_enum_items,
|
||||
name="Location Axis",
|
||||
description="Coordinate axis used to mirror the location part of the transform",
|
||||
default='x',
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
mirror_axis_rot: bpy.props.EnumProperty( # type: ignore
|
||||
items=_axis_enum_items,
|
||||
name="Rotation Axis",
|
||||
description="Coordinate axis used to mirror the rotation part of the transform",
|
||||
default='z',
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
use_relative: bpy.props.BoolProperty( # type: ignore
|
||||
name="Use Relative Paste",
|
||||
description="When pasting, assume the pasted matrix is relative to another object (set in the user interface)",
|
||||
default=False,
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_pose_bone and not context.active_object:
|
||||
cls.poll_message_set("Select an object or pose bone")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def string_to_matrix(cls, value: str) -> Matrix | None:
|
||||
if value.startswith("Matrix"):
|
||||
return cls.parse_matrix(value)
|
||||
if value.startswith("<Matrix 4x4"):
|
||||
return cls.parse_repr_m4(value[12:-1])
|
||||
if value:
|
||||
return cls.parse_print_m4(value)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_matrix(value: str) -> Matrix | None:
|
||||
import ast
|
||||
try:
|
||||
return Matrix(ast.literal_eval(value[6:]))
|
||||
except Exception:
|
||||
# ast.literal_eval() can raise a slew of exceptions, all of
|
||||
# which means that it's not a matrix on the clipboard.
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_print_m4(value: str) -> Optional[Matrix]:
|
||||
"""Parse output from Blender's print_m4() function.
|
||||
|
||||
Expects four lines of space-separated floats.
|
||||
"""
|
||||
|
||||
lines = value.strip().splitlines()
|
||||
if len(lines) != 4:
|
||||
return None
|
||||
|
||||
try:
|
||||
floats = tuple(tuple(float(item) for item in line.split()) for line in lines)
|
||||
except ValueError:
|
||||
# Apparently not the expected format.
|
||||
return None
|
||||
return Matrix(floats)
|
||||
|
||||
@staticmethod
|
||||
def parse_repr_m4(value: str) -> Optional[Matrix]:
|
||||
"""Four lines of (a, b, c, d) floats."""
|
||||
|
||||
lines = value.strip().splitlines()
|
||||
if len(lines) != 4:
|
||||
return None
|
||||
|
||||
try:
|
||||
floats = tuple(tuple(float(item.strip()) for item in line.strip()[1:-1].split(',')) for line in lines)
|
||||
except ValueError:
|
||||
# Apparently not the expected format.
|
||||
return None
|
||||
return Matrix(floats)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
clipboard = context.window_manager.clipboard.strip()
|
||||
|
||||
mat = self.string_to_matrix(clipboard)
|
||||
if mat is None:
|
||||
self.report({'ERROR'}, "Clipboard does not contain a matrix")
|
||||
return {'CANCELLED'}
|
||||
|
||||
try:
|
||||
mat = self._preprocess_matrix(context, mat)
|
||||
except UnableToMirrorError:
|
||||
self.report({'ERROR'}, "Unable to mirror, no mirror object/bone configured")
|
||||
return {'CANCELLED'}
|
||||
|
||||
applicator = {
|
||||
'CURRENT': self._paste_current,
|
||||
'EXISTING_KEYS': self._paste_existing_keys,
|
||||
'BAKE': self._paste_bake,
|
||||
}[self.method]
|
||||
return applicator(context, mat)
|
||||
|
||||
def _preprocess_matrix(self, context: Context, matrix: Matrix) -> Matrix:
|
||||
if self.use_relative:
|
||||
matrix = self._relative_to_world(context, matrix)
|
||||
|
||||
if self.use_mirror:
|
||||
matrix = self._mirror_matrix(context, matrix)
|
||||
return matrix
|
||||
|
||||
def _relative_to_world(self, context: Context, matrix: Matrix) -> Matrix:
|
||||
rel_ob = get_relative_ob(context)
|
||||
if not rel_ob:
|
||||
return matrix
|
||||
|
||||
rel_ob_eval = rel_ob.evaluated_get(context.view_layer.depsgraph)
|
||||
return rel_ob_eval.matrix_world @ matrix
|
||||
|
||||
def _mirror_matrix(self, context: Context, matrix: Matrix) -> Matrix:
|
||||
mirror_ob = context.scene.tool_settings.anim_mirror_object
|
||||
mirror_bone = context.scene.tool_settings.anim_mirror_bone
|
||||
|
||||
# No mirror object means "current armature object".
|
||||
ctx_ob = context.object
|
||||
if not mirror_ob and mirror_bone and ctx_ob and ctx_ob.type == 'ARMATURE':
|
||||
mirror_ob = ctx_ob
|
||||
|
||||
if not mirror_ob:
|
||||
raise UnableToMirrorError()
|
||||
|
||||
if mirror_ob.type == 'ARMATURE' and mirror_bone:
|
||||
return self._mirror_over_bone(matrix, mirror_ob, mirror_bone)
|
||||
return self._mirror_over_ob(matrix, mirror_ob)
|
||||
|
||||
def _mirror_over_ob(self, matrix: Matrix, mirror_ob: bpy.types.Object) -> Matrix:
|
||||
mirror_matrix = mirror_ob.matrix_world
|
||||
return self._mirror_over_matrix(matrix, mirror_matrix)
|
||||
|
||||
def _mirror_over_bone(self, matrix: Matrix, mirror_ob: bpy.types.Object, mirror_bone_name: str) -> Matrix:
|
||||
bone = mirror_ob.pose.bones[mirror_bone_name]
|
||||
mirror_matrix = mirror_ob.matrix_world @ bone.matrix
|
||||
return self._mirror_over_matrix(matrix, mirror_matrix)
|
||||
|
||||
def _mirror_over_matrix(self, matrix: Matrix, mirror_matrix: Matrix) -> Matrix:
|
||||
# Compute the matrix in the space of the mirror matrix:
|
||||
mat_local = mirror_matrix.inverted() @ matrix
|
||||
|
||||
# Decompose the matrix, as we don't want to touch the scale. This
|
||||
# operator should only mirror the translation and rotation components.
|
||||
trans, rot_q, scale = mat_local.decompose()
|
||||
|
||||
# Mirror the translation component:
|
||||
axis_index = ord(self.mirror_axis_loc) - ord('x')
|
||||
trans[axis_index] *= -1
|
||||
|
||||
# Flip the rotation, and use a rotation order that applies the to-be-flipped axes first.
|
||||
match self.mirror_axis_rot:
|
||||
case 'x':
|
||||
rot_e = rot_q.to_euler('XYZ')
|
||||
rot_e.x *= -1 # Flip the requested rotation axis.
|
||||
rot_e.y *= -1 # Also flip the bone roll.
|
||||
case 'y':
|
||||
rot_e = rot_q.to_euler('YZX')
|
||||
rot_e.y *= -1 # Flip the requested rotation axis.
|
||||
rot_e.z *= -1 # Also flip another axis? Not sure how to handle this one.
|
||||
case 'z':
|
||||
rot_e = rot_q.to_euler('ZYX')
|
||||
rot_e.z *= -1 # Flip the requested rotation axis.
|
||||
rot_e.y *= -1 # Also flip the bone roll.
|
||||
|
||||
# Recompose the local matrix:
|
||||
mat_local = Matrix.LocRotScale(trans, rot_e, scale)
|
||||
|
||||
# Go back to world space:
|
||||
mirrored_world = mirror_matrix @ mat_local
|
||||
return mirrored_world
|
||||
|
||||
@staticmethod
|
||||
def _paste_current(context: Context, matrix: Matrix) -> set[str]:
|
||||
set_matrix(context, matrix)
|
||||
return {'FINISHED'}
|
||||
|
||||
def _paste_existing_keys(self, context: Context, matrix: Matrix) -> set[str]:
|
||||
if not context.scene.tool_settings.use_keyframe_insert_auto:
|
||||
self.report({'ERROR'}, "This mode requires auto-keying to work properly")
|
||||
return {'CANCELLED'}
|
||||
|
||||
frame_numbers = _selected_keyframes(context)
|
||||
if not frame_numbers:
|
||||
self.report({'WARNING'}, "No selected frames found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self._paste_on_frames(context, frame_numbers, matrix)
|
||||
return {'FINISHED'}
|
||||
|
||||
def _paste_bake(self, context: Context, matrix: Matrix) -> set[str]:
|
||||
if not context.scene.tool_settings.use_keyframe_insert_auto:
|
||||
self.report({'ERROR'}, "This mode requires auto-keying to work properly")
|
||||
return {'CANCELLED'}
|
||||
|
||||
bake_step = max(1, self.bake_step)
|
||||
# Put the clamped bake step back into RNA for the redo panel.
|
||||
self.bake_step = bake_step
|
||||
|
||||
frame_start, frame_end = self._determine_bake_range(context)
|
||||
frame_range = range(round(frame_start), round(frame_end) + bake_step, bake_step)
|
||||
self._paste_on_frames(context, frame_range, matrix)
|
||||
return {'FINISHED'}
|
||||
|
||||
def _determine_bake_range(self, context: Context) -> tuple[float, float]:
|
||||
frame_numbers = _selected_keyframes(context)
|
||||
if frame_numbers:
|
||||
# Note that these could be the same frame, if len(frame_numbers) == 1:
|
||||
return frame_numbers[0], frame_numbers[-1]
|
||||
|
||||
if context.scene.use_preview_range:
|
||||
self.report({'INFO'}, "No selected keys, pasting over preview range")
|
||||
return context.scene.frame_preview_start, context.scene.frame_preview_end
|
||||
|
||||
self.report({'INFO'}, "No selected keys, pasting over scene range")
|
||||
return context.scene.frame_start, context.scene.frame_end
|
||||
|
||||
def _paste_on_frames(self, context: Context, frame_numbers: Iterable[float], matrix: Matrix) -> None:
|
||||
current_frame = context.scene.frame_current_final
|
||||
try:
|
||||
for frame in frame_numbers:
|
||||
context.scene.frame_set(int(frame), subframe=frame % 1.0)
|
||||
set_matrix(context, matrix)
|
||||
finally:
|
||||
context.scene.frame_set(int(current_frame), subframe=current_frame % 1.0)
|
||||
|
||||
|
||||
class Transformable(metaclass=abc.ABCMeta):
|
||||
"""Interface for a bone or an object."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._key_info_cache: Optional[KeyInfo] = None
|
||||
|
||||
@abc.abstractmethod
|
||||
def matrix_world(self) -> Matrix:
|
||||
pass
|
||||
|
||||
def set_matrix_world(self, context: Context, matrix: Matrix) -> None:
|
||||
"""Set the world matrix, without auto-keying."""
|
||||
self._set_matrix_world(context, matrix)
|
||||
|
||||
def set_matrix_world_autokey(self, context: Context, matrix: Matrix) -> None:
|
||||
"""Set the world matrix, and auto-key the resulting transform."""
|
||||
self._set_matrix_world(context, matrix)
|
||||
self._autokey_matrix_world(context)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _set_matrix_world(self, context: Context, matrix: Matrix) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _autokey_matrix_world(self, context: Context) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _my_fcurves(self) -> Iterable[bpy.types.FCurve]:
|
||||
pass
|
||||
|
||||
def key_info(self) -> KeyInfo:
|
||||
if self._key_info_cache is not None:
|
||||
return self._key_info_cache
|
||||
|
||||
keyinfo: KeyInfo = {}
|
||||
for fcurve in self._my_fcurves():
|
||||
for kp in fcurve.keyframe_points:
|
||||
frame = kp.co.x
|
||||
if kp.type == 'GENERATED' and frame in keyinfo:
|
||||
# Don't bother overwriting other key types.
|
||||
continue
|
||||
keyinfo[frame] = kp.type
|
||||
|
||||
self._key_info_cache = keyinfo
|
||||
return keyinfo
|
||||
|
||||
def remove_keys_of_type(
|
||||
self,
|
||||
key_type: str,
|
||||
*,
|
||||
frame_start: float | int = float("-inf"),
|
||||
frame_end: float | int = float("inf")) -> None:
|
||||
self._key_info_cache = None
|
||||
|
||||
for fcurve in self._my_fcurves():
|
||||
to_remove = [
|
||||
kp for kp in fcurve.keyframe_points if kp.type == key_type and (frame_start <= kp.co.x <= frame_end)
|
||||
]
|
||||
for kp in reversed(to_remove):
|
||||
fcurve.keyframe_points.remove(kp, fast=True)
|
||||
fcurve.keyframe_points.handles_recalc()
|
||||
|
||||
|
||||
class TransformableObject(Transformable):
|
||||
object: Object
|
||||
|
||||
def __init__(self, object: Object) -> None:
|
||||
super().__init__()
|
||||
self.object = object
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "TransformableObject({:s})".format(self.object.name)
|
||||
|
||||
def matrix_world(self) -> Matrix:
|
||||
return self.object.matrix_world
|
||||
|
||||
def _set_matrix_world(self, _context: Context, matrix: Matrix) -> None:
|
||||
self.object.matrix_world = matrix
|
||||
|
||||
def _autokey_matrix_world(self, context: Context) -> None:
|
||||
from bpy_extras.anim_utils import AutoKeying
|
||||
AutoKeying.autokey_transformation(context, self.object)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.object.as_pointer())
|
||||
|
||||
def _my_fcurves(self) -> Iterable[bpy.types.FCurve]:
|
||||
cbag = _channelbag_for_id(self.object)
|
||||
if not cbag:
|
||||
return
|
||||
yield from cbag.fcurves
|
||||
|
||||
|
||||
class TransformableBone(Transformable):
|
||||
arm_object: Object
|
||||
pose_bone: PoseBone
|
||||
|
||||
def __init__(self, pose_bone: PoseBone) -> None:
|
||||
super().__init__()
|
||||
self.arm_object = pose_bone.id_data
|
||||
self.pose_bone = pose_bone
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "TransformableBone({:s}, bone={:s})".format(self.arm_object.name, self.pose_bone.name)
|
||||
|
||||
def matrix_world(self) -> Matrix:
|
||||
mat = self.arm_object.matrix_world @ self.pose_bone.matrix
|
||||
return mat
|
||||
|
||||
def _set_matrix_world(self, context: Context, matrix: Matrix) -> None:
|
||||
# Convert matrix to armature-local space
|
||||
arm_eval = self.arm_object.evaluated_get(context.view_layer.depsgraph)
|
||||
self.pose_bone.matrix = arm_eval.matrix_world.inverted() @ matrix
|
||||
|
||||
def _autokey_matrix_world(self, context: Context) -> None:
|
||||
from bpy_extras.anim_utils import AutoKeying
|
||||
AutoKeying.autokey_transformation(context, self.pose_bone)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.pose_bone.as_pointer())
|
||||
|
||||
def _my_fcurves(self) -> Iterable[bpy.types.FCurve]:
|
||||
cbag = _channelbag_for_id(self.arm_object)
|
||||
if not cbag:
|
||||
return
|
||||
|
||||
rna_prefix = self.pose_bone.path_from_id() + "."
|
||||
for fcurve in cbag.fcurves:
|
||||
if fcurve.data_path.startswith(rna_prefix):
|
||||
yield fcurve
|
||||
|
||||
|
||||
class FixToCameraCommon:
|
||||
"""Common functionality for the Fix To Scene Camera operator + its 'delete' button."""
|
||||
|
||||
keytype = 'GENERATED'
|
||||
|
||||
# Operator method stubs to avoid PyLance/MyPy errors:
|
||||
@classmethod
|
||||
def poll_message_set(cls, message: str) -> None:
|
||||
super().poll_message_set(message)
|
||||
|
||||
def report(self, level: set[str], message: str) -> None:
|
||||
super().report(level, message)
|
||||
|
||||
# Implement in subclass:
|
||||
def _execute(self, context: Context, transformables: list[Transformable]) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_pose_bone and not context.active_object:
|
||||
cls.poll_message_set("Select an object or pose bone")
|
||||
return False
|
||||
if context.mode not in {'POSE', 'OBJECT'}:
|
||||
cls.poll_message_set("Switch to Pose or Object mode")
|
||||
return False
|
||||
if not context.scene.camera:
|
||||
cls.poll_message_set("The Scene needs a camera")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
match context.mode:
|
||||
case 'OBJECT':
|
||||
transformables = self._transformable_objects(context)
|
||||
case 'POSE':
|
||||
transformables = self._transformable_pbones(context)
|
||||
case mode:
|
||||
self.report({'ERROR'}, "Unsupported mode: {!r}".format(mode))
|
||||
return {'CANCELLED'}
|
||||
|
||||
restore_frame = context.scene.frame_current
|
||||
restore_matrices = [(transformable, transformable.matrix_world().copy()) for transformable in transformables]
|
||||
|
||||
try:
|
||||
self._execute(context, transformables)
|
||||
finally:
|
||||
# Restore the state of the scene & the transformables. This is necessary
|
||||
# as not all properties may have been auto-keyed (for example 'only
|
||||
# available' enabled, and rotation is not actually keyed yet), so we can't
|
||||
# assume that going to the original frame restores the entire matrix.
|
||||
context.scene.frame_set(restore_frame)
|
||||
for transformable, matrix in restore_matrices:
|
||||
transformable.set_matrix_world(context, matrix)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def _transformable_objects(self, context: Context) -> list[Transformable]:
|
||||
return [TransformableObject(object=ob) for ob in context.selected_editable_objects]
|
||||
|
||||
def _transformable_pbones(self, context: Context) -> list[Transformable]:
|
||||
return [TransformableBone(pose_bone=bone) for bone in context.selected_pose_bones]
|
||||
|
||||
|
||||
class OBJECT_OT_fix_to_camera(FixToCameraCommon, Operator):
|
||||
bl_idname = "object.fix_to_camera"
|
||||
bl_label = "Fix to Scene Camera"
|
||||
bl_description = "Generate new keys to fix the selected object/bone to the camera on unkeyed frames"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
use_location: bpy.props.BoolProperty( # type: ignore
|
||||
name="Location",
|
||||
description="Create Location keys when fixing to the scene camera",
|
||||
default=True,
|
||||
)
|
||||
use_rotation: bpy.props.BoolProperty( # type: ignore
|
||||
name="Rotation",
|
||||
description="Create Rotation keys when fixing to the scene camera",
|
||||
default=True,
|
||||
)
|
||||
use_scale: bpy.props.BoolProperty( # type: ignore
|
||||
name="Scale",
|
||||
description="Create Scale keys when fixing to the scene camera",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def _get_matrices(self, camera: Camera, transformables: list[Transformable]) -> dict[Transformable, Matrix]:
|
||||
camera_mat_inv = camera.matrix_world.inverted()
|
||||
return {t: camera_mat_inv @ t.matrix_world() for t in transformables}
|
||||
|
||||
def _execute(self, context: Context, transformables: list[Transformable]) -> None:
|
||||
from bpy_extras.anim_utils import AutoKeying
|
||||
from bpy_extras.wm_utils import progress_report
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
scene = context.scene
|
||||
|
||||
scene.frame_set(scene.frame_start)
|
||||
camera_eval = scene.camera.evaluated_get(depsgraph)
|
||||
last_camera_name = scene.camera.name
|
||||
matrices = self._get_matrices(camera_eval, transformables)
|
||||
|
||||
if scene.use_preview_range:
|
||||
frame_start = scene.frame_preview_start
|
||||
frame_end = scene.frame_preview_end
|
||||
else:
|
||||
frame_start = scene.frame_start
|
||||
frame_end = scene.frame_end
|
||||
|
||||
with (
|
||||
AutoKeying.options(
|
||||
keytype=self.keytype,
|
||||
use_loc=self.use_location,
|
||||
use_rot=self.use_rotation,
|
||||
use_scale=self.use_scale,
|
||||
force_autokey=True,
|
||||
),
|
||||
progress_report.ProgressReport(context.window_manager) as progress,
|
||||
):
|
||||
frames_to_visit = range(frame_start, frame_end + scene.frame_step, scene.frame_step)
|
||||
progress.enter_substeps(len(frames_to_visit))
|
||||
|
||||
for frame in frames_to_visit:
|
||||
scene.frame_set(frame)
|
||||
progress.step()
|
||||
|
||||
camera_eval = scene.camera.evaluated_get(depsgraph)
|
||||
cam_matrix_world = camera_eval.matrix_world
|
||||
camera_mat_inv = cam_matrix_world.inverted()
|
||||
|
||||
if scene.camera.name != last_camera_name:
|
||||
# The scene camera changed, so the previous
|
||||
# relative-to-camera matrices can no longer be used.
|
||||
matrices = self._get_matrices(camera_eval, transformables)
|
||||
last_camera_name = scene.camera.name
|
||||
|
||||
for t, camera_rel_matrix in matrices.items():
|
||||
key_info = t.key_info()
|
||||
key_type = key_info.get(frame, "")
|
||||
if key_type not in {self.keytype, ""}:
|
||||
# Manually set key, remember the current camera-relative matrix.
|
||||
matrices[t] = camera_mat_inv @ t.matrix_world()
|
||||
continue
|
||||
|
||||
# No key, or a generated one. Overwrite it with a new transform.
|
||||
t.set_matrix_world_autokey(context, cam_matrix_world @ camera_rel_matrix)
|
||||
|
||||
|
||||
class OBJECT_OT_delete_fix_to_camera_keys(Operator, FixToCameraCommon):
|
||||
bl_idname = "object.delete_fix_to_camera_keys"
|
||||
bl_label = "Delete Generated Keys"
|
||||
bl_description = "Delete all keys that were generated by the 'Fix to Scene Camera' operator"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def _execute(self, context: Context, transformables: list[Transformable]) -> None:
|
||||
scene = context.scene
|
||||
if scene.use_preview_range:
|
||||
frame_start = scene.frame_preview_start
|
||||
frame_end = scene.frame_preview_end
|
||||
else:
|
||||
frame_start = scene.frame_start
|
||||
frame_end = scene.frame_end
|
||||
|
||||
for t in transformables:
|
||||
t.remove_keys_of_type(self.keytype, frame_start=frame_start, frame_end=frame_end)
|
||||
|
||||
|
||||
# MessageBus subscription to monitor changes & refresh panels.
|
||||
_msgbus_owner = object()
|
||||
|
||||
|
||||
def _refresh_3d_panels():
|
||||
refresh_area_types = {'VIEW_3D'}
|
||||
for win in bpy.context.window_manager.windows:
|
||||
for area in win.screen.areas:
|
||||
if area.type not in refresh_area_types:
|
||||
continue
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
classes = (
|
||||
OBJECT_OT_copy_global_transform,
|
||||
OBJECT_OT_copy_relative_transform,
|
||||
OBJECT_OT_paste_transform,
|
||||
OBJECT_OT_fix_to_camera,
|
||||
OBJECT_OT_delete_fix_to_camera_keys,
|
||||
)
|
||||
|
||||
|
||||
def _register_message_bus() -> None:
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=(bpy.types.ToolSettings, "use_keyframe_insert_auto"),
|
||||
owner=_msgbus_owner,
|
||||
args=(),
|
||||
notify=_refresh_3d_panels,
|
||||
options={'PERSISTENT'},
|
||||
)
|
||||
|
||||
|
||||
def _unregister_message_bus() -> None:
|
||||
bpy.msgbus.clear_by_owner(_msgbus_owner)
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent # type: ignore
|
||||
def _on_blendfile_load_post(_none: Any, _other_none: Any) -> None:
|
||||
# The parameters are required, but both are None.
|
||||
_register_message_bus()
|
||||
|
||||
|
||||
def register():
|
||||
bpy.app.handlers.load_post.append(_on_blendfile_load_post)
|
||||
|
||||
|
||||
def unregister():
|
||||
_unregister_message_bus()
|
||||
bpy.app.handlers.load_post.remove(_on_blendfile_load_post)
|
||||
308
blender-5.2.0/scripts/startup/bl_operators/file.py
Normal file
308
blender-5.2.0/scripts/startup/bl_operators/file.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# SPDX-FileCopyrightText: 2015-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
OperatorFileListElement,
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
|
||||
# ########## Datablock previews... ##########
|
||||
|
||||
|
||||
class WM_OT_previews_batch_generate(Operator):
|
||||
"""Generate selected .blend file's previews"""
|
||||
bl_idname = "wm.previews_batch_generate"
|
||||
bl_label = "Batch-Generate Previews"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
# -----------
|
||||
# File props.
|
||||
files: CollectionProperty(
|
||||
type=OperatorFileListElement,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
name="",
|
||||
description="Collection of file paths with common ``directory`` root",
|
||||
)
|
||||
|
||||
directory: StringProperty(
|
||||
maxlen=1024,
|
||||
subtype='FILE_PATH',
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
name="",
|
||||
description="Root path of all files listed in ``files`` collection",
|
||||
)
|
||||
|
||||
# Show only images/videos, and directories!
|
||||
filter_blender: BoolProperty(
|
||||
default=True,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
name="",
|
||||
description="Show Blender files in the File Browser",
|
||||
)
|
||||
filter_folder: BoolProperty(
|
||||
default=True,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
name="",
|
||||
description="Show folders in the File Browser",
|
||||
)
|
||||
|
||||
# -----------
|
||||
# Own props.
|
||||
use_scenes: BoolProperty(
|
||||
default=True,
|
||||
name="Scenes",
|
||||
description="Generate scenes' previews",
|
||||
)
|
||||
use_collections: BoolProperty(
|
||||
default=True,
|
||||
name="Collections",
|
||||
description="Generate collections' previews",
|
||||
)
|
||||
use_objects: BoolProperty(
|
||||
default=True,
|
||||
name="Objects",
|
||||
description="Generate objects' previews",
|
||||
)
|
||||
use_intern_data: BoolProperty(
|
||||
default=True,
|
||||
name="Materials & Textures",
|
||||
description="Generate 'internal' previews (materials, textures, images, etc.)",
|
||||
)
|
||||
|
||||
use_trusted: BoolProperty(
|
||||
default=False,
|
||||
name="Trusted Blend Files",
|
||||
description="Enable Python evaluation for selected files",
|
||||
)
|
||||
use_backups: BoolProperty(
|
||||
default=True,
|
||||
name="Save Backups",
|
||||
description="Keep a backup (.blend1) version of the files when saving with generated previews",
|
||||
)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
import subprocess
|
||||
from _bl_previews_utils import bl_previews_render as preview_render
|
||||
|
||||
context.window_manager.progress_begin(0, len(self.files))
|
||||
context.window_manager.progress_update(0)
|
||||
for i, fn in enumerate(self.files):
|
||||
blen_path = os.path.join(self.directory, fn.name)
|
||||
cmd = [
|
||||
bpy.app.binary_path,
|
||||
"--background",
|
||||
"--factory-startup",
|
||||
]
|
||||
if self.use_trusted:
|
||||
cmd.append("--enable-autoexec")
|
||||
cmd.extend([
|
||||
blen_path,
|
||||
"--python",
|
||||
os.path.join(os.path.dirname(preview_render.__file__), "bl_previews_render.py"),
|
||||
"--",
|
||||
])
|
||||
if not self.use_scenes:
|
||||
cmd.append("--no_scenes")
|
||||
if not self.use_collections:
|
||||
cmd.append("--no_collections")
|
||||
if not self.use_objects:
|
||||
cmd.append("--no_objects")
|
||||
if not self.use_intern_data:
|
||||
cmd.append("--no_data_intern")
|
||||
if not self.use_backups:
|
||||
cmd.append("--no_backups")
|
||||
if subprocess.call(cmd):
|
||||
self.report({'ERROR'}, rpt_("Previews generation process failed for file '{:s}'!").format(blen_path))
|
||||
context.window_manager.progress_end()
|
||||
return {'CANCELLED'}
|
||||
context.window_manager.progress_update(i + 1)
|
||||
context.window_manager.progress_end()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class WM_OT_previews_batch_clear(Operator):
|
||||
"""Clear selected .blend file's previews"""
|
||||
bl_idname = "wm.previews_batch_clear"
|
||||
bl_label = "Batch-Clear Previews"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
# -----------
|
||||
# File props.
|
||||
files: CollectionProperty(
|
||||
type=OperatorFileListElement,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
directory: StringProperty(
|
||||
maxlen=1024,
|
||||
subtype='FILE_PATH',
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
# Show only images/videos, and directories!
|
||||
filter_blender: BoolProperty(
|
||||
default=True,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
)
|
||||
filter_folder: BoolProperty(
|
||||
default=True,
|
||||
options={'HIDDEN', 'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
# -----------
|
||||
# Own props.
|
||||
use_scenes: BoolProperty(
|
||||
default=True,
|
||||
name="Scenes",
|
||||
description="Clear scenes' previews",
|
||||
)
|
||||
use_collections: BoolProperty(
|
||||
default=True,
|
||||
name="Collections",
|
||||
description="Clear collections' previews",
|
||||
)
|
||||
use_objects: BoolProperty(
|
||||
default=True,
|
||||
name="Objects",
|
||||
description="Clear objects' previews",
|
||||
)
|
||||
use_intern_data: BoolProperty(
|
||||
default=True,
|
||||
name="Materials & Textures",
|
||||
description="Clear 'internal' previews (materials, textures, images, etc.)",
|
||||
)
|
||||
|
||||
use_trusted: BoolProperty(
|
||||
default=False,
|
||||
name="Trusted Blend Files",
|
||||
description="Enable Python evaluation for selected files",
|
||||
)
|
||||
use_backups: BoolProperty(
|
||||
default=True,
|
||||
name="Save Backups",
|
||||
description="Keep a backup (.blend1) version of the files when saving with cleared previews",
|
||||
)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
import subprocess
|
||||
from _bl_previews_utils import bl_previews_render as preview_render
|
||||
|
||||
context.window_manager.progress_begin(0, len(self.files))
|
||||
context.window_manager.progress_update(0)
|
||||
for i, fn in enumerate(self.files):
|
||||
blen_path = os.path.join(self.directory, fn.name)
|
||||
cmd = [
|
||||
bpy.app.binary_path,
|
||||
"--background",
|
||||
"--factory-startup",
|
||||
]
|
||||
if self.use_trusted:
|
||||
cmd.append("--enable-autoexec")
|
||||
cmd.extend([
|
||||
blen_path,
|
||||
"--python",
|
||||
os.path.join(os.path.dirname(preview_render.__file__), "bl_previews_render.py"),
|
||||
"--",
|
||||
"--clear",
|
||||
])
|
||||
if not self.use_scenes:
|
||||
cmd.append("--no_scenes")
|
||||
if not self.use_collections:
|
||||
cmd.append("--no_collections")
|
||||
if not self.use_objects:
|
||||
cmd.append("--no_objects")
|
||||
if not self.use_intern_data:
|
||||
cmd.append("--no_data_intern")
|
||||
if not self.use_backups:
|
||||
cmd.append("--no_backups")
|
||||
if subprocess.call(cmd):
|
||||
self.report({'ERROR'}, rpt_("Previews clear process failed for file '{:s}'!").format(blen_path))
|
||||
context.window_manager.progress_end()
|
||||
return {'CANCELLED'}
|
||||
context.window_manager.progress_update(i + 1)
|
||||
context.window_manager.progress_end()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class WM_OT_blend_strings_utf8_validate(Operator):
|
||||
"""Check and fix all strings in current .blend file to be valid UTF-8 Unicode """ \
|
||||
"""(needed for some old, 2.4x area files)"""
|
||||
bl_idname = "wm.blend_strings_utf8_validate"
|
||||
bl_label = "Validate .blend strings"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
def validate_strings(self, item, done_items):
|
||||
if item is None:
|
||||
return False
|
||||
|
||||
if item in done_items:
|
||||
return False
|
||||
done_items.add(item)
|
||||
|
||||
if getattr(item, "library", None) is not None:
|
||||
return False # No point in checking library data, we cannot fix it anyway...
|
||||
|
||||
changed = False
|
||||
for prop in item.bl_rna.properties:
|
||||
if prop.identifier in {"bl_rna", "rna_type"}:
|
||||
continue # Or we'd recurse 'till Hell freezes.
|
||||
if prop.is_readonly:
|
||||
continue
|
||||
if prop.type == 'STRING':
|
||||
val_bytes = item.path_resolve(prop.identifier, False).as_bytes()
|
||||
val_utf8 = val_bytes.decode("utf-8", "replace")
|
||||
val_bytes_valid = val_utf8.encode("utf-8")
|
||||
if val_bytes_valid != val_bytes:
|
||||
print("found bad utf8 encoded string {!r}, fixing to {!r} ({!r})...".format(
|
||||
val_bytes, val_bytes_valid, val_utf8,
|
||||
))
|
||||
setattr(item, prop.identifier, val_utf8)
|
||||
changed = True
|
||||
elif prop.type == 'POINTER':
|
||||
it = getattr(item, prop.identifier)
|
||||
changed |= self.validate_strings(it, done_items)
|
||||
elif prop.type == 'COLLECTION':
|
||||
for it in getattr(item, prop.identifier):
|
||||
changed |= self.validate_strings(it, done_items)
|
||||
return changed
|
||||
|
||||
def execute(self, _context):
|
||||
changed = False
|
||||
done_items = set()
|
||||
for prop in bpy.data.bl_rna.properties:
|
||||
if prop.type == 'COLLECTION':
|
||||
for it in getattr(bpy.data, prop.identifier):
|
||||
changed |= self.validate_strings(it, done_items)
|
||||
if changed:
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
"Some strings were fixed, don't forget to save the .blend file to keep those changes",
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
WM_OT_previews_batch_clear,
|
||||
WM_OT_previews_batch_generate,
|
||||
WM_OT_blend_strings_utf8_validate,
|
||||
)
|
||||
226
blender-5.2.0/scripts/startup/bl_operators/freestyle.py
Normal file
226
blender-5.2.0/scripts/startup/bl_operators/freestyle.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from bpy.app.translations import (
|
||||
pgettext_rpt as rpt_,
|
||||
)
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
|
||||
class SCENE_OT_freestyle_fill_range_by_selection(Operator):
|
||||
"""Fill the Range Min/Max entries by the min/max distance between selected mesh objects and the source object """ \
|
||||
"""(either a user-specified object or the active camera)"""
|
||||
bl_idname = "scene.freestyle_fill_range_by_selection"
|
||||
bl_label = "Fill Range by Selection"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
type: EnumProperty(
|
||||
name="Type", description="Type of the modifier to work on",
|
||||
items=(
|
||||
('COLOR', "Color", "Color modifier type"),
|
||||
('ALPHA', "Alpha", "Alpha modifier type"),
|
||||
('THICKNESS', "Thickness", "Thickness modifier type"),
|
||||
),
|
||||
)
|
||||
name: StringProperty(
|
||||
name="Name",
|
||||
description="Name of the modifier to work on",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
view_layer = context.view_layer
|
||||
return view_layer and view_layer.freestyle_settings.linesets.active
|
||||
|
||||
def execute(self, context):
|
||||
import sys
|
||||
|
||||
scene = context.scene
|
||||
view_layer = context.view_layer
|
||||
lineset = view_layer.freestyle_settings.linesets.active
|
||||
linestyle = lineset.linestyle
|
||||
# Find the modifier to work on
|
||||
if self.type == 'COLOR':
|
||||
m = linestyle.color_modifiers[self.name]
|
||||
elif self.type == 'ALPHA':
|
||||
m = linestyle.alpha_modifiers[self.name]
|
||||
else:
|
||||
m = linestyle.thickness_modifiers[self.name]
|
||||
# Find the reference object
|
||||
if m.type == 'DISTANCE_FROM_CAMERA':
|
||||
ref = scene.camera
|
||||
if ref is None:
|
||||
self.report({'ERROR'}, "No active camera in the scene")
|
||||
return {'CANCELLED'}
|
||||
matrix_to_camera = ref.matrix_world.inverted()
|
||||
elif m.type == 'DISTANCE_FROM_OBJECT':
|
||||
if m.target is None:
|
||||
self.report({'ERROR'}, "Target object not specified")
|
||||
return {'CANCELLED'}
|
||||
ref = m.target
|
||||
target_location = ref.location
|
||||
else:
|
||||
self.report({'ERROR'}, rpt_("Unexpected modifier type: {:s}").format(m.type))
|
||||
return {'CANCELLED'}
|
||||
# Find selected vertices in edit-mesh.
|
||||
ob = context.active_object
|
||||
if ob.type == 'MESH' and ob.mode == 'EDIT' and ob.name != ref.name:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
selected_verts = [v for v in ob.data.vertices if v.select]
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
# Compute the min/max distance from the reference to mesh vertices
|
||||
min_dist = sys.float_info.max
|
||||
max_dist = -min_dist
|
||||
if m.type == 'DISTANCE_FROM_CAMERA':
|
||||
ob_to_cam = matrix_to_camera @ ob.matrix_world
|
||||
for vert in selected_verts:
|
||||
# dist in the camera space
|
||||
dist = (ob_to_cam @ vert.co).length
|
||||
min_dist = min(dist, min_dist)
|
||||
max_dist = max(dist, max_dist)
|
||||
elif m.type == 'DISTANCE_FROM_OBJECT':
|
||||
for vert in selected_verts:
|
||||
# dist in the world space
|
||||
dist = (ob.matrix_world @ vert.co - target_location).length
|
||||
min_dist = min(dist, min_dist)
|
||||
max_dist = max(dist, max_dist)
|
||||
# Fill the Range Min/Max entries with the computed distances
|
||||
m.range_min = min_dist
|
||||
m.range_max = max_dist
|
||||
return {'FINISHED'}
|
||||
# Find selected mesh objects
|
||||
selection = [ob for ob in scene.objects if ob.select_get() and ob.type == 'MESH' and ob.name != ref.name]
|
||||
if selection:
|
||||
# Compute the min/max distance from the reference to mesh vertices
|
||||
min_dist = sys.float_info.max
|
||||
max_dist = -min_dist
|
||||
if m.type == 'DISTANCE_FROM_CAMERA':
|
||||
for ob in selection:
|
||||
ob_to_cam = matrix_to_camera @ ob.matrix_world
|
||||
for vert in ob.data.vertices:
|
||||
# dist in the camera space
|
||||
dist = (ob_to_cam @ vert.co).length
|
||||
min_dist = min(dist, min_dist)
|
||||
max_dist = max(dist, max_dist)
|
||||
elif m.type == 'DISTANCE_FROM_OBJECT':
|
||||
for ob in selection:
|
||||
for vert in ob.data.vertices:
|
||||
# dist in the world space
|
||||
dist = (ob.matrix_world @ vert.co - target_location).length
|
||||
min_dist = min(dist, min_dist)
|
||||
max_dist = max(dist, max_dist)
|
||||
# Fill the Range Min/Max entries with the computed distances
|
||||
m.range_min = min_dist
|
||||
m.range_max = max_dist
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SCENE_OT_freestyle_add_edge_marks_to_keying_set(Operator):
|
||||
"""Add the data paths to the Freestyle Edge Mark property of selected edges to the active keying set"""
|
||||
bl_idname = "scene.freestyle_add_edge_marks_to_keying_set"
|
||||
bl_label = "Add Edge Marks to Keying Set"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.active_object
|
||||
return (ob and ob.type == 'MESH')
|
||||
|
||||
def execute(self, context):
|
||||
# active keying set
|
||||
scene = context.scene
|
||||
ks = scene.keying_sets.active
|
||||
if ks is None:
|
||||
ks = scene.keying_sets.new(idname="FreestyleEdgeMarkKeyingSet", name="Freestyle Edge Mark Keying Set")
|
||||
ks.bl_description = ""
|
||||
# add data paths to the keying set
|
||||
ob = context.active_object
|
||||
ob_mode = ob.mode
|
||||
mesh = ob.data
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
for i, edge in enumerate(mesh.edges):
|
||||
if not edge.hide and edge.select:
|
||||
path = "attributes[\"freestyle_edge\"].data[{:d}].value".format(i)
|
||||
ks.paths.add(mesh, path, index=0)
|
||||
bpy.ops.object.mode_set(mode=ob_mode, toggle=False)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SCENE_OT_freestyle_add_face_marks_to_keying_set(Operator):
|
||||
"""Add the data paths to the Freestyle Face Mark property of selected polygons to the active keying set"""
|
||||
bl_idname = "scene.freestyle_add_face_marks_to_keying_set"
|
||||
bl_label = "Add Face Marks to Keying Set"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.active_object
|
||||
return (ob and ob.type == 'MESH')
|
||||
|
||||
def execute(self, context):
|
||||
# active keying set
|
||||
scene = context.scene
|
||||
ks = scene.keying_sets.active
|
||||
if ks is None:
|
||||
ks = scene.keying_sets.new(idname="FreestyleFaceMarkKeyingSet", name="Freestyle Face Mark Keying Set")
|
||||
ks.bl_description = ""
|
||||
# add data paths to the keying set
|
||||
ob = context.active_object
|
||||
ob_mode = ob.mode
|
||||
mesh = ob.data
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
for i, polygon in enumerate(mesh.polygons):
|
||||
if not polygon.hide and polygon.select:
|
||||
path = "attributes[\"freestyle_face\"].data[{:d}].value".format(i)
|
||||
ks.paths.add(mesh, path, index=0)
|
||||
bpy.ops.object.mode_set(mode=ob_mode, toggle=False)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SCENE_OT_freestyle_module_open(Operator):
|
||||
"""Open a style module file"""
|
||||
bl_idname = "scene.freestyle_module_open"
|
||||
bl_label = "Open Style Module File"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
filepath: StringProperty(subtype='FILE_PATH')
|
||||
|
||||
make_internal: BoolProperty(
|
||||
name="Make internal",
|
||||
description="Make module file internal after loading",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
view_layer = context.view_layer
|
||||
return view_layer and view_layer.freestyle_settings.mode == 'SCRIPT'
|
||||
|
||||
def invoke(self, context, _event):
|
||||
self.freestyle_module = context.freestyle_module
|
||||
wm = context.window_manager
|
||||
wm.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, _context):
|
||||
text = bpy.data.texts.load(self.filepath, internal=self.make_internal)
|
||||
self.freestyle_module.script = text
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
SCENE_OT_freestyle_add_edge_marks_to_keying_set,
|
||||
SCENE_OT_freestyle_add_face_marks_to_keying_set,
|
||||
SCENE_OT_freestyle_fill_range_by_selection,
|
||||
SCENE_OT_freestyle_module_open,
|
||||
)
|
||||
384
blender-5.2.0/scripts/startup/bl_operators/geometry_nodes.py
Normal file
384
blender-5.2.0/scripts/startup/bl_operators/geometry_nodes.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty
|
||||
|
||||
from bpy.app.translations import pgettext_data as data_
|
||||
|
||||
|
||||
def add_empty_geometry_node_group(name, add_geometry_input=True):
|
||||
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
|
||||
|
||||
if add_geometry_input:
|
||||
group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
input_node = group.nodes.new('NodeGroupInput')
|
||||
input_node.select = False
|
||||
input_node.location.x = -200 - input_node.width
|
||||
|
||||
group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
output_node = group.nodes.new('NodeGroupOutput')
|
||||
output_node.is_active_output = True
|
||||
output_node.select = False
|
||||
output_node.location.x = 200
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def geometry_node_group_empty_new(name, add_geometry_input=True):
|
||||
group = add_empty_geometry_node_group(name, add_geometry_input)
|
||||
if add_geometry_input:
|
||||
group.links.new(group.nodes[data_("Group Input")].outputs[0], group.nodes[data_("Group Output")].inputs[0])
|
||||
return group
|
||||
|
||||
|
||||
def geometry_node_group_empty_modifier_new(name, add_geometry_input=True):
|
||||
group = geometry_node_group_empty_new(name, add_geometry_input)
|
||||
group.is_modifier = True
|
||||
return group
|
||||
|
||||
|
||||
def geometry_node_group_empty_tool_new(context):
|
||||
import re
|
||||
|
||||
group = geometry_node_group_empty_new(data_("Tool"))
|
||||
# Node tools have fake users by default, otherwise Blender will delete them since they have no users.
|
||||
group.use_fake_user = True
|
||||
group.is_tool = True
|
||||
|
||||
# Operator identifier names only support lowercase ASCII characters or numbers.
|
||||
group.node_tool_idname = "geometry." + re.sub('[^0-9a-z]+', '_', group.name.strip().lower())
|
||||
|
||||
ob = context.object
|
||||
ob_type = ob.type if ob else 'MESH'
|
||||
if ob_type == 'CURVES':
|
||||
group.is_type_curve = True
|
||||
elif ob_type == 'POINTCLOUD':
|
||||
group.is_type_pointcloud = True
|
||||
elif ob_type == 'GREASEPENCIL':
|
||||
group.is_type_grease_pencil = True
|
||||
else:
|
||||
group.is_type_mesh = True
|
||||
|
||||
mode = ob.mode if ob else 'OBJECT'
|
||||
if mode in {'SCULPT', 'SCULPT_CURVES', 'SCULPT_GREASE_PENCIL'}:
|
||||
group.is_mode_sculpt = True
|
||||
elif mode == 'PAINT_GREASE_PENCIL':
|
||||
group.is_mode_paint = True
|
||||
elif mode == 'EDIT':
|
||||
group.is_mode_edit = True
|
||||
else:
|
||||
group.is_mode_object = True
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def geometry_modifier_poll(context):
|
||||
ob = context.object
|
||||
|
||||
# Test object support for geometry node modifier
|
||||
if not ob or ob.type not in {'MESH', 'POINTCLOUD', 'VOLUME', 'CURVE', 'FONT', 'CURVES', 'GREASEPENCIL', 'EMPTY'}:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_context_modifier(context):
|
||||
# Context only has a "modifier" attribute in the modifier extra operators drop-down.
|
||||
modifier = getattr(context, "modifier", ...)
|
||||
if modifier is ...:
|
||||
ob = context.object
|
||||
if ob is None:
|
||||
return None
|
||||
modifier = ob.modifiers.active
|
||||
if modifier is None or modifier.type != 'NODES':
|
||||
return None
|
||||
return modifier
|
||||
|
||||
|
||||
def edit_geometry_nodes_modifier_poll(context):
|
||||
modifier = get_context_modifier(context)
|
||||
if modifier is None:
|
||||
return False
|
||||
return modifier.id_data.is_editable
|
||||
|
||||
|
||||
def socket_idname_to_attribute_type(idname):
|
||||
if idname.startswith("NodeSocketInt"):
|
||||
return 'INT'
|
||||
elif idname.startswith("NodeSocketColor"):
|
||||
return 'FLOAT_COLOR'
|
||||
elif idname.startswith("NodeSocketVector"):
|
||||
return 'FLOAT_VECTOR'
|
||||
elif idname.startswith("NodeSocketBool"):
|
||||
return 'BOOLEAN'
|
||||
elif idname.startswith("NodeSocketFloat"):
|
||||
return 'FLOAT'
|
||||
raise ValueError("Unsupported socket type")
|
||||
|
||||
|
||||
def get_socket_with_identifier(sockets, identifier):
|
||||
for socket in sockets:
|
||||
if socket.identifier == identifier:
|
||||
return socket
|
||||
return None
|
||||
|
||||
|
||||
def get_enabled_socket_with_name(sockets, name):
|
||||
for socket in sockets:
|
||||
if socket.name == name and socket.enabled:
|
||||
return socket
|
||||
return None
|
||||
|
||||
|
||||
def create_wrapper_group(operator, modifier, old_group):
|
||||
wrapper_name = old_group.name + ".wrapper"
|
||||
group = bpy.data.node_groups.new(wrapper_name, 'GeometryNodeTree')
|
||||
group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
group.is_modifier = True
|
||||
|
||||
first_geometry_input = next(
|
||||
(
|
||||
item for item in old_group.interface.items_tree if item.item_type == 'SOCKET' and
|
||||
item.in_out == 'INPUT' and
|
||||
item.bl_socket_idname == 'NodeSocketGeometry'
|
||||
),
|
||||
None,
|
||||
)
|
||||
if first_geometry_input:
|
||||
group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
|
||||
group_input_node = group.nodes.new('NodeGroupInput')
|
||||
group_input_node.location.x = -200 - group_input_node.width
|
||||
group_input_node.select = False
|
||||
|
||||
group_output_node = group.nodes.new('NodeGroupOutput')
|
||||
group_output_node.is_active_output = True
|
||||
group_output_node.location.x = 200
|
||||
group_output_node.select = False
|
||||
|
||||
group_node = group.nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = old_group
|
||||
group_node.update()
|
||||
|
||||
# Copy default values for inputs and create named attribute input nodes.
|
||||
input_nodes = []
|
||||
for input_socket in old_group.interface.items_tree:
|
||||
if input_socket.item_type != 'SOCKET' or (input_socket.in_out not in {'INPUT', 'BOTH'}):
|
||||
continue
|
||||
identifier = input_socket.identifier
|
||||
group_node_input = get_socket_with_identifier(group_node.inputs, identifier)
|
||||
prop = getattr(modifier.properties.inputs, identifier)
|
||||
if hasattr(prop, "type") and prop.type == "ATTRIBUTE":
|
||||
input_node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
input_nodes.append(input_node)
|
||||
input_node.data_type = socket_idname_to_attribute_type(input_socket.bl_socket_idname)
|
||||
attribute_name = prop.attribute_name
|
||||
input_node.inputs["Name"].default_value = attribute_name
|
||||
output_socket = get_enabled_socket_with_name(input_node.outputs, "Attribute")
|
||||
group.links.new(output_socket, group_node_input)
|
||||
elif hasattr(input_socket, "default_value"):
|
||||
group_node_input.default_value = prop.value
|
||||
|
||||
if first_geometry_input:
|
||||
group.links.new(
|
||||
group_input_node.outputs[0],
|
||||
get_socket_with_identifier(group_node.inputs, first_geometry_input.identifier),
|
||||
)
|
||||
|
||||
# Adjust locations of named attribute input nodes and group input node to make some space.
|
||||
if input_nodes:
|
||||
for i, node in enumerate(input_nodes):
|
||||
node.location.x = -175
|
||||
node.location.y = i * -50
|
||||
group_input_node.location.x = -350
|
||||
|
||||
# Connect outputs to store named attribute nodes to replace modifier attribute outputs.
|
||||
store_nodes = []
|
||||
first_geometry_output = None
|
||||
for output_socket in old_group.interface.items_tree:
|
||||
if output_socket.item_type != 'SOCKET' or (output_socket.in_out not in {'OUTPUT', 'BOTH'}):
|
||||
continue
|
||||
identifier = output_socket.identifier
|
||||
group_node_output = get_socket_with_identifier(group_node.outputs, identifier)
|
||||
|
||||
attribute_name = getattr(group_node_output, "attribute_name", None)
|
||||
if attribute_name:
|
||||
store_node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store_nodes.append(store_node)
|
||||
store_node.data_type = socket_idname_to_attribute_type(output_socket.bl_socket_idname)
|
||||
store_node.domain = output_socket.attribute_domain
|
||||
store_node.inputs["Name"].default_value = attribute_name
|
||||
input_socket = get_enabled_socket_with_name(store_node.inputs, "Value")
|
||||
group.links.new(group_node_output, input_socket)
|
||||
elif output_socket.bl_socket_idname == 'NodeSocketGeometry':
|
||||
if not first_geometry_output:
|
||||
first_geometry_output = group_node_output
|
||||
|
||||
# Adjust locations of store named attribute nodes and move group output.
|
||||
# Note that the node group has its sockets names translated, while the built-in nodes don't.
|
||||
if store_nodes:
|
||||
for i, node in enumerate(store_nodes):
|
||||
node.location.x = (i + 1) * 175
|
||||
node.location.y = 0
|
||||
group_output_node.location.x = (len(store_nodes) + 1) * 175
|
||||
|
||||
group.links.new(first_geometry_output, store_nodes[0].inputs["Geometry"])
|
||||
for i in range(len(store_nodes) - 1):
|
||||
group.links.new(store_nodes[i].outputs["Geometry"], store_nodes[i + 1].inputs["Geometry"])
|
||||
|
||||
group.links.new(store_nodes[-1].outputs["Geometry"], group_output_node.inputs[data_("Geometry")])
|
||||
else:
|
||||
if not first_geometry_output:
|
||||
operator.report({'WARNING'}, "Node group must have a geometry output")
|
||||
return None
|
||||
group.links.new(first_geometry_output, group_output_node.inputs[data_("Geometry")])
|
||||
|
||||
return group
|
||||
|
||||
|
||||
class MoveModifierToNodes(Operator):
|
||||
"""Move inputs and outputs from in the modifier to a new node group"""
|
||||
|
||||
bl_idname = "object.geometry_nodes_move_to_nodes"
|
||||
bl_label = "Move to Nodes"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
use_selected_objects: BoolProperty(
|
||||
name="Selected Objects",
|
||||
description="Affect all selected objects instead of just the active object",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return edit_geometry_nodes_modifier_poll(context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.alt:
|
||||
self.use_selected_objects = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
active_modifier = get_context_modifier(context)
|
||||
if not active_modifier:
|
||||
return {'CANCELLED'}
|
||||
modifier_name = active_modifier.name
|
||||
|
||||
objects = []
|
||||
if self.use_selected_objects:
|
||||
objects = context.selected_editable_objects
|
||||
else:
|
||||
objects = [context.object]
|
||||
|
||||
for ob in objects:
|
||||
modifier = ob.modifiers[modifier_name]
|
||||
if not modifier:
|
||||
continue
|
||||
old_group = modifier.node_group
|
||||
if not old_group:
|
||||
continue
|
||||
new_group = create_wrapper_group(self, modifier, old_group)
|
||||
if new_group:
|
||||
modifier.node_group = new_group
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NewGeometryNodesModifier(Operator):
|
||||
"""Create a new modifier with a new geometry node group"""
|
||||
|
||||
bl_idname = "node.new_geometry_nodes_modifier"
|
||||
bl_label = "New Geometry Node Modifier"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return geometry_modifier_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
modifier = ob.modifiers.new(data_("GeometryNodes"), 'NODES')
|
||||
if not modifier:
|
||||
return {'CANCELLED'}
|
||||
|
||||
is_first_modifier = ob.modifiers[0] == modifier
|
||||
# For empty objects, don't add a geometry input for the first modifier
|
||||
add_geometry_input = not (ob.type == 'EMPTY' and ob.instance_collection is None and is_first_modifier)
|
||||
group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"), add_geometry_input)
|
||||
modifier.node_group = group
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NewGeometryNodeTreeAssign(Operator):
|
||||
"""Create a new geometry node group and assign it to the active modifier"""
|
||||
|
||||
bl_idname = "node.new_geometry_node_group_assign"
|
||||
bl_label = "Assign New Geometry Node Group"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return geometry_modifier_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
modifier = get_context_modifier(context)
|
||||
if not modifier:
|
||||
return {'CANCELLED'}
|
||||
|
||||
ob = context.object
|
||||
is_first_modifier = ob.modifiers[0] == modifier
|
||||
# For empty objects, don't add a geometry input for the first modifier
|
||||
add_geometry_input = not (ob.type == 'EMPTY' and ob.instance_collection is None and is_first_modifier)
|
||||
group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"), add_geometry_input)
|
||||
modifier.node_group = group
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NewGeometryNodeGroupTool(Operator):
|
||||
"""Create a new geometry node group for a tool"""
|
||||
bl_idname = "node.new_geometry_node_group_tool"
|
||||
bl_label = "New Geometry Node Tool Group"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return space and space.type == 'NODE_EDITOR' and space.node_tree_sub_type == 'TOOL'
|
||||
|
||||
def execute(self, context):
|
||||
group = geometry_node_group_empty_tool_new(context)
|
||||
context.space_data.selected_node_group = group
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ZoneOperator:
|
||||
@classmethod
|
||||
def get_node(cls, context):
|
||||
node = context.active_node
|
||||
if node is None:
|
||||
return None
|
||||
if node.bl_idname == cls.output_node_type:
|
||||
return node
|
||||
if node.bl_idname == cls.input_node_type:
|
||||
return node.paired_output
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
# Needs active node editor and a tree.
|
||||
if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or not space.edit_tree.is_editable:
|
||||
return False
|
||||
if cls.get_node(context) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
classes = (
|
||||
NewGeometryNodesModifier,
|
||||
NewGeometryNodeTreeAssign,
|
||||
NewGeometryNodeGroupTool,
|
||||
MoveModifierToNodes,
|
||||
)
|
||||
63
blender-5.2.0/scripts/startup/bl_operators/grease_pencil.py
Normal file
63
blender-5.2.0/scripts/startup/bl_operators/grease_pencil.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
class GREASE_PENCIL_OT_relative_layer_mask_add(Operator):
|
||||
"""Mask active layer with layer above or below"""
|
||||
|
||||
bl_idname = "grease_pencil.relative_layer_mask_add"
|
||||
bl_label = "Mask with Layer Above/Below"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode: EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
('ABOVE', "Above", ""),
|
||||
('BELOW', "Below", "")
|
||||
),
|
||||
description="Which relative layer (above or below) to use as a mask",
|
||||
default='ABOVE',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
(obj := context.active_object) is not None and
|
||||
obj.is_editable and
|
||||
obj.type == 'GREASEPENCIL' and
|
||||
obj.data.layers.active is not None and
|
||||
obj.data.is_editable
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
active_layer = obj.data.layers.active
|
||||
|
||||
if self.mode == 'ABOVE':
|
||||
masking_layer = active_layer.next_node
|
||||
elif self.mode == 'BELOW':
|
||||
masking_layer = active_layer.prev_node
|
||||
|
||||
if masking_layer is None or type(masking_layer) != bpy.types.GreasePencilLayer:
|
||||
self.report({'ERROR'}, "No layer found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if masking_layer.name in active_layer.mask_layers:
|
||||
self.report({'ERROR'}, "Layer is already added as a mask")
|
||||
return {'CANCELLED'}
|
||||
|
||||
bpy.ops.grease_pencil.layer_mask_add(name=masking_layer.name)
|
||||
active_layer.use_masks = True
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
GREASE_PENCIL_OT_relative_layer_mask_add,
|
||||
)
|
||||
322
blender-5.2.0/scripts/startup/bl_operators/image.py
Normal file
322
blender-5.2.0/scripts/startup/bl_operators/image.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
FileHandler,
|
||||
Operator,
|
||||
OperatorFileListElement,
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
|
||||
|
||||
class EditExternally(Operator):
|
||||
"""Edit image in an external application"""
|
||||
bl_idname = "image.external_edit"
|
||||
bl_label = "Image Edit Externally"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
filepath: StringProperty(
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _editor_guess(context):
|
||||
import sys
|
||||
|
||||
image_editor = context.preferences.filepaths.image_editor
|
||||
|
||||
# use image editor in the preferences when available.
|
||||
if not image_editor:
|
||||
if sys.platform[:3] == "win":
|
||||
image_editor = ["start"] # not tested!
|
||||
elif sys.platform == "darwin":
|
||||
image_editor = ["open"]
|
||||
else:
|
||||
image_editor = ["gimp"]
|
||||
else:
|
||||
if sys.platform == "darwin":
|
||||
# blender file selector treats .app as a folder
|
||||
# and will include a trailing backslash, so we strip it.
|
||||
image_editor.rstrip('\\')
|
||||
image_editor = ["open", "-a", image_editor]
|
||||
else:
|
||||
image_editor = [image_editor]
|
||||
|
||||
return image_editor
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
filepath = self.filepath
|
||||
|
||||
if not filepath:
|
||||
self.report({'ERROR'}, "Image path not set")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not os.path.exists(filepath) or not os.path.isfile(filepath):
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
rpt_("Image path {!r} not found, image may be packed or unsaved").format(filepath),
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
cmd = self._editor_guess(context) + [filepath]
|
||||
|
||||
try:
|
||||
subprocess.Popen(cmd)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Image editor could not be launched, ensure that "
|
||||
"the path in User Preferences > File is valid, and Blender has rights to launch it",
|
||||
)
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
import os
|
||||
sd = context.space_data
|
||||
try:
|
||||
image = sd.image
|
||||
except AttributeError:
|
||||
self.report({'ERROR'}, "Context incorrect, image not found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if image.packed_file:
|
||||
self.report({'ERROR'}, "Image is packed, unpack before editing")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if sd.type == 'IMAGE_EDITOR':
|
||||
filepath = image.filepath_from_user(image_user=sd.image_user)
|
||||
else:
|
||||
filepath = image.filepath
|
||||
|
||||
filepath = bpy.path.abspath(filepath, library=image.library)
|
||||
|
||||
self.filepath = os.path.normpath(filepath)
|
||||
self.execute(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ProjectEdit(Operator):
|
||||
"""Edit a snapshot of the 3D Viewport in an external image editor"""
|
||||
bl_idname = "image.project_edit"
|
||||
bl_label = "Project Edit"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
_proj_hack = [""]
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
|
||||
EXT = "png" # could be made an option but for now ok
|
||||
|
||||
for image in bpy.data.images:
|
||||
image.tag = True
|
||||
|
||||
# opengl buffer may fail, we can't help this, but best report it.
|
||||
try:
|
||||
bpy.ops.paint.image_from_view()
|
||||
except RuntimeError as ex:
|
||||
self.report({'ERROR'}, str(ex))
|
||||
return {'CANCELLED'}
|
||||
|
||||
image_new = None
|
||||
for image in bpy.data.images:
|
||||
if not image.tag:
|
||||
image_new = image
|
||||
break
|
||||
|
||||
if not image_new:
|
||||
self.report({'ERROR'}, "Could not make new image")
|
||||
return {'CANCELLED'}
|
||||
|
||||
filepath = os.path.basename(bpy.data.filepath)
|
||||
filepath = os.path.splitext(filepath)[0]
|
||||
# fixes <memory> rubbish, needs checking
|
||||
# filepath = bpy.path.clean_name(filepath)
|
||||
|
||||
if bpy.data.is_saved:
|
||||
filepath = "//" + filepath
|
||||
else:
|
||||
filepath = os.path.join(bpy.app.tempdir, "project_edit")
|
||||
|
||||
obj = context.object
|
||||
|
||||
if obj:
|
||||
filepath += "_" + bpy.path.clean_name(obj.name)
|
||||
|
||||
filepath_final = filepath + "." + EXT
|
||||
i = 0
|
||||
|
||||
while os.path.exists(bpy.path.abspath(filepath_final)):
|
||||
filepath_final = filepath + "{:03d}.{:s}".format(i, EXT)
|
||||
i += 1
|
||||
|
||||
image_new.name = bpy.path.basename(filepath_final)
|
||||
ProjectEdit._proj_hack[0] = image_new.name
|
||||
|
||||
image_new.filepath_raw = filepath_final # TODO, filepath raw is crummy
|
||||
image_new.file_format = 'PNG'
|
||||
image_new.save()
|
||||
|
||||
filepath_final = bpy.path.abspath(filepath_final)
|
||||
|
||||
try:
|
||||
bpy.ops.image.external_edit(filepath=filepath_final)
|
||||
except RuntimeError as ex:
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ProjectApply(Operator):
|
||||
"""Project edited image back onto the object"""
|
||||
bl_idname = "image.project_apply"
|
||||
bl_label = "Project Apply"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
def execute(self, _context):
|
||||
image_name = ProjectEdit._proj_hack[0] # TODO, deal with this nicer
|
||||
image = bpy.data.images.get((image_name, None))
|
||||
if image is None:
|
||||
self.report({'ERROR'}, rpt_("Could not find image '{:s}'").format(image_name))
|
||||
return {'CANCELLED'}
|
||||
|
||||
image.reload()
|
||||
bpy.ops.paint.project_image(image=image_name)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
bl_file_extensions_image_movie = (*bpy.path.extensions_image, *bpy.path.extensions_movie)
|
||||
|
||||
|
||||
class IMAGE_OT_open_images(Operator):
|
||||
bl_idname = "image.open_images"
|
||||
bl_label = "Open Images"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
directory: StringProperty(
|
||||
subtype='FILE_PATH',
|
||||
options={'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
files: CollectionProperty(
|
||||
type=OperatorFileListElement,
|
||||
options={'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
relative_path: BoolProperty(
|
||||
name="Relative Path",
|
||||
default=True,
|
||||
)
|
||||
use_sequence_detection: BoolProperty(
|
||||
name="Detect Sequence",
|
||||
default=True,
|
||||
)
|
||||
use_udim_detection: BoolProperty(
|
||||
name="Detect UDIM",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area and context.area.type == 'IMAGE_EDITOR'
|
||||
|
||||
def execute(self, context):
|
||||
if not self.directory or len(self.files) == 0:
|
||||
return {'CANCELLED'}
|
||||
# List of files that are not part of an image sequence or UDIM group.
|
||||
files = []
|
||||
# Groups of files that may be part of an image sequence or a UDIM group.
|
||||
sequences = []
|
||||
import re
|
||||
regex_extension = re.compile(
|
||||
"(" + "|".join([re.escape(ext) for ext in bl_file_extensions_image_movie]) + ")$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
regex_sequence = re.compile("(\\d+)(\\.[\\w\\d]+)$")
|
||||
for file in self.files:
|
||||
# Filter by extension
|
||||
if not regex_extension.search(file.name):
|
||||
continue
|
||||
match = regex_sequence.search(file.name)
|
||||
if not (match and (self.use_sequence_detection or self.use_udim_detection)):
|
||||
files.append(file.name)
|
||||
continue
|
||||
seq = {
|
||||
"prefix": file.name[:len(file.name) - len(match.group(0))],
|
||||
"ext": match.group(2),
|
||||
"frame_size": len(match.group(1)),
|
||||
"files": [file.name],
|
||||
}
|
||||
for test_seq in sequences:
|
||||
if (
|
||||
(test_seq["prefix"] == seq["prefix"]) and
|
||||
(test_seq["ext"] == seq["ext"]) and
|
||||
(test_seq["frame_size"] == seq["frame_size"])
|
||||
):
|
||||
test_seq["files"].append(file.name)
|
||||
seq = None
|
||||
break
|
||||
if seq:
|
||||
sequences.append(seq)
|
||||
|
||||
import os
|
||||
for file in files:
|
||||
filepath = os.path.join(self.directory, file)
|
||||
bpy.ops.image.open(filepath=filepath, relative_path=self.relative_path)
|
||||
for seq in sequences:
|
||||
seq["files"].sort()
|
||||
filepath = os.path.join(self.directory, seq["files"][0])
|
||||
files = [{"name": file} for file in seq["files"]]
|
||||
bpy.ops.image.open(
|
||||
filepath=filepath,
|
||||
directory=self.directory,
|
||||
files=files,
|
||||
use_sequence_detection=self.use_sequence_detection,
|
||||
use_udim_detecting=self.use_udim_detection,
|
||||
relative_path=self.relative_path,
|
||||
)
|
||||
is_tiled = context.edit_image.source == 'TILED'
|
||||
if len(files) > 1 and self.use_sequence_detection and not is_tiled:
|
||||
context.edit_image.name = "{:s}{:s}{:s}".format(seq["prefix"], ("#" * seq["frame_size"]), seq["ext"])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class IMAGE_FH_drop_handler(FileHandler):
|
||||
bl_idname = "IMAGE_FH_drop_handler"
|
||||
bl_label = "Open images"
|
||||
bl_import_operator = "image.open_images"
|
||||
bl_file_extensions = ";".join(bl_file_extensions_image_movie)
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
return (
|
||||
(context.area is not None) and
|
||||
(context.area.type == 'IMAGE_EDITOR') and
|
||||
(context.region is not None) and
|
||||
(context.region.type == 'WINDOW')
|
||||
)
|
||||
|
||||
|
||||
classes = (
|
||||
EditExternally,
|
||||
ProjectApply,
|
||||
IMAGE_OT_open_images,
|
||||
IMAGE_FH_drop_handler,
|
||||
ProjectEdit,
|
||||
)
|
||||
1218
blender-5.2.0/scripts/startup/bl_operators/image_as_planes.py
Normal file
1218
blender-5.2.0/scripts/startup/bl_operators/image_as_planes.py
Normal file
File diff suppressed because it is too large
Load Diff
61
blender-5.2.0/scripts/startup/bl_operators/mesh.py
Normal file
61
blender-5.2.0/scripts/startup/bl_operators/mesh.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
class MeshSelectNext(Operator):
|
||||
"""Select the next element (using selection order)"""
|
||||
bl_idname = "mesh.select_next_item"
|
||||
bl_label = "Select Next Element"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.mode == 'EDIT_MESH')
|
||||
|
||||
def execute(self, context):
|
||||
import bmesh
|
||||
from .bmesh import find_adjacent
|
||||
|
||||
obj = context.active_object
|
||||
me = obj.data
|
||||
bm = bmesh.from_edit_mesh(me)
|
||||
|
||||
if find_adjacent.select_next(bm, self.report):
|
||||
bm.select_flush_mode()
|
||||
bmesh.update_edit_mesh(me, loop_triangles=False)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class MeshSelectPrev(Operator):
|
||||
"""Select the previous element (using selection order)"""
|
||||
bl_idname = "mesh.select_prev_item"
|
||||
bl_label = "Select Previous Element"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.mode == 'EDIT_MESH')
|
||||
|
||||
def execute(self, context):
|
||||
import bmesh
|
||||
from .bmesh import find_adjacent
|
||||
|
||||
obj = context.active_object
|
||||
me = obj.data
|
||||
bm = bmesh.from_edit_mesh(me)
|
||||
|
||||
if find_adjacent.select_prev(bm, self.report):
|
||||
bm.select_flush_mode()
|
||||
bmesh.update_edit_mesh(me, loop_triangles=False)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
MeshSelectNext,
|
||||
MeshSelectPrev,
|
||||
)
|
||||
1595
blender-5.2.0/scripts/startup/bl_operators/node.py
Normal file
1595
blender-5.2.0/scripts/startup/bl_operators/node.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from bpy.app.translations import pgettext_tip as tip_
|
||||
|
||||
|
||||
def node_editor_poll(cls, context):
|
||||
space = context.space_data
|
||||
if space is None or (space.type != 'NODE_EDITOR'):
|
||||
cls.poll_message_set("Active editor is not a node editor.")
|
||||
return False
|
||||
if space.node_tree is None:
|
||||
cls.poll_message_set("Node tree was not found in the active node editor.")
|
||||
return False
|
||||
if space.node_tree.library is not None:
|
||||
cls.poll_message_set("Active node tree is linked from another .blend file.")
|
||||
return False
|
||||
if not space.edit_tree.nodes:
|
||||
cls.poll_message_set("Active node tree does not contain any nodes.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def node_space_type_poll(cls, context, types):
|
||||
if context.space_data.tree_type not in types:
|
||||
tree_types_str = ", ".join(t.split("NodeTree")[0].lower() for t in sorted(types))
|
||||
poll_message = tip_(
|
||||
"Current node tree type not supported.\n"
|
||||
"Should be one of {:s}."
|
||||
).format(tree_types_str)
|
||||
cls.poll_message_set(poll_message)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_group_output_node(tree, output_node_idname='NodeGroupOutput'):
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == output_node_idname and node.is_active_output:
|
||||
return node
|
||||
|
||||
|
||||
def get_output_location(tree):
|
||||
# get right-most location.
|
||||
sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
|
||||
max_xloc_node = sorted_by_xloc[-1]
|
||||
|
||||
# get average y location.
|
||||
sum_yloc = 0
|
||||
for node in tree.nodes:
|
||||
sum_yloc += node.location.y
|
||||
|
||||
loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
|
||||
loc_y = sum_yloc / len(tree.nodes)
|
||||
return loc_x, loc_y
|
||||
|
||||
|
||||
def get_internal_socket(socket):
|
||||
# get the internal socket from a socket inside or outside the group.
|
||||
node = socket.node
|
||||
if node.type == 'GROUP_OUTPUT':
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif node.type == 'GROUP_INPUT':
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif hasattr(node, "node_tree"):
|
||||
iterator = node.node_tree.interface.items_tree
|
||||
else:
|
||||
return None
|
||||
|
||||
for s in iterator:
|
||||
if s.identifier == socket.identifier:
|
||||
return s
|
||||
return iterator[0]
|
||||
|
||||
|
||||
def is_visible_socket(socket):
|
||||
return socket.is_icon_visible and socket.type != 'CUSTOM'
|
||||
|
||||
|
||||
def is_viewer_link(link, output_node):
|
||||
if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
|
||||
return True
|
||||
if link.to_node.type == 'GROUP_OUTPUT':
|
||||
socket = get_internal_socket(link.to_socket)
|
||||
if socket.is_inspect_output:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def force_update(context):
|
||||
context.space_data.node_tree.update_tag()
|
||||
|
||||
|
||||
class NodeEditorBase:
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return node_editor_poll(cls, context)
|
||||
1036
blender-5.2.0/scripts/startup/bl_operators/object.py
Normal file
1036
blender-5.2.0/scripts/startup/bl_operators/object.py
Normal file
File diff suppressed because it is too large
Load Diff
407
blender-5.2.0/scripts/startup/bl_operators/object_align.py
Normal file
407
blender-5.2.0/scripts/startup/bl_operators/object_align.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def worldspace_bounds_from_object_bounds(bb_world):
|
||||
|
||||
# Initialize the variables with the 8th vertex
|
||||
left, right, front, back, down, up = (
|
||||
bb_world[7][0],
|
||||
bb_world[7][0],
|
||||
bb_world[7][1],
|
||||
bb_world[7][1],
|
||||
bb_world[7][2],
|
||||
bb_world[7][2],
|
||||
)
|
||||
|
||||
# Test against the other 7 verts
|
||||
for i in range(7):
|
||||
|
||||
# X Range
|
||||
val = bb_world[i][0]
|
||||
if val < left:
|
||||
left = val
|
||||
|
||||
if val > right:
|
||||
right = val
|
||||
|
||||
# Y Range
|
||||
val = bb_world[i][1]
|
||||
if val < front:
|
||||
front = val
|
||||
|
||||
if val > back:
|
||||
back = val
|
||||
|
||||
# Z Range
|
||||
val = bb_world[i][2]
|
||||
if val < down:
|
||||
down = val
|
||||
|
||||
if val > up:
|
||||
up = val
|
||||
|
||||
return (Vector((left, front, up)), Vector((right, back, down)))
|
||||
|
||||
|
||||
def worldspace_bounds_from_object_data(depsgraph, obj):
|
||||
|
||||
matrix_world = obj.matrix_world.copy()
|
||||
|
||||
# Initialize the variables with the last vertex
|
||||
ob_eval = obj.evaluated_get(depsgraph)
|
||||
me = ob_eval.to_mesh()
|
||||
verts = me.vertices
|
||||
|
||||
val = matrix_world @ (verts[-1].co if verts else Vector((0.0, 0.0, 0.0)))
|
||||
|
||||
left, right, front, back, down, up = (
|
||||
val[0],
|
||||
val[0],
|
||||
val[1],
|
||||
val[1],
|
||||
val[2],
|
||||
val[2],
|
||||
)
|
||||
|
||||
# Test against all other verts
|
||||
for v in verts:
|
||||
vco = matrix_world @ v.co
|
||||
|
||||
# X Range
|
||||
val = vco[0]
|
||||
if val < left:
|
||||
left = val
|
||||
|
||||
if val > right:
|
||||
right = val
|
||||
|
||||
# Y Range
|
||||
val = vco[1]
|
||||
if val < front:
|
||||
front = val
|
||||
|
||||
if val > back:
|
||||
back = val
|
||||
|
||||
# Z Range
|
||||
val = vco[2]
|
||||
if val < down:
|
||||
down = val
|
||||
|
||||
if val > up:
|
||||
up = val
|
||||
|
||||
ob_eval.to_mesh_clear()
|
||||
|
||||
return Vector((left, front, up)), Vector((right, back, down))
|
||||
|
||||
|
||||
def align_objects(context, align_x, align_y, align_z, align_mode, relative_to, bb_quality):
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
scene = context.scene
|
||||
|
||||
cursor = scene.cursor.location
|
||||
|
||||
# We are accessing runtime data such as evaluated bounding box, so we need to
|
||||
# be sure it is properly updated and valid (bounding box might be lost on operator redo).
|
||||
context.view_layer.update()
|
||||
|
||||
Left_Front_Up_SEL = [0.0, 0.0, 0.0]
|
||||
Right_Back_Down_SEL = [0.0, 0.0, 0.0]
|
||||
|
||||
flag_first = True
|
||||
|
||||
objects = []
|
||||
|
||||
for obj in context.selected_objects:
|
||||
matrix_world = obj.matrix_world.copy()
|
||||
bb_world = [matrix_world @ Vector(v) for v in obj.bound_box]
|
||||
objects.append((obj, bb_world))
|
||||
|
||||
if not objects:
|
||||
return False
|
||||
|
||||
for obj, bb_world in objects:
|
||||
|
||||
if bb_quality and obj.type == 'MESH':
|
||||
GBB = worldspace_bounds_from_object_data(depsgraph, obj)
|
||||
else:
|
||||
GBB = worldspace_bounds_from_object_bounds(bb_world)
|
||||
|
||||
Left_Front_Up = GBB[0]
|
||||
Right_Back_Down = GBB[1]
|
||||
|
||||
# Active Center
|
||||
|
||||
if obj == context.active_object:
|
||||
|
||||
center_active_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0
|
||||
center_active_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0
|
||||
center_active_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0
|
||||
|
||||
size_active_x = (Right_Back_Down[0] - Left_Front_Up[0]) / 2.0
|
||||
size_active_y = (Right_Back_Down[1] - Left_Front_Up[1]) / 2.0
|
||||
size_active_z = (Left_Front_Up[2] - Right_Back_Down[2]) / 2.0
|
||||
|
||||
# Selection Center
|
||||
|
||||
if flag_first:
|
||||
flag_first = False
|
||||
|
||||
Left_Front_Up_SEL[0] = Left_Front_Up[0]
|
||||
Left_Front_Up_SEL[1] = Left_Front_Up[1]
|
||||
Left_Front_Up_SEL[2] = Left_Front_Up[2]
|
||||
|
||||
Right_Back_Down_SEL[0] = Right_Back_Down[0]
|
||||
Right_Back_Down_SEL[1] = Right_Back_Down[1]
|
||||
Right_Back_Down_SEL[2] = Right_Back_Down[2]
|
||||
|
||||
else:
|
||||
# X axis
|
||||
if Left_Front_Up[0] < Left_Front_Up_SEL[0]:
|
||||
Left_Front_Up_SEL[0] = Left_Front_Up[0]
|
||||
# Y axis
|
||||
if Left_Front_Up[1] < Left_Front_Up_SEL[1]:
|
||||
Left_Front_Up_SEL[1] = Left_Front_Up[1]
|
||||
# Z axis
|
||||
if Left_Front_Up[2] > Left_Front_Up_SEL[2]:
|
||||
Left_Front_Up_SEL[2] = Left_Front_Up[2]
|
||||
|
||||
# X axis
|
||||
if Right_Back_Down[0] > Right_Back_Down_SEL[0]:
|
||||
Right_Back_Down_SEL[0] = Right_Back_Down[0]
|
||||
# Y axis
|
||||
if Right_Back_Down[1] > Right_Back_Down_SEL[1]:
|
||||
Right_Back_Down_SEL[1] = Right_Back_Down[1]
|
||||
# Z axis
|
||||
if Right_Back_Down[2] < Right_Back_Down_SEL[2]:
|
||||
Right_Back_Down_SEL[2] = Right_Back_Down[2]
|
||||
|
||||
center_sel_x = (Left_Front_Up_SEL[0] + Right_Back_Down_SEL[0]) / 2.0
|
||||
center_sel_y = (Left_Front_Up_SEL[1] + Right_Back_Down_SEL[1]) / 2.0
|
||||
center_sel_z = (Left_Front_Up_SEL[2] + Right_Back_Down_SEL[2]) / 2.0
|
||||
|
||||
# Main Loop
|
||||
|
||||
for obj, bb_world in objects:
|
||||
matrix_world = obj.matrix_world.copy()
|
||||
bb_world = [matrix_world @ Vector(v[:]) for v in obj.bound_box]
|
||||
|
||||
if bb_quality and obj.type == 'MESH':
|
||||
GBB = worldspace_bounds_from_object_data(depsgraph, obj)
|
||||
else:
|
||||
GBB = worldspace_bounds_from_object_bounds(bb_world)
|
||||
|
||||
Left_Front_Up = GBB[0]
|
||||
Right_Back_Down = GBB[1]
|
||||
|
||||
center_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0
|
||||
center_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0
|
||||
center_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0
|
||||
|
||||
positive_x = Right_Back_Down[0]
|
||||
positive_y = Right_Back_Down[1]
|
||||
positive_z = Left_Front_Up[2]
|
||||
|
||||
negative_x = Left_Front_Up[0]
|
||||
negative_y = Left_Front_Up[1]
|
||||
negative_z = Right_Back_Down[2]
|
||||
|
||||
obj_loc = obj.location
|
||||
|
||||
if align_x:
|
||||
|
||||
# Align Mode
|
||||
|
||||
if relative_to == 'OPT_4': # Active relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_x = obj_loc[0] - negative_x - size_active_x
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_x = obj_loc[0] - positive_x + size_active_x
|
||||
|
||||
else: # Everything else relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_x = obj_loc[0] - negative_x
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_x = obj_loc[0] - positive_x
|
||||
|
||||
if align_mode == 'OPT_2': # All relative
|
||||
obj_x = obj_loc[0] - center_x
|
||||
|
||||
# Relative To
|
||||
|
||||
if relative_to == 'OPT_1':
|
||||
loc_x = obj_x
|
||||
|
||||
elif relative_to == 'OPT_2':
|
||||
loc_x = obj_x + cursor[0]
|
||||
|
||||
elif relative_to == 'OPT_3':
|
||||
loc_x = obj_x + center_sel_x
|
||||
|
||||
elif relative_to == 'OPT_4':
|
||||
loc_x = obj_x + center_active_x
|
||||
|
||||
obj.location[0] = loc_x
|
||||
|
||||
if align_y:
|
||||
# Align Mode
|
||||
|
||||
if relative_to == 'OPT_4': # Active relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_y = obj_loc[1] - negative_y - size_active_y
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_y = obj_loc[1] - positive_y + size_active_y
|
||||
|
||||
else: # Everything else relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_y = obj_loc[1] - negative_y
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_y = obj_loc[1] - positive_y
|
||||
|
||||
if align_mode == 'OPT_2': # All relative
|
||||
obj_y = obj_loc[1] - center_y
|
||||
|
||||
# Relative To
|
||||
|
||||
if relative_to == 'OPT_1':
|
||||
loc_y = obj_y
|
||||
|
||||
elif relative_to == 'OPT_2':
|
||||
loc_y = obj_y + cursor[1]
|
||||
|
||||
elif relative_to == 'OPT_3':
|
||||
loc_y = obj_y + center_sel_y
|
||||
|
||||
elif relative_to == 'OPT_4':
|
||||
loc_y = obj_y + center_active_y
|
||||
|
||||
obj.location[1] = loc_y
|
||||
|
||||
if align_z:
|
||||
# Align Mode
|
||||
if relative_to == 'OPT_4': # Active relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_z = obj_loc[2] - negative_z - size_active_z
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_z = obj_loc[2] - positive_z + size_active_z
|
||||
|
||||
else: # Everything else relative
|
||||
if align_mode == 'OPT_1':
|
||||
obj_z = obj_loc[2] - negative_z
|
||||
|
||||
elif align_mode == 'OPT_3':
|
||||
obj_z = obj_loc[2] - positive_z
|
||||
|
||||
if align_mode == 'OPT_2': # All relative
|
||||
obj_z = obj_loc[2] - center_z
|
||||
|
||||
# Relative To
|
||||
|
||||
if relative_to == 'OPT_1':
|
||||
loc_z = obj_z
|
||||
|
||||
elif relative_to == 'OPT_2':
|
||||
loc_z = obj_z + cursor[2]
|
||||
|
||||
elif relative_to == 'OPT_3':
|
||||
loc_z = obj_z + center_sel_z
|
||||
|
||||
elif relative_to == 'OPT_4':
|
||||
loc_z = obj_z + center_active_z
|
||||
|
||||
obj.location[2] = loc_z
|
||||
|
||||
return True
|
||||
|
||||
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
class AlignObjects(Operator):
|
||||
"""Align objects"""
|
||||
bl_idname = "object.align"
|
||||
bl_label = "Align Objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
bb_quality: BoolProperty(
|
||||
name="High Quality",
|
||||
description=(
|
||||
"Enables high quality but slow calculation of the "
|
||||
"bounding box for perfect results on complex "
|
||||
"shape meshes with rotation/scale"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
align_mode: EnumProperty(
|
||||
name="Align Mode",
|
||||
description="Side of object to use for alignment",
|
||||
items=(
|
||||
('OPT_1', "Negative Sides", ""),
|
||||
('OPT_2', "Centers", ""),
|
||||
('OPT_3', "Positive Sides", ""),
|
||||
),
|
||||
default='OPT_2',
|
||||
)
|
||||
relative_to: EnumProperty(
|
||||
name="Relative To",
|
||||
description="Reference location to align to",
|
||||
items=(
|
||||
('OPT_1', "Scene Origin", "Use the scene origin as the position for the selected objects to align to"),
|
||||
('OPT_2', "3D Cursor", "Use the 3D cursor as the position for the selected objects to align to"),
|
||||
('OPT_3', "Selection", "Use the selected objects as the position for the selected objects to align to"),
|
||||
('OPT_4', "Active", "Use the active object as the position for the selected objects to align to"),
|
||||
),
|
||||
default='OPT_4',
|
||||
)
|
||||
align_axis: EnumProperty(
|
||||
name="Align",
|
||||
description="Align to axis",
|
||||
items=(
|
||||
('X', "X", ""),
|
||||
('Y', "Y", ""),
|
||||
('Z', "Z", ""),
|
||||
),
|
||||
options={'ENUM_FLAG'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'OBJECT'
|
||||
|
||||
def execute(self, context):
|
||||
align_axis = self.align_axis
|
||||
ret = align_objects(
|
||||
context,
|
||||
'X' in align_axis,
|
||||
'Y' in align_axis,
|
||||
'Z' in align_axis,
|
||||
self.align_mode,
|
||||
self.relative_to,
|
||||
self.bb_quality,
|
||||
)
|
||||
|
||||
if not ret:
|
||||
self.report({'WARNING'}, "No objects with bound-box selected")
|
||||
return {'CANCELLED'}
|
||||
else:
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
AlignObjects,
|
||||
)
|
||||
@@ -0,0 +1,677 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from mathutils import Vector
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
)
|
||||
from bpy.app.translations import (
|
||||
pgettext_rpt as rpt_,
|
||||
pgettext_data as data_,
|
||||
)
|
||||
|
||||
|
||||
def object_ensure_material(obj, mat_name):
|
||||
""" Use an existing material or add a new one.
|
||||
"""
|
||||
mat = mat_slot = None
|
||||
for mat_slot in obj.material_slots:
|
||||
mat = mat_slot.material
|
||||
if mat:
|
||||
break
|
||||
if mat is None:
|
||||
mat = bpy.data.materials.new(mat_name)
|
||||
mat.node_tree.nodes.clear()
|
||||
if mat_slot:
|
||||
mat_slot.material = mat
|
||||
else:
|
||||
obj.data.materials.append(mat)
|
||||
return mat
|
||||
|
||||
|
||||
class ObjectModeOperator:
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'OBJECT'
|
||||
|
||||
|
||||
class QuickFur(ObjectModeOperator, Operator):
|
||||
"""Add a fur setup to the selected objects"""
|
||||
bl_idname = "object.quick_fur"
|
||||
bl_label = "Quick Fur"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
density: EnumProperty(
|
||||
name="Density",
|
||||
items=(
|
||||
('LOW', "Low", ""),
|
||||
('MEDIUM', "Medium", ""),
|
||||
('HIGH', "High", ""),
|
||||
),
|
||||
default='MEDIUM',
|
||||
)
|
||||
length: FloatProperty(
|
||||
name="Length",
|
||||
min=0.001, max=100,
|
||||
soft_min=0.01, soft_max=10,
|
||||
default=0.1,
|
||||
subtype='DISTANCE',
|
||||
)
|
||||
radius: FloatProperty(
|
||||
name="Hair Radius",
|
||||
min=0.0, max=10,
|
||||
soft_min=0.0001, soft_max=0.1,
|
||||
default=0.001,
|
||||
subtype='DISTANCE',
|
||||
)
|
||||
view_percentage: FloatProperty(
|
||||
name="View Percentage",
|
||||
min=0.0, max=1.0,
|
||||
default=1.0,
|
||||
subtype='FACTOR',
|
||||
)
|
||||
apply_hair_guides: BoolProperty(
|
||||
name="Apply Hair Guides",
|
||||
default=True,
|
||||
)
|
||||
use_noise: BoolProperty(
|
||||
name="Noise",
|
||||
default=True,
|
||||
)
|
||||
use_frizz: BoolProperty(
|
||||
name="Frizz",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
|
||||
if context.active_object is None or context.active_object.type != 'MESH':
|
||||
cls.poll_message_set("No active mesh object.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
from collections import namedtuple
|
||||
|
||||
mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
|
||||
if not mesh_objects:
|
||||
self.report({'ERROR'}, "Select at least one mesh object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.density == 'LOW':
|
||||
count = 1000
|
||||
elif self.density == 'MEDIUM':
|
||||
count = 10000
|
||||
elif self.density == 'HIGH':
|
||||
count = 100000
|
||||
|
||||
asset_library_filepath = os.path.join(
|
||||
bpy.utils.system_resource('DATAFILES'),
|
||||
"assets",
|
||||
"nodes",
|
||||
"procedural_hair_node_assets.blend",
|
||||
)
|
||||
|
||||
# Create a named tuple that stores attributes for the node-group names.
|
||||
attr_name_pairs = [
|
||||
("generate", "Generate Hair Curves"),
|
||||
("interpolate", "Interpolate Hair Curves"),
|
||||
("radius", "Set Hair Curve Profile"),
|
||||
|
||||
]
|
||||
if self.use_noise:
|
||||
attr_name_pairs.append(("noise", "Hair Curves Noise"))
|
||||
if self.use_frizz:
|
||||
attr_name_pairs.append(("frizz", "Frizz Hair Curves"))
|
||||
|
||||
NodeGroupData = namedtuple("NodeGroupData", tuple(v for v, _ in attr_name_pairs))
|
||||
|
||||
with bpy.data.libraries.load(
|
||||
asset_library_filepath,
|
||||
link=True,
|
||||
pack=True,
|
||||
set_fake=False,
|
||||
) as (data_src, data_dst):
|
||||
# The values are assumed to exist, no inspection of the source is needed.
|
||||
del data_src
|
||||
data_dst.node_groups.extend([name for _, name in attr_name_pairs])
|
||||
|
||||
# For convenient name lookups.
|
||||
node_groups_name_map = {id.name: id for id in data_dst.node_groups}
|
||||
node_groups = NodeGroupData(*(node_groups_name_map[name] for _, name in attr_name_pairs))
|
||||
del node_groups_name_map
|
||||
|
||||
material = bpy.data.materials.new(data_("Fur Material"))
|
||||
|
||||
mesh_with_zero_area = False
|
||||
mesh_missing_uv_map = False
|
||||
modifier_apply_error = False
|
||||
|
||||
for mesh_object in mesh_objects:
|
||||
mesh = mesh_object.data
|
||||
if len(mesh.uv_layers) == 0:
|
||||
mesh_missing_uv_map = True
|
||||
continue
|
||||
|
||||
with context.temp_override(active_object=mesh_object):
|
||||
bpy.ops.object.curves_empty_hair_add()
|
||||
curves_object = context.active_object
|
||||
curves = curves_object.data
|
||||
curves.materials.append(material)
|
||||
|
||||
area = 0.0
|
||||
for poly in mesh.polygons:
|
||||
area += poly.area
|
||||
if area == 0.0:
|
||||
mesh_with_zero_area = True
|
||||
density = 10
|
||||
else:
|
||||
density = count / area
|
||||
|
||||
generate_modifier = curves_object.modifiers.new(name=data_("Generate"), type='NODES')
|
||||
generate_modifier.node_group = node_groups.generate
|
||||
generate_modifier.properties.inputs.Input_12.value = True
|
||||
generate_modifier.properties.inputs.Input_20.value = self.length
|
||||
generate_modifier.properties.inputs.Input_22.value = material
|
||||
generate_modifier.properties.inputs.Input_15.value = density * 0.01
|
||||
|
||||
radius_modifier = curves_object.modifiers.new(name=data_("Set Hair Curve Profile"), type='NODES')
|
||||
radius_modifier.node_group = node_groups.radius
|
||||
radius_modifier.properties.inputs.Input_3.value = self.radius
|
||||
|
||||
interpolate_modifier = curves_object.modifiers.new(name=data_("Interpolate Hair Curves"), type='NODES')
|
||||
interpolate_modifier.node_group = node_groups.interpolate
|
||||
interpolate_modifier.properties.inputs.Input_12.value = True
|
||||
interpolate_modifier.properties.inputs.Input_15.value = density
|
||||
interpolate_modifier.properties.inputs.Input_17.value = self.view_percentage
|
||||
interpolate_modifier.properties.inputs.Input_24.value = True
|
||||
|
||||
if self.use_noise:
|
||||
noise_modifier = curves_object.modifiers.new(name=data_("Hair Curves Noise"), type='NODES')
|
||||
noise_modifier.node_group = node_groups.noise
|
||||
|
||||
if self.use_frizz:
|
||||
frizz_modifier = curves_object.modifiers.new(name=data_("Frizz Hair Curves"), type='NODES')
|
||||
frizz_modifier.node_group = node_groups.frizz
|
||||
|
||||
if self.apply_hair_guides:
|
||||
with context.temp_override(object=curves_object):
|
||||
try:
|
||||
bpy.ops.object.modifier_apply(modifier=generate_modifier.name)
|
||||
except Exception:
|
||||
modifier_apply_error = True
|
||||
|
||||
curves_object.modifiers.move(0, len(curves_object.modifiers) - 1)
|
||||
|
||||
if mesh_with_zero_area:
|
||||
self.report({'WARNING'}, "Mesh has no face area")
|
||||
if mesh_missing_uv_map:
|
||||
self.report({'WARNING'}, "Mesh UV map required")
|
||||
if modifier_apply_error and not mesh_with_zero_area:
|
||||
self.report({'WARNING'}, "Unable to apply \"Generate\" modifier")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class QuickExplode(ObjectModeOperator, Operator):
|
||||
"""Make selected objects explode"""
|
||||
bl_idname = "object.quick_explode"
|
||||
bl_label = "Quick Explode"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
style: EnumProperty(
|
||||
name="Explode Style",
|
||||
items=(
|
||||
('EXPLODE', "Explode", ""),
|
||||
('BLEND', "Blend", ""),
|
||||
),
|
||||
default='EXPLODE',
|
||||
)
|
||||
amount: IntProperty(
|
||||
name="Number of Pieces",
|
||||
min=2, max=10000,
|
||||
soft_min=2, soft_max=10000,
|
||||
default=100,
|
||||
)
|
||||
frame_duration: IntProperty(
|
||||
name="Duration",
|
||||
min=1, max=300000,
|
||||
soft_min=1, soft_max=10000,
|
||||
default=50,
|
||||
)
|
||||
|
||||
frame_start: IntProperty(
|
||||
name="Start Frame",
|
||||
min=1, max=300000,
|
||||
soft_min=1, soft_max=10000,
|
||||
default=1,
|
||||
)
|
||||
frame_end: IntProperty(
|
||||
name="End Frame",
|
||||
min=1, max=300000,
|
||||
soft_min=1, soft_max=10000,
|
||||
default=10,
|
||||
)
|
||||
|
||||
velocity: FloatProperty(
|
||||
name="Outwards Velocity",
|
||||
min=0, max=300000,
|
||||
soft_min=0, soft_max=10,
|
||||
default=1,
|
||||
)
|
||||
|
||||
fade: BoolProperty(
|
||||
name="Fade",
|
||||
description="Fade the pieces over time",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
context_override = context.copy()
|
||||
obj_act = context.active_object
|
||||
|
||||
if obj_act is None or obj_act.type != 'MESH':
|
||||
self.report({'ERROR'}, "Active object is not a mesh")
|
||||
return {'CANCELLED'}
|
||||
|
||||
mesh_objects = [
|
||||
obj for obj in context.selected_objects
|
||||
if obj.type == 'MESH' and obj != obj_act
|
||||
]
|
||||
mesh_objects.insert(0, obj_act)
|
||||
|
||||
if self.style == 'BLEND' and len(mesh_objects) != 2:
|
||||
self.report({'ERROR'}, "Select two mesh objects")
|
||||
self.style = 'EXPLODE'
|
||||
return {'CANCELLED'}
|
||||
elif not mesh_objects:
|
||||
self.report({'ERROR'}, "Select at least one mesh object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
for obj in mesh_objects:
|
||||
if obj.particle_systems:
|
||||
self.report({'ERROR'}, rpt_("Object {!r} already has a particle system").format(obj.name))
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.style == 'BLEND':
|
||||
from_obj = mesh_objects[1]
|
||||
to_obj = mesh_objects[0]
|
||||
|
||||
for obj in mesh_objects:
|
||||
context_override["object"] = obj
|
||||
with context.temp_override(**context_override):
|
||||
bpy.ops.object.particle_system_add()
|
||||
|
||||
settings = obj.particle_systems[-1].settings
|
||||
settings.count = self.amount
|
||||
# first set frame end, to prevent frame start clamping
|
||||
settings.frame_end = self.frame_end - self.frame_duration
|
||||
settings.frame_start = self.frame_start
|
||||
settings.lifetime = self.frame_duration
|
||||
settings.normal_factor = self.velocity
|
||||
settings.render_type = 'NONE'
|
||||
|
||||
explode = obj.modifiers.new(name=data_("Explode"), type='EXPLODE')
|
||||
explode.use_edge_cut = True
|
||||
|
||||
if self.fade:
|
||||
explode.show_dead = False
|
||||
uv = obj.data.uv_layers.new(name=data_("Explode fade"))
|
||||
explode.particle_uv = uv.name
|
||||
|
||||
mat = object_ensure_material(obj, data_("Explode Fade"))
|
||||
mat.surface_render_method = 'DITHERED'
|
||||
|
||||
nodes = mat.node_tree.nodes
|
||||
node_out_mat = nodes.new("ShaderNodeOutputMaterial")
|
||||
node_surface = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
nodes.active = node_out_mat
|
||||
|
||||
node_x = node_surface.location[0]
|
||||
node_y = node_surface.location[1] - 400
|
||||
offset_x = 200
|
||||
|
||||
node_out_mat.location[0] = node_x + node_surface.width + offset_x
|
||||
|
||||
node_mix = nodes.new('ShaderNodeMixShader')
|
||||
node_mix.location = (node_x - offset_x, node_y)
|
||||
mat.node_tree.links.new(node_surface.outputs[0], node_mix.inputs[1])
|
||||
mat.node_tree.links.new(node_mix.outputs["Shader"], node_out_mat.inputs["Surface"])
|
||||
offset_x += 200
|
||||
|
||||
node_trans = nodes.new('ShaderNodeBsdfTransparent')
|
||||
node_trans.location = (node_x - offset_x, node_y)
|
||||
mat.node_tree.links.new(node_trans.outputs["BSDF"], node_mix.inputs[2])
|
||||
offset_x += 200
|
||||
|
||||
node_ramp = nodes.new('ShaderNodeValToRGB')
|
||||
node_ramp.location = (node_x - offset_x, node_y)
|
||||
offset_x += 200
|
||||
mat.node_tree.links.new(node_ramp.outputs["Alpha"], node_mix.inputs["Fac"])
|
||||
color_ramp = node_ramp.color_ramp
|
||||
color_ramp.elements[0].color[3] = 0.0
|
||||
color_ramp.elements[1].color[3] = 1.0
|
||||
|
||||
if self.style == 'BLEND':
|
||||
color_ramp.elements[0].position = 0.333
|
||||
color_ramp.elements[1].position = 0.666
|
||||
if obj == to_obj:
|
||||
# reverse ramp alpha
|
||||
color_ramp.elements[0].color[3] = 1.0
|
||||
color_ramp.elements[1].color[3] = 0.0
|
||||
|
||||
node_sep = nodes.new('ShaderNodeSeparateXYZ')
|
||||
node_sep.location = (node_x - offset_x, node_y)
|
||||
offset_x += 200
|
||||
mat.node_tree.links.new(node_sep.outputs["X"], node_ramp.inputs["Fac"])
|
||||
|
||||
node_uv = nodes.new('ShaderNodeUVMap')
|
||||
node_uv.location = (node_x - offset_x, node_y)
|
||||
node_uv.uv_map = uv.name
|
||||
mat.node_tree.links.new(node_uv.outputs["UV"], node_sep.inputs["Vector"])
|
||||
|
||||
if self.style == 'BLEND':
|
||||
settings.physics_type = 'KEYED'
|
||||
settings.use_emit_random = False
|
||||
settings.rotation_mode = 'NOR'
|
||||
|
||||
psys = obj.particle_systems[-1]
|
||||
|
||||
context_override["particle_system"] = obj.particle_systems[-1]
|
||||
with context.temp_override(**context_override):
|
||||
bpy.ops.particle.new_target()
|
||||
bpy.ops.particle.new_target()
|
||||
|
||||
if obj == from_obj:
|
||||
psys.targets[1].object = to_obj
|
||||
else:
|
||||
psys.targets[0].object = from_obj
|
||||
settings.normal_factor = -self.velocity
|
||||
explode.show_unborn = False
|
||||
explode.show_dead = True
|
||||
else:
|
||||
settings.factor_random = self.velocity
|
||||
settings.angular_velocity_factor = self.velocity / 10.0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
self.frame_start = context.scene.frame_current
|
||||
self.frame_end = self.frame_start + self.frame_duration
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
def obj_bb_minmax(obj, min_co, max_co):
|
||||
for i in range(0, 8):
|
||||
bb_vec = obj.matrix_world @ Vector(obj.bound_box[i])
|
||||
|
||||
min_co[0] = min(bb_vec[0], min_co[0])
|
||||
min_co[1] = min(bb_vec[1], min_co[1])
|
||||
min_co[2] = min(bb_vec[2], min_co[2])
|
||||
max_co[0] = max(bb_vec[0], max_co[0])
|
||||
max_co[1] = max(bb_vec[1], max_co[1])
|
||||
max_co[2] = max(bb_vec[2], max_co[2])
|
||||
|
||||
|
||||
def grid_location(x, y):
|
||||
return (x * 200, y * 150)
|
||||
|
||||
|
||||
class QuickSmoke(ObjectModeOperator, Operator):
|
||||
"""Use selected objects as smoke emitters"""
|
||||
bl_idname = "object.quick_smoke"
|
||||
bl_label = "Quick Smoke"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
style: EnumProperty(
|
||||
name="Smoke Style",
|
||||
items=(
|
||||
('SMOKE', "Smoke", ""),
|
||||
('FIRE', "Fire", ""),
|
||||
('BOTH', "Smoke & Fire", ""),
|
||||
),
|
||||
default='SMOKE',
|
||||
)
|
||||
|
||||
show_flows: BoolProperty(
|
||||
name="Render Smoke Objects",
|
||||
description="Keep the smoke objects visible during rendering",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
if not bpy.app.build_options.fluid:
|
||||
self.report({'ERROR'}, "Built without Fluid modifier")
|
||||
return {'CANCELLED'}
|
||||
|
||||
mesh_objects = [
|
||||
obj for obj in context.selected_objects
|
||||
if obj.type == 'MESH'
|
||||
]
|
||||
min_co = Vector((100000.0, 100000.0, 100000.0))
|
||||
max_co = -min_co
|
||||
|
||||
if not mesh_objects:
|
||||
self.report({'ERROR'}, "Select at least one mesh object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
for obj in mesh_objects:
|
||||
fluid = obj.modifiers.new(name=data_("Fluid"), type='FLUID')
|
||||
fluid.fluid_type = 'FLOW'
|
||||
|
||||
# set type
|
||||
fluid.flow_settings.flow_type = self.style
|
||||
|
||||
# set flow behavior
|
||||
fluid.flow_settings.flow_behavior = 'INFLOW'
|
||||
|
||||
# use some surface distance for smoke emission
|
||||
fluid.flow_settings.surface_distance = 1.0
|
||||
|
||||
if not self.show_flows:
|
||||
obj.display_type = 'WIRE'
|
||||
|
||||
# store bounding box min/max for the domain object
|
||||
obj_bb_minmax(obj, min_co, max_co)
|
||||
|
||||
# add the smoke domain object
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
obj = context.active_object
|
||||
obj.name = data_("Smoke Domain")
|
||||
|
||||
# give the smoke some room above the flows
|
||||
obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, 1.0))
|
||||
obj.scale = 0.5 * (max_co - min_co) + Vector((1.0, 1.0, 2.0))
|
||||
|
||||
# setup smoke domain
|
||||
fluid = obj.modifiers.new(name=data_("Fluid"), type='FLUID')
|
||||
fluid.fluid_type = 'DOMAIN'
|
||||
# The default value leads to unstable simulations (see #126924).
|
||||
fluid.domain_settings.cfl_condition = 4.0
|
||||
if self.style == {'FIRE', 'BOTH'}:
|
||||
fluid.domain_settings.use_noise = True
|
||||
|
||||
# ensure correct cache file format for smoke
|
||||
if bpy.app.build_options.openvdb:
|
||||
fluid.domain_settings.cache_data_format = 'OPENVDB'
|
||||
|
||||
# Setup material
|
||||
|
||||
# Cycles and EEVEE.
|
||||
bpy.ops.object.material_slot_add()
|
||||
|
||||
mat = bpy.data.materials.new(data_("Smoke Domain Material"))
|
||||
obj.material_slots[0].material = mat
|
||||
|
||||
# Set node variables and clear the default nodes
|
||||
tree = mat.node_tree
|
||||
nodes = tree.nodes
|
||||
links = tree.links
|
||||
|
||||
nodes.clear()
|
||||
|
||||
# Create shader nodes
|
||||
|
||||
# Material output
|
||||
node_out = nodes.new(type='ShaderNodeOutputMaterial')
|
||||
node_out.location = grid_location(6, 1)
|
||||
|
||||
# Add Principled Volume
|
||||
node_principled = nodes.new(type='ShaderNodeVolumePrincipled')
|
||||
node_principled.location = grid_location(4, 1)
|
||||
links.new(node_principled.outputs["Volume"], node_out.inputs["Volume"])
|
||||
|
||||
node_principled.inputs["Density"].default_value = 5.0
|
||||
|
||||
if self.style in {'FIRE', 'BOTH'}:
|
||||
node_principled.inputs["Blackbody Intensity"].default_value = 1.0
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class QuickLiquid(Operator):
|
||||
"""Make selected objects liquid"""
|
||||
bl_idname = "object.quick_liquid"
|
||||
bl_label = "Quick Liquid"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
show_flows: BoolProperty(
|
||||
name="Render Liquid Objects",
|
||||
description="Keep the liquid objects visible during rendering",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
if not bpy.app.build_options.fluid:
|
||||
self.report({'ERROR'}, "Built without Fluid modifier")
|
||||
return {'CANCELLED'}
|
||||
|
||||
mesh_objects = [
|
||||
obj for obj in context.selected_objects
|
||||
if obj.type == 'MESH'
|
||||
]
|
||||
min_co = Vector((100000.0, 100000.0, 100000.0))
|
||||
max_co = -min_co
|
||||
|
||||
if not mesh_objects:
|
||||
self.report({'ERROR'}, "Select at least one mesh object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# set shading type to wireframe so that liquid particles are visible
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for space in area.spaces:
|
||||
if space.type == 'VIEW_3D':
|
||||
space.shading.type = 'WIREFRAME'
|
||||
|
||||
for obj in mesh_objects:
|
||||
fluid = obj.modifiers.new(name=data_("Fluid"), type='FLUID')
|
||||
fluid.fluid_type = 'FLOW'
|
||||
|
||||
# set type
|
||||
fluid.flow_settings.flow_type = 'LIQUID'
|
||||
|
||||
# set flow behavior
|
||||
fluid.flow_settings.flow_behavior = 'GEOMETRY'
|
||||
|
||||
# use some surface distance for smoke emission
|
||||
fluid.flow_settings.surface_distance = 0.0
|
||||
|
||||
if not self.show_flows:
|
||||
obj.display_type = 'WIRE'
|
||||
|
||||
# store bounding box min/max for the domain object
|
||||
obj_bb_minmax(obj, min_co, max_co)
|
||||
|
||||
# add the liquid domain object
|
||||
bpy.ops.mesh.primitive_cube_add(align='WORLD')
|
||||
obj = context.active_object
|
||||
obj.name = data_("Liquid Domain")
|
||||
|
||||
# give the liquid some room above the flows
|
||||
obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, -1.0))
|
||||
obj.scale = 0.5 * (max_co - min_co) + Vector((1.0, 1.0, 2.0))
|
||||
|
||||
# setup liquid domain
|
||||
fluid = obj.modifiers.new(name=data_("Fluid"), type='FLUID')
|
||||
fluid.fluid_type = 'DOMAIN'
|
||||
# set all domain borders to obstacle
|
||||
fluid.domain_settings.use_collision_border_front = True
|
||||
fluid.domain_settings.use_collision_border_back = True
|
||||
fluid.domain_settings.use_collision_border_right = True
|
||||
fluid.domain_settings.use_collision_border_left = True
|
||||
fluid.domain_settings.use_collision_border_top = True
|
||||
fluid.domain_settings.use_collision_border_bottom = True
|
||||
|
||||
# ensure correct cache file formats for liquid
|
||||
if bpy.app.build_options.openvdb:
|
||||
fluid.domain_settings.cache_data_format = 'OPENVDB'
|
||||
fluid.domain_settings.cache_mesh_format = 'BOBJECT'
|
||||
|
||||
# change domain type, will also allocate and show particle system for FLIP
|
||||
fluid.domain_settings.domain_type = 'LIQUID'
|
||||
|
||||
# set color mapping field to show phi grid for liquid
|
||||
fluid.domain_settings.color_ramp_field = 'PHI'
|
||||
|
||||
# perform a single slice of the domain
|
||||
fluid.domain_settings.use_slice = True
|
||||
|
||||
# set display thickness to a lower value for more detailed display of phi grids
|
||||
fluid.domain_settings.display_thickness = 0.02
|
||||
|
||||
# make the domain smooth so it renders nicely
|
||||
bpy.ops.object.shade_smooth()
|
||||
|
||||
# create a ray-transparent material for the domain
|
||||
bpy.ops.object.material_slot_add()
|
||||
|
||||
mat = bpy.data.materials.new(data_("Liquid Domain Material"))
|
||||
obj.material_slots[0].material = mat
|
||||
|
||||
# Set node variables and clear the default nodes
|
||||
tree = mat.node_tree
|
||||
nodes = tree.nodes
|
||||
links = tree.links
|
||||
|
||||
nodes.clear()
|
||||
|
||||
# Create shader nodes
|
||||
|
||||
# Material output
|
||||
node_out = nodes.new(type='ShaderNodeOutputMaterial')
|
||||
node_out.location = grid_location(6, 1)
|
||||
|
||||
# Add Glass
|
||||
node_glass = nodes.new(type='ShaderNodeBsdfGlass')
|
||||
node_glass.location = grid_location(4, 1)
|
||||
links.new(node_glass.outputs["BSDF"], node_out.inputs["Surface"])
|
||||
node_glass.inputs["IOR"].default_value = 1.33
|
||||
|
||||
# Add Absorption
|
||||
node_absorption = nodes.new(type='ShaderNodeVolumeAbsorption')
|
||||
node_absorption.location = grid_location(4, 2)
|
||||
links.new(node_absorption.outputs["Volume"], node_out.inputs["Volume"])
|
||||
node_absorption.inputs["Color"].default_value = (0.8, 0.9, 1.0, 1.0)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
QuickExplode,
|
||||
QuickFur,
|
||||
QuickSmoke,
|
||||
QuickLiquid,
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def randomize_selected(context, seed, delta, loc, rot, scale, scale_even, _scale_min):
|
||||
|
||||
import random
|
||||
from random import uniform
|
||||
|
||||
random.seed(seed)
|
||||
|
||||
def rand_vec(vec_range):
|
||||
return Vector(uniform(-val, val) for val in vec_range)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
|
||||
if loc:
|
||||
if delta:
|
||||
obj.delta_location += rand_vec(loc)
|
||||
else:
|
||||
obj.location += rand_vec(loc)
|
||||
else:
|
||||
# Otherwise the values change under us.
|
||||
uniform(0.0, 0.0)
|
||||
uniform(0.0, 0.0)
|
||||
uniform(0.0, 0.0)
|
||||
|
||||
if rot:
|
||||
vec = rand_vec(rot)
|
||||
|
||||
rotation_mode = obj.rotation_mode
|
||||
if rotation_mode in {'QUATERNION', 'AXIS_ANGLE'}:
|
||||
obj.rotation_mode = 'XYZ'
|
||||
|
||||
if delta:
|
||||
obj.delta_rotation_euler[0] += vec[0]
|
||||
obj.delta_rotation_euler[1] += vec[1]
|
||||
obj.delta_rotation_euler[2] += vec[2]
|
||||
else:
|
||||
obj.rotation_euler[0] += vec[0]
|
||||
obj.rotation_euler[1] += vec[1]
|
||||
obj.rotation_euler[2] += vec[2]
|
||||
obj.rotation_mode = rotation_mode
|
||||
else:
|
||||
uniform(0.0, 0.0), uniform(0.0, 0.0), uniform(0.0, 0.0)
|
||||
|
||||
if scale:
|
||||
if delta:
|
||||
org_sca_x, org_sca_y, org_sca_z = obj.delta_scale
|
||||
else:
|
||||
org_sca_x, org_sca_y, org_sca_z = obj.scale
|
||||
|
||||
sca_x, sca_y, sca_z = (
|
||||
uniform(-scale[0] + 2.0, scale[0]),
|
||||
uniform(-scale[1] + 2.0, scale[1]),
|
||||
uniform(-scale[2] + 2.0, scale[2]),
|
||||
)
|
||||
|
||||
if scale_even:
|
||||
aX = sca_x * org_sca_x
|
||||
aY = sca_x * org_sca_y
|
||||
aZ = sca_x * org_sca_z
|
||||
else:
|
||||
aX = sca_x * org_sca_x
|
||||
aY = sca_y * org_sca_y
|
||||
aZ = sca_z * org_sca_z
|
||||
|
||||
if delta:
|
||||
obj.delta_scale = aX, aY, aZ
|
||||
else:
|
||||
obj.scale = aX, aY, aZ
|
||||
else:
|
||||
uniform(0.0, 0.0)
|
||||
uniform(0.0, 0.0)
|
||||
uniform(0.0, 0.0)
|
||||
|
||||
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
)
|
||||
|
||||
|
||||
class RandomizeLocRotSize(Operator):
|
||||
"""Randomize objects location, rotation, and scale"""
|
||||
bl_idname = "object.randomize_transform"
|
||||
bl_label = "Randomize Transform"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
random_seed: IntProperty(
|
||||
name="Random Seed",
|
||||
description="Seed value for the random generator",
|
||||
min=0,
|
||||
max=10000,
|
||||
default=0,
|
||||
)
|
||||
use_delta: BoolProperty(
|
||||
name="Transform Delta",
|
||||
description="Randomize delta transform values instead of regular transform",
|
||||
default=False,
|
||||
)
|
||||
use_loc: BoolProperty(
|
||||
name="Randomize Location",
|
||||
description="Randomize the location values",
|
||||
default=True,
|
||||
)
|
||||
loc: FloatVectorProperty(
|
||||
name="Location",
|
||||
description="Maximum distance the objects can spread over each axis",
|
||||
min=-100.0,
|
||||
max=100.0,
|
||||
default=(0.0, 0.0, 0.0),
|
||||
subtype='TRANSLATION',
|
||||
)
|
||||
use_rot: BoolProperty(
|
||||
name="Randomize Rotation",
|
||||
description="Randomize the rotation values",
|
||||
default=True,
|
||||
)
|
||||
rot: FloatVectorProperty(
|
||||
name="Rotation",
|
||||
description="Maximum rotation over each axis",
|
||||
min=-3.141592, # math.pi
|
||||
max=+3.141592,
|
||||
default=(0.0, 0.0, 0.0),
|
||||
subtype='EULER',
|
||||
)
|
||||
use_scale: BoolProperty(
|
||||
name="Randomize Scale",
|
||||
description="Randomize the scale values",
|
||||
default=True,
|
||||
)
|
||||
scale_even: BoolProperty(
|
||||
name="Scale Even",
|
||||
description="Use the same scale value for all axis",
|
||||
default=False,
|
||||
)
|
||||
|
||||
'''scale_min: FloatProperty(
|
||||
name="Minimum Scale Factor",
|
||||
description="Lowest scale percentage possible",
|
||||
min=-1.0, max=1.0, precision=3,
|
||||
default=0.15,
|
||||
)'''
|
||||
|
||||
scale: FloatVectorProperty(
|
||||
name="Scale",
|
||||
description="Maximum scale randomization over each axis",
|
||||
min=-100.0,
|
||||
max=100.0,
|
||||
default=(1.0, 1.0, 1.0),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'OBJECT'
|
||||
|
||||
def execute(self, context):
|
||||
seed = self.random_seed
|
||||
|
||||
delta = self.use_delta
|
||||
|
||||
loc = None if not self.use_loc else self.loc
|
||||
rot = None if not self.use_rot else Vector(self.rot)
|
||||
scale = None if not self.use_scale else self.scale
|
||||
|
||||
scale_even = self.scale_even
|
||||
# scale_min = self.scale_min
|
||||
scale_min = 0
|
||||
|
||||
randomize_selected(context, seed, delta, loc, rot, scale, scale_even, scale_min)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
RandomizeLocRotSize,
|
||||
)
|
||||
1101
blender-5.2.0/scripts/startup/bl_operators/presets.py
Normal file
1101
blender-5.2.0/scripts/startup/bl_operators/presets.py
Normal file
File diff suppressed because it is too large
Load Diff
21
blender-5.2.0/scripts/startup/bl_operators/render.py
Normal file
21
blender-5.2.0/scripts/startup/bl_operators/render.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
class RENDER_OT_swap_dimensions(Operator):
|
||||
bl_label = "Swap Dimensions"
|
||||
bl_idname = 'render.swap_dimensions'
|
||||
bl_description = "Flip X and Y resolutions"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
rd = context.scene.render
|
||||
rd.resolution_x, rd.resolution_y = (rd.resolution_y, rd.resolution_x)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (RENDER_OT_swap_dimensions,)
|
||||
323
blender-5.2.0/scripts/startup/bl_operators/rigidbody.py
Normal file
323
blender-5.2.0/scripts/startup/bl_operators/rigidbody.py
Normal file
@@ -0,0 +1,323 @@
|
||||
# SPDX-FileCopyrightText: 2013-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
)
|
||||
|
||||
|
||||
class CopyRigidbodySettings(Operator):
|
||||
"""Copy Rigid Body settings from active object to selected"""
|
||||
bl_idname = "rigidbody.object_settings_copy"
|
||||
bl_label = "Copy Rigid Body Settings"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
_attrs = (
|
||||
"type",
|
||||
"kinematic",
|
||||
"mass",
|
||||
"collision_shape",
|
||||
"use_margin",
|
||||
"collision_margin",
|
||||
"friction",
|
||||
"restitution",
|
||||
"use_deactivation",
|
||||
"use_start_deactivated",
|
||||
"deactivate_linear_velocity",
|
||||
"deactivate_angular_velocity",
|
||||
"linear_damping",
|
||||
"angular_damping",
|
||||
"collision_collections",
|
||||
"mesh_source",
|
||||
"use_deform",
|
||||
"enabled",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return (obj and obj.rigid_body)
|
||||
|
||||
def execute(self, context):
|
||||
obj_act = context.object
|
||||
|
||||
# Deselect all non mesh objects and objects that
|
||||
# already have a rigid body attached.
|
||||
rb_objects = []
|
||||
for o in context.selected_objects:
|
||||
if o.type != 'MESH' or o.rigid_body is not None:
|
||||
o.select_set(False)
|
||||
if o.rigid_body is not None:
|
||||
rb_objects.append(o)
|
||||
|
||||
bpy.ops.rigidbody.objects_add()
|
||||
|
||||
# Ensure that the rigid body objects
|
||||
# we've de-selected are selected again.
|
||||
for o in rb_objects:
|
||||
o.select_set(True)
|
||||
|
||||
objects = context.selected_objects
|
||||
if objects:
|
||||
rb_from = obj_act.rigid_body
|
||||
# copy settings
|
||||
for o in objects:
|
||||
rb_to = o.rigid_body
|
||||
if o == obj_act:
|
||||
continue
|
||||
for attr in self._attrs:
|
||||
setattr(rb_to, attr, getattr(rb_from, attr))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BakeToKeyframes(Operator):
|
||||
"""Bake rigid body transformations of selected objects to keyframes"""
|
||||
bl_idname = "rigidbody.bake_to_keyframes"
|
||||
bl_label = "Bake to Keyframes"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
frame_start: IntProperty(
|
||||
name="Start Frame",
|
||||
description="Start frame for baking",
|
||||
min=0, max=300000,
|
||||
default=1,
|
||||
)
|
||||
frame_end: IntProperty(
|
||||
name="End Frame",
|
||||
description="End frame for baking",
|
||||
min=1, max=300000,
|
||||
default=250,
|
||||
)
|
||||
step: IntProperty(
|
||||
name="Frame Step",
|
||||
description="Frame Step",
|
||||
min=1, max=120,
|
||||
default=1,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return (obj and obj.rigid_body)
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras import anim_utils
|
||||
|
||||
bake = []
|
||||
objects = []
|
||||
scene = context.scene
|
||||
frame_orig = scene.frame_current
|
||||
frames_step = range(self.frame_start, self.frame_end + 1, self.step)
|
||||
frames_full = range(self.frame_start, self.frame_end + 1)
|
||||
|
||||
# filter objects selection
|
||||
for obj in context.selected_objects:
|
||||
if not obj.rigid_body or obj.rigid_body.type != 'ACTIVE':
|
||||
obj.select_set(False)
|
||||
|
||||
objects = context.selected_objects
|
||||
|
||||
if objects:
|
||||
# store transformation data
|
||||
# need to start at scene start frame so simulation is run from the beginning
|
||||
for f in frames_full:
|
||||
scene.frame_set(f)
|
||||
if f in frames_step:
|
||||
mat = {}
|
||||
for i, obj in enumerate(objects):
|
||||
mat[i] = obj.matrix_world.copy()
|
||||
bake.append(mat)
|
||||
|
||||
# apply transformations as keyframes
|
||||
for i, f in enumerate(frames_step):
|
||||
scene.frame_set(f)
|
||||
for j, obj in enumerate(objects):
|
||||
mat = bake[i][j]
|
||||
# Convert world space transform to parent space, so parented objects don't get offset after baking.
|
||||
if obj.parent:
|
||||
mat = obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ mat
|
||||
|
||||
obj.location = mat.to_translation()
|
||||
|
||||
rot_mode = obj.rotation_mode
|
||||
if rot_mode == 'QUATERNION':
|
||||
q1 = obj.rotation_quaternion
|
||||
q2 = mat.to_quaternion()
|
||||
# make quaternion compatible with the previous one
|
||||
if q1.dot(q2) < 0.0:
|
||||
obj.rotation_quaternion = -q2
|
||||
else:
|
||||
obj.rotation_quaternion = q2
|
||||
elif rot_mode == 'AXIS_ANGLE':
|
||||
# this is a little roundabout but there's no better way right now
|
||||
aa = mat.to_quaternion().to_axis_angle()
|
||||
obj.rotation_axis_angle = (aa[1], *aa[0])
|
||||
else: # euler
|
||||
# make sure euler rotation is compatible to previous frame
|
||||
# NOTE: assume that on first frame, the starting rotation is appropriate
|
||||
obj.rotation_euler = mat.to_euler(rot_mode, obj.rotation_euler)
|
||||
|
||||
bpy.ops.anim.keyframe_insert_by_name(type='BUILTIN_KSI_LocRot')
|
||||
|
||||
# remove baked objects from simulation
|
||||
bpy.ops.rigidbody.objects_remove()
|
||||
|
||||
# clean up keyframes
|
||||
for obj in objects:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
obj.animation_data.action,
|
||||
obj.animation_data.action_slot,
|
||||
)
|
||||
if not channelbag:
|
||||
continue
|
||||
for fcu in channelbag.fcurves:
|
||||
keyframe_points = fcu.keyframe_points
|
||||
i = 1
|
||||
# remove unneeded keyframes
|
||||
while i < len(keyframe_points) - 1:
|
||||
val_prev = keyframe_points[i - 1].co[1]
|
||||
val_next = keyframe_points[i + 1].co[1]
|
||||
val = keyframe_points[i].co[1]
|
||||
|
||||
if abs(val - val_prev) + abs(val - val_next) < 0.0001:
|
||||
keyframe_points.remove(keyframe_points[i])
|
||||
else:
|
||||
i += 1
|
||||
# use linear interpolation for better visual results
|
||||
for keyframe in keyframe_points:
|
||||
keyframe.interpolation = 'LINEAR'
|
||||
|
||||
# return to the frame we started on
|
||||
scene.frame_set(frame_orig)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
scene = context.scene
|
||||
self.frame_start = scene.frame_start
|
||||
self.frame_end = scene.frame_end
|
||||
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class ConnectRigidBodies(Operator):
|
||||
"""Create rigid body constraints between selected rigid bodies"""
|
||||
bl_idname = "rigidbody.connect"
|
||||
bl_label = "Connect Rigid Bodies"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
con_type: EnumProperty(
|
||||
name="Type",
|
||||
description="Type of generated constraint",
|
||||
# XXX Would be nice to get icons too, but currently not possible ;)
|
||||
items=tuple(
|
||||
(e.identifier, e.name, e.description, e. value)
|
||||
for e in bpy.types.RigidBodyConstraint.bl_rna.properties["type"].enum_items
|
||||
),
|
||||
default='FIXED',
|
||||
)
|
||||
pivot_type: EnumProperty(
|
||||
name="Location",
|
||||
description="Constraint pivot location",
|
||||
items=(
|
||||
('CENTER', "Center", "Pivot location is between the constrained rigid bodies"),
|
||||
('ACTIVE', "Active", "Pivot location is at the active object position"),
|
||||
('SELECTED', "Selected", "Pivot location is at the selected object position"),
|
||||
),
|
||||
default='CENTER',
|
||||
)
|
||||
connection_pattern: EnumProperty(
|
||||
name="Connection Pattern",
|
||||
description="Pattern used to connect objects",
|
||||
items=(
|
||||
('SELECTED_TO_ACTIVE', "Selected to Active", "Connect selected objects to the active object"),
|
||||
('CHAIN_DISTANCE', "Chain by Distance", "Connect objects as a chain based on distance, "
|
||||
"starting at the active object"),
|
||||
),
|
||||
default='SELECTED_TO_ACTIVE',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return (obj and obj.rigid_body)
|
||||
|
||||
def _add_constraint(self, context, object1, object2):
|
||||
if object1 == object2:
|
||||
return
|
||||
|
||||
if self.pivot_type == 'ACTIVE':
|
||||
loc = object1.location
|
||||
elif self.pivot_type == 'SELECTED':
|
||||
loc = object2.location
|
||||
else:
|
||||
loc = (object1.location + object2.location) / 2.0
|
||||
|
||||
ob = bpy.data.objects.new("Constraint", object_data=None)
|
||||
ob.location = loc
|
||||
context.scene.collection.objects.link(ob)
|
||||
context.view_layer.objects.active = ob
|
||||
ob.select_set(True)
|
||||
|
||||
bpy.ops.rigidbody.constraint_add()
|
||||
con_obj = context.active_object
|
||||
con_obj.empty_display_type = 'ARROWS'
|
||||
con = con_obj.rigid_body_constraint
|
||||
con.type = self.con_type
|
||||
|
||||
con.object1 = object1
|
||||
con.object2 = object2
|
||||
|
||||
def execute(self, context):
|
||||
view_layer = context.view_layer
|
||||
objects = context.selected_objects
|
||||
obj_act = context.active_object
|
||||
change = False
|
||||
|
||||
if self.connection_pattern == 'CHAIN_DISTANCE':
|
||||
objs_sorted = [obj_act]
|
||||
objects_tmp = context.selected_objects
|
||||
try:
|
||||
objects_tmp.remove(obj_act)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
last_obj = obj_act
|
||||
|
||||
while objects_tmp:
|
||||
objects_tmp.sort(key=lambda o: (last_obj.location - o.location).length)
|
||||
last_obj = objects_tmp.pop(0)
|
||||
objs_sorted.append(last_obj)
|
||||
|
||||
for i in range(1, len(objs_sorted)):
|
||||
self._add_constraint(context, objs_sorted[i - 1], objs_sorted[i])
|
||||
change = True
|
||||
|
||||
else: # SELECTED_TO_ACTIVE
|
||||
for obj in objects:
|
||||
self._add_constraint(context, obj_act, obj)
|
||||
change = True
|
||||
|
||||
if change:
|
||||
# restore selection
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in objects:
|
||||
obj.select_set(True)
|
||||
view_layer.objects.active = obj_act
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'WARNING'}, "No other objects selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
classes = (
|
||||
BakeToKeyframes,
|
||||
ConnectRigidBodies,
|
||||
CopyRigidbodySettings,
|
||||
)
|
||||
@@ -0,0 +1,291 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Originally written by Matt Ebb
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
|
||||
|
||||
def guess_player_path(preset):
|
||||
import sys
|
||||
|
||||
found_version = (0, 0, 0)
|
||||
|
||||
if preset == 'INTERNAL':
|
||||
return bpy.app.binary_path, found_version
|
||||
|
||||
elif preset == 'DJV':
|
||||
player_path = "djv"
|
||||
if sys.platform == "linux":
|
||||
found_version = (3, 4, 0) # Assume it is at least 3.4.0
|
||||
elif sys.platform == "darwin":
|
||||
import os
|
||||
djv3_path = "/Applications/DJV.app/Contents/MacOS/DJV"
|
||||
djv2_path = "/Applications/DJV2.app/Contents/Resources/bin/djv"
|
||||
if os.path.exists(djv3_path):
|
||||
player_path = djv3_path
|
||||
found_version = (3, 4, 0) # Assume it is at least 3.4.0
|
||||
elif os.path.exists(djv2_path):
|
||||
player_path = djv2_path
|
||||
found_version = (2, 0, 0)
|
||||
elif sys.platform == "win32":
|
||||
import winreg
|
||||
|
||||
reg_value = None
|
||||
try:
|
||||
def extract_version(key_name):
|
||||
"""Given a key_name like "key name 3.3.4" extract version as (3, 3, 4)"""
|
||||
version = None
|
||||
parts = key_name.rsplit(" ", 1)
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
version = tuple(int(x) for x in parts[1].split("."))
|
||||
except ValueError:
|
||||
pass
|
||||
return version
|
||||
|
||||
# Enumerate versioned subkeys (e.g. "DJV 3.3.4", "DJV 3.4.0", etc.) and
|
||||
# pick the key with the greatest version.
|
||||
reg_base = r"SOFTWARE\WOW6432Node\Grizzly Peak 3D"
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_base, 0, winreg.KEY_READ) as base_key:
|
||||
best_version = None
|
||||
best_subkey = None
|
||||
index = 0
|
||||
while True:
|
||||
try:
|
||||
subkey_name = winreg.EnumKey(base_key, index)
|
||||
except OSError:
|
||||
break
|
||||
index += 1
|
||||
|
||||
if (version := extract_version(subkey_name)) is not None:
|
||||
if best_version is None or version > best_version:
|
||||
best_subkey = subkey_name
|
||||
best_version = version
|
||||
found_version = version
|
||||
|
||||
if best_subkey is not None:
|
||||
reg_path = reg_base + "\\" + best_subkey
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, winreg.KEY_READ) as regkey:
|
||||
reg_value = winreg.QueryValue(regkey, None)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Fallback to djv2 if we didn't find anything
|
||||
if not reg_value:
|
||||
try:
|
||||
reg_path = r"SOFTWARE\Classes\djv\shell\open\command"
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, winreg.KEY_READ) as regkey:
|
||||
reg_value = winreg.QueryValue(regkey, None)
|
||||
found_version = (2, 0, 0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if reg_value:
|
||||
if found_version > (2, 0, 0):
|
||||
player_path = reg_value.strip() + "\\bin\\djv.exe"
|
||||
else:
|
||||
binary = "djv.exe"
|
||||
index = reg_value.find(binary)
|
||||
if index > 0:
|
||||
player_path = reg_value[:index + len(binary)]
|
||||
|
||||
elif preset == 'FRAMECYCLER':
|
||||
player_path = "framecycler"
|
||||
|
||||
elif preset == 'RV':
|
||||
player_path = "rv"
|
||||
|
||||
elif preset == 'MPLAYER':
|
||||
player_path = "mplayer"
|
||||
|
||||
else:
|
||||
player_path = ""
|
||||
|
||||
return player_path, found_version
|
||||
|
||||
|
||||
class PlayRenderedAnim(Operator):
|
||||
"""Play back rendered frames/movies using an external player"""
|
||||
bl_idname = "render.play_rendered_anim"
|
||||
bl_label = "Play Rendered Animation"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@staticmethod
|
||||
def _frame_path_with_number_char(rd, ch, **kwargs):
|
||||
# Replace the number with `ch`.
|
||||
|
||||
# NOTE: make an api call for this would be nice, however this isn't needed in many places.
|
||||
file_a = rd.frame_path(frame=0, **kwargs)
|
||||
file_b = rd.frame_path(frame=-1, **kwargs)
|
||||
assert len(file_b) == len(file_a) + 1
|
||||
|
||||
for number_beg in range(len(file_a)):
|
||||
if file_a[number_beg] != file_b[number_beg]:
|
||||
break
|
||||
|
||||
for number_end in range(-1, -(len(file_a) + 1), -1):
|
||||
if file_a[number_end] != file_b[number_end]:
|
||||
break
|
||||
|
||||
number_end += len(file_a) + 1
|
||||
return file_a[:number_beg] + (ch * (number_end - number_beg)) + file_a[number_end:]
|
||||
|
||||
def execute(self, context):
|
||||
import os
|
||||
import subprocess
|
||||
from shlex import quote
|
||||
|
||||
scene = context.scene
|
||||
rd = scene.render
|
||||
prefs = context.preferences
|
||||
fps_final = rd.fps / rd.fps_base
|
||||
|
||||
preset = prefs.filepaths.animation_player_preset
|
||||
# file_path = bpy.path.abspath(rd.filepath) # UNUSED
|
||||
is_movie = rd.is_movie_format
|
||||
|
||||
views_format = rd.image_settings.views_format
|
||||
if rd.use_multiview and views_format == 'INDIVIDUAL':
|
||||
view_suffix = rd.views.active.file_suffix
|
||||
else:
|
||||
view_suffix = ""
|
||||
|
||||
found_version = (0, 0, 0)
|
||||
|
||||
# try and guess a command line if it doesn't exist
|
||||
if preset == 'CUSTOM':
|
||||
player_path = prefs.filepaths.animation_player
|
||||
else:
|
||||
player_path, found_version = guess_player_path(preset)
|
||||
|
||||
if is_movie is False and preset in {'FRAMECYCLER', 'RV', 'MPLAYER'}:
|
||||
file = PlayRenderedAnim._frame_path_with_number_char(rd, "#", view=view_suffix)
|
||||
file = bpy.path.abspath(file) # expand '//'
|
||||
else:
|
||||
path_valid = True
|
||||
# works for movies and images
|
||||
file = rd.frame_path(frame=scene.frame_start, preview=scene.use_preview_range, view=view_suffix)
|
||||
file = bpy.path.abspath(file) # expand '//'
|
||||
if not os.path.exists(file):
|
||||
err_msg = rpt_("File {!r} not found").format(file)
|
||||
self.report({'WARNING'}, err_msg)
|
||||
path_valid = False
|
||||
|
||||
# one last try for full range if we used preview range
|
||||
if scene.use_preview_range and not path_valid:
|
||||
file = rd.frame_path(frame=scene.frame_start, preview=False, view=view_suffix)
|
||||
file = bpy.path.abspath(file) # expand '//'
|
||||
err_msg = rpt_("File {!r} not found").format(file)
|
||||
if not os.path.exists(file):
|
||||
self.report({'WARNING'}, err_msg)
|
||||
|
||||
remove_OCIO_env = False
|
||||
cmd = [player_path]
|
||||
# extra options, fps controls etc.
|
||||
if scene.use_preview_range:
|
||||
frame_start = scene.frame_preview_start
|
||||
frame_end = scene.frame_preview_end
|
||||
else:
|
||||
frame_start = scene.frame_start
|
||||
frame_end = scene.frame_end
|
||||
if preset == 'INTERNAL':
|
||||
# Use the current GPU backend for the player.
|
||||
import gpu
|
||||
gpu_backend = gpu.platform.backend_type_get()
|
||||
if gpu_backend not in {'NONE', 'UNKNOWN'}:
|
||||
cmd.extend([
|
||||
"--gpu-backend", gpu_backend.lower(),
|
||||
])
|
||||
del gpu, gpu_backend
|
||||
|
||||
opts = [
|
||||
"-a",
|
||||
"-f", str(rd.fps), str(rd.fps_base),
|
||||
"-s", str(frame_start),
|
||||
"-e", str(frame_end),
|
||||
"-j", str(scene.frame_step),
|
||||
"-c", str(prefs.system.memory_cache_limit),
|
||||
file,
|
||||
]
|
||||
cmd.extend(opts)
|
||||
elif preset == 'DJV':
|
||||
if found_version >= (3, 4, 0):
|
||||
opts = [
|
||||
file,
|
||||
"-speed", str(fps_final),
|
||||
"-in", str(frame_start),
|
||||
"-out", str(frame_end),
|
||||
"-seek", str(scene.frame_current),
|
||||
"-timeUnits", "Frames",
|
||||
]
|
||||
elif found_version >= (3, 0, 0):
|
||||
opts = [
|
||||
file,
|
||||
"-speed", str(fps_final),
|
||||
]
|
||||
else:
|
||||
remove_OCIO_env = True
|
||||
opts = [
|
||||
file,
|
||||
"-speed", str(fps_final),
|
||||
"-in_out", str(frame_start), str(frame_end),
|
||||
"-frame", str(scene.frame_current),
|
||||
"-time_units", "Frames",
|
||||
]
|
||||
cmd.extend(opts)
|
||||
elif preset == 'FRAMECYCLER':
|
||||
opts = [file, "{:d}-{:d}".format(scene.frame_start, scene.frame_end)]
|
||||
cmd.extend(opts)
|
||||
elif preset == 'RV':
|
||||
opts = ["-fps", str(rd.fps), "-play"]
|
||||
if scene.use_preview_range:
|
||||
opts += [
|
||||
file.replace("#", "", file.count('#') - 1),
|
||||
"{:d}-{:d}".format(frame_start, frame_end),
|
||||
]
|
||||
else:
|
||||
opts.append(file)
|
||||
|
||||
cmd.extend(opts)
|
||||
elif preset == 'MPLAYER':
|
||||
opts = []
|
||||
if is_movie:
|
||||
opts.append(file)
|
||||
else:
|
||||
opts += [
|
||||
("mf://" + file.replace("#", "?")),
|
||||
"-mf",
|
||||
"fps={:.4f}".format(fps_final),
|
||||
]
|
||||
|
||||
opts += ["-loop", "0", "-really-quiet", "-fs"]
|
||||
cmd.extend(opts)
|
||||
else: # 'CUSTOM'
|
||||
cmd.append(file)
|
||||
|
||||
# launch it
|
||||
print("Executing command:\n ", " ".join(quote(c) for c in cmd))
|
||||
|
||||
try:
|
||||
env_copy = os.environ.copy()
|
||||
if remove_OCIO_env:
|
||||
env_copy.pop("OCIO", None)
|
||||
subprocess.Popen(cmd, env=env_copy)
|
||||
except Exception as ex:
|
||||
err_msg = rpt_("Couldn't run external animation player with command {!r}\n{:s}").format(cmd, str(ex))
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
err_msg,
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
PlayRenderedAnim,
|
||||
)
|
||||
433
blender-5.2.0/scripts/startup/bl_operators/sequencer.py
Normal file
433
blender-5.2.0/scripts/startup/bl_operators/sequencer.py
Normal file
@@ -0,0 +1,433 @@
|
||||
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
FileHandler,
|
||||
Operator,
|
||||
)
|
||||
|
||||
from bpy.props import (
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
)
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
|
||||
|
||||
def _animated_properties_get(strip):
|
||||
animated_properties = []
|
||||
if hasattr(strip, "volume"):
|
||||
animated_properties.append("volume")
|
||||
if hasattr(strip, "blend_alpha"):
|
||||
animated_properties.append("blend_alpha")
|
||||
return animated_properties
|
||||
|
||||
|
||||
class SequencerCrossfadeSounds(Operator):
|
||||
"""Do cross-fading volume animation of two selected sound strips"""
|
||||
|
||||
bl_idname = "sequencer.crossfade_sounds"
|
||||
bl_label = "Crossfade Sounds"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sequencer_scene = context.sequencer_scene
|
||||
if not sequencer_scene:
|
||||
return False
|
||||
strip = context.active_strip
|
||||
return strip and (strip.type == 'SOUND')
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.sequencer_scene
|
||||
strip1 = None
|
||||
strip2 = None
|
||||
for strip in scene.sequence_editor.strips:
|
||||
if strip.select and strip.type == 'SOUND':
|
||||
if strip1 is None:
|
||||
strip1 = strip
|
||||
elif strip2 is None:
|
||||
strip2 = strip
|
||||
else:
|
||||
strip2 = None
|
||||
break
|
||||
if strip2 is None:
|
||||
self.report({'ERROR'}, "Select 2 sound strips")
|
||||
return {'CANCELLED'}
|
||||
if strip1.left_handle > strip2.left_handle:
|
||||
strip1, strip2 = strip2, strip1
|
||||
if strip1.right_handle > strip2.left_handle:
|
||||
strip1.keyframe_insert("volume", frame=strip2.left_handle)
|
||||
strip1.volume = 0
|
||||
strip1.keyframe_insert("volume", frame=strip1.right_handle)
|
||||
strip2.keyframe_insert("volume", frame=strip1.right_handle)
|
||||
strip2.volume = 0
|
||||
strip2.keyframe_insert("volume", frame=strip2.left_handle)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'ERROR'}, "The selected strips don't overlap")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class SequencerSplitMulticam(Operator):
|
||||
"""Split multicam strip and select camera"""
|
||||
|
||||
bl_idname = "sequencer.split_multicam"
|
||||
bl_label = "Split Multicam"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
camera: IntProperty(
|
||||
name="Camera",
|
||||
min=1, max=32,
|
||||
soft_min=1, soft_max=32,
|
||||
default=1,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sequencer_scene = context.sequencer_scene
|
||||
if not sequencer_scene:
|
||||
return False
|
||||
strip = context.active_strip
|
||||
return strip and (strip.type == 'MULTICAM')
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.sequencer_scene
|
||||
camera = self.camera
|
||||
|
||||
strip = context.active_strip
|
||||
|
||||
if strip.multicam_source == camera or camera >= strip.channel:
|
||||
return {'FINISHED'}
|
||||
|
||||
cfra = scene.frame_current
|
||||
right_strip = strip.split(frame=cfra, split_method='SOFT')
|
||||
|
||||
if right_strip:
|
||||
strip.select = False
|
||||
right_strip.select = True
|
||||
scene.sequence_editor.active_strip = right_strip
|
||||
|
||||
context.active_strip.multicam_source = camera
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SequencerDeinterlaceSelectedMovies(Operator):
|
||||
"""Deinterlace all selected movie sources"""
|
||||
|
||||
bl_idname = "sequencer.deinterlace_selected_movies"
|
||||
bl_label = "Deinterlace Movies"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
scene = context.sequencer_scene
|
||||
return (scene and scene.sequence_editor)
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.sequencer_scene
|
||||
for strip in scene.sequence_editor.strips:
|
||||
if strip.select and strip.type == 'MOVIE':
|
||||
strip.use_deinterlace = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SequencerFadesClear(Operator):
|
||||
"""Removes fade animation from selected strips"""
|
||||
bl_idname = "sequencer.fades_clear"
|
||||
bl_label = "Clear Fades"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sequencer_scene = context.sequencer_scene
|
||||
if not sequencer_scene:
|
||||
return False
|
||||
strip = context.active_strip
|
||||
return strip is not None
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras import anim_utils
|
||||
|
||||
scene = context.sequencer_scene
|
||||
animation_data = scene.animation_data
|
||||
if animation_data is None:
|
||||
return {'CANCELLED'}
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(animation_data.action, animation_data.action_slot)
|
||||
if channelbag is None:
|
||||
return {'CANCELLED'}
|
||||
fcurves = channelbag.fcurves
|
||||
fcurve_map = {
|
||||
curve.data_path: curve
|
||||
for curve in fcurves
|
||||
if curve.data_path.startswith("sequence_editor.strips")
|
||||
}
|
||||
for strip in context.selected_strips:
|
||||
for animated_property in _animated_properties_get(strip):
|
||||
data_path = strip.path_from_id() + "." + animated_property
|
||||
curve = fcurve_map.get(data_path)
|
||||
if curve:
|
||||
fcurves.remove(curve)
|
||||
setattr(strip, animated_property, 1.0)
|
||||
strip.invalidate_cache('COMPOSITE')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SequencerFadesAdd(Operator):
|
||||
"""Adds or updates a fade animation for either visual or audio strips"""
|
||||
bl_idname = "sequencer.fades_add"
|
||||
bl_label = "Add Fades"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
duration_seconds: FloatProperty(
|
||||
name="Fade Duration",
|
||||
description="Duration of the fade in seconds",
|
||||
default=1.0,
|
||||
min=0.01,
|
||||
)
|
||||
type: EnumProperty(
|
||||
items=(
|
||||
('IN_OUT', "Fade In and Out", "Fade selected strips in and out"),
|
||||
('IN', "Fade In", "Fade in selected strips"),
|
||||
('OUT', "Fade Out", "Fade out selected strips"),
|
||||
('CURSOR_FROM', "From Current Frame",
|
||||
"Fade from the time cursor to the end of overlapping strips"),
|
||||
('CURSOR_TO', "To Current Frame",
|
||||
"Fade from the start of strips under the time cursor to the current frame"),
|
||||
),
|
||||
name="Fade Type",
|
||||
description="Fade in, out, both in and out, to, or from the current frame. Default is both in and out",
|
||||
default='IN_OUT',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sequencer_scene = context.sequencer_scene
|
||||
if not sequencer_scene:
|
||||
return False
|
||||
# Can't use context.selected_strips as it can have an impact on performances
|
||||
strip = context.active_strip
|
||||
return strip is not None
|
||||
|
||||
def execute(self, context):
|
||||
from math import floor
|
||||
|
||||
# We must create a scene action first if there's none
|
||||
scene = context.sequencer_scene
|
||||
if not scene.animation_data:
|
||||
scene.animation_data_create()
|
||||
if not scene.animation_data.action:
|
||||
action = bpy.data.actions.new(scene.name + "Action")
|
||||
scene.animation_data.action = action
|
||||
|
||||
strips = context.selected_strips
|
||||
|
||||
if not strips:
|
||||
self.report({'ERROR'}, "No strips selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.type in {'CURSOR_TO', 'CURSOR_FROM'}:
|
||||
strips = [
|
||||
strip for strip in strips
|
||||
if strip.left_handle < scene.frame_current < strip.right_handle
|
||||
]
|
||||
if not strips:
|
||||
self.report({'ERROR'}, "Current frame not within strip framerange")
|
||||
return {'CANCELLED'}
|
||||
|
||||
max_duration = min(strips, key=lambda strip: strip.duration).duration
|
||||
max_duration = floor(max_duration / 2.0) if self.type == 'IN_OUT' else max_duration
|
||||
|
||||
faded_strips = []
|
||||
for strip in strips:
|
||||
duration = self.calculate_fade_duration(context, strip)
|
||||
duration = min(duration, max_duration)
|
||||
if not self.is_long_enough(strip, duration):
|
||||
continue
|
||||
|
||||
for animated_property in _animated_properties_get(strip):
|
||||
fade_fcurve = self.fade_find_or_create_fcurve(context, strip, animated_property)
|
||||
fades = self.calculate_fades(strip, fade_fcurve, animated_property, duration)
|
||||
self.fade_animation_clear(fade_fcurve, fades)
|
||||
self.fade_animation_create(fade_fcurve, fades)
|
||||
faded_strips.append(strip)
|
||||
strip.invalidate_cache('COMPOSITE')
|
||||
|
||||
strip_string = "strip" if len(faded_strips) == 1 else "strips"
|
||||
self.report({'INFO'}, rpt_("Added fade animation to {:d} {:s}").format(len(faded_strips), strip_string))
|
||||
return {'FINISHED'}
|
||||
|
||||
def calculate_fade_duration(self, context, strip):
|
||||
scene = context.sequencer_scene
|
||||
frame_current = scene.frame_current
|
||||
duration = 0.0
|
||||
if self.type == 'CURSOR_TO':
|
||||
duration = abs(frame_current - strip.left_handle)
|
||||
elif self.type == 'CURSOR_FROM':
|
||||
duration = abs(strip.right_handle - frame_current)
|
||||
else:
|
||||
duration = calculate_duration_frames(scene, self.duration_seconds)
|
||||
return max(1, duration)
|
||||
|
||||
def is_long_enough(self, strip, duration=0.0):
|
||||
minimum_duration = duration * 2 if self.type == 'IN_OUT' else duration
|
||||
return strip.duration >= minimum_duration
|
||||
|
||||
def calculate_fades(self, strip, fade_fcurve, animated_property, duration):
|
||||
"""
|
||||
Returns a list of Fade objects
|
||||
"""
|
||||
fades = []
|
||||
if self.type in {'IN', 'IN_OUT', 'CURSOR_TO'}:
|
||||
fade = Fade(strip, fade_fcurve, 'IN', animated_property, duration)
|
||||
fades.append(fade)
|
||||
if self.type in {'OUT', 'IN_OUT', 'CURSOR_FROM'}:
|
||||
fade = Fade(strip, fade_fcurve, 'OUT', animated_property, duration)
|
||||
fades.append(fade)
|
||||
return fades
|
||||
|
||||
def fade_find_or_create_fcurve(self, context, strip, animated_property):
|
||||
"""
|
||||
Iterates over all the fcurves until it finds an fcurve with a data path
|
||||
that corresponds to the strip.
|
||||
Returns the matching FCurve or creates a new one if the function can't find a match.
|
||||
"""
|
||||
scene = context.sequencer_scene
|
||||
action = scene.animation_data.action
|
||||
searched_data_path = strip.path_from_id(animated_property)
|
||||
return action.fcurve_ensure_for_datablock(scene, searched_data_path)
|
||||
|
||||
def fade_animation_clear(self, fade_fcurve, fades):
|
||||
"""
|
||||
Removes existing keyframes in the fades' time range, in fast mode, without
|
||||
updating the fcurve
|
||||
"""
|
||||
keyframe_points = fade_fcurve.keyframe_points
|
||||
for fade in fades:
|
||||
for keyframe in keyframe_points:
|
||||
# The keyframe points list doesn't seem to always update as the
|
||||
# operator re-runs Leading to trying to remove nonexistent keyframes
|
||||
try:
|
||||
if fade.start.x < keyframe.co[0] <= fade.end.x:
|
||||
keyframe_points.remove(keyframe, fast=True)
|
||||
except Exception:
|
||||
pass
|
||||
fade_fcurve.update()
|
||||
|
||||
def fade_animation_create(self, fade_fcurve, fades):
|
||||
"""
|
||||
Inserts keyframes in the fade_fcurve in fast mode using the Fade objects.
|
||||
Updates the fcurve after having inserted all keyframes to finish the animation.
|
||||
"""
|
||||
keyframe_points = fade_fcurve.keyframe_points
|
||||
for fade in fades:
|
||||
for point in (fade.start, fade.end):
|
||||
keyframe_points.insert(frame=point.x, value=point.y, options={'FAST'})
|
||||
fade_fcurve.update()
|
||||
# The graph editor and the audio wave-forms only redraw upon "moving" a keyframe.
|
||||
keyframe_points[-1].co = keyframe_points[-1].co
|
||||
|
||||
|
||||
class Fade:
|
||||
# Data structure to represent fades.
|
||||
__slots__ = (
|
||||
"type",
|
||||
"animated_property",
|
||||
"duration",
|
||||
"max_value",
|
||||
"start",
|
||||
"end",
|
||||
)
|
||||
|
||||
def __init__(self, strip, fade_fcurve, ty, animated_property, duration):
|
||||
from mathutils import Vector
|
||||
self.type = ty
|
||||
self.animated_property = animated_property
|
||||
self.duration = duration
|
||||
self.max_value = self.calculate_max_value(strip, fade_fcurve)
|
||||
|
||||
if ty == 'IN':
|
||||
self.start = Vector((strip.left_handle, 0.0))
|
||||
self.end = Vector((strip.left_handle + self.duration, self.max_value))
|
||||
elif ty == 'OUT':
|
||||
self.start = Vector((strip.right_handle - self.duration, self.max_value))
|
||||
self.end = Vector((strip.right_handle, 0.0))
|
||||
|
||||
def calculate_max_value(self, strip, fade_fcurve):
|
||||
"""
|
||||
Returns the maximum Y coordinate the fade animation should use for a given strip
|
||||
Uses either the strip's value for the animated property, or the next keyframe after the fade
|
||||
"""
|
||||
max_value = 0.0
|
||||
|
||||
if not fade_fcurve.keyframe_points:
|
||||
max_value = getattr(strip, self.animated_property, 1.0)
|
||||
else:
|
||||
if self.type == 'IN':
|
||||
fade_end = strip.left_handle + self.duration
|
||||
keyframes = (k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end)
|
||||
if self.type == 'OUT':
|
||||
fade_start = strip.right_handle - self.duration
|
||||
keyframes = (k for k in reversed(fade_fcurve.keyframe_points) if k.co[0] <= fade_start)
|
||||
try:
|
||||
max_value = next(keyframes).co[1]
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
return max_value if max_value > 0.0 else 1.0
|
||||
|
||||
def __repr__(self):
|
||||
return "Fade {!r}: {!r} to {!r}".format(self.type, self.start, self.end)
|
||||
|
||||
|
||||
def calculate_duration_frames(scene, duration_seconds):
|
||||
return round(duration_seconds * scene.render.fps / scene.render.fps_base)
|
||||
|
||||
|
||||
class SequencerFileHandlerBase:
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
return (
|
||||
(context.region is not None) and
|
||||
(context.region.type == 'WINDOW') and
|
||||
(context.area is not None) and
|
||||
(context.area.ui_type == 'SEQUENCE_EDITOR')
|
||||
)
|
||||
|
||||
|
||||
class SEQUENCER_FH_image_strip(FileHandler, SequencerFileHandlerBase):
|
||||
bl_idname = "SEQUENCER_FH_image_strip"
|
||||
bl_label = "Image strip"
|
||||
bl_import_operator = "SEQUENCER_OT_image_strip_add"
|
||||
bl_file_extensions = ";".join(bpy.path.extensions_image)
|
||||
|
||||
|
||||
class SEQUENCER_FH_movie_strip(FileHandler, SequencerFileHandlerBase):
|
||||
bl_idname = "SEQUENCER_FH_movie_strip"
|
||||
bl_label = "Movie strip"
|
||||
bl_import_operator = "SEQUENCER_OT_movie_strip_add"
|
||||
bl_file_extensions = ";".join(bpy.path.extensions_movie)
|
||||
|
||||
|
||||
class SEQUENCER_FH_sound_strip(FileHandler, SequencerFileHandlerBase):
|
||||
bl_idname = "SEQUENCER_FH_sound_strip"
|
||||
bl_label = "Sound strip"
|
||||
bl_import_operator = "SEQUENCER_OT_sound_strip_add"
|
||||
bl_file_extensions = ";".join(bpy.path.extensions_audio)
|
||||
|
||||
|
||||
classes = (
|
||||
SequencerCrossfadeSounds,
|
||||
SequencerSplitMulticam,
|
||||
SequencerDeinterlaceSelectedMovies,
|
||||
SequencerFadesClear,
|
||||
SequencerFadesAdd,
|
||||
|
||||
SEQUENCER_FH_image_strip,
|
||||
SEQUENCER_FH_movie_strip,
|
||||
SEQUENCER_FH_sound_strip,
|
||||
)
|
||||
46
blender-5.2.0/scripts/startup/bl_operators/spreadsheet.py
Normal file
46
blender-5.2.0/scripts/startup/bl_operators/spreadsheet.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
class SPREADSHEET_OT_toggle_pin(Operator):
|
||||
"""Turn on or off pinning"""
|
||||
bl_idname = "spreadsheet.toggle_pin"
|
||||
bl_label = "Toggle Pin"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
return space and space.type == 'SPREADSHEET'
|
||||
|
||||
def execute(self, context):
|
||||
space = context.space_data
|
||||
|
||||
if space.is_pinned:
|
||||
self.unpin(context)
|
||||
else:
|
||||
self.pin(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
def pin(self, context):
|
||||
space = context.space_data
|
||||
space.is_pinned = True
|
||||
|
||||
def unpin(self, context):
|
||||
space = context.space_data
|
||||
space.is_pinned = False
|
||||
|
||||
|
||||
classes = (
|
||||
SPREADSHEET_OT_toggle_pin,
|
||||
)
|
||||
|
||||
if __name__ == "__main__": # Only for live edit.
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
1324
blender-5.2.0/scripts/startup/bl_operators/userpref.py
Normal file
1324
blender-5.2.0/scripts/startup/bl_operators/userpref.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"classes",
|
||||
)
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from bpy.props import (
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
STATUS_OK = (1 << 0)
|
||||
STATUS_ERR_ACTIVE_FACE = (1 << 1)
|
||||
STATUS_ERR_NOT_SELECTED = (1 << 2)
|
||||
STATUS_ERR_NOT_QUAD = (1 << 3)
|
||||
STATUS_ERR_MISSING_UV_LAYER = (1 << 4)
|
||||
STATUS_ERR_NO_FACES_SELECTED = (1 << 5)
|
||||
|
||||
|
||||
def extend(scene, obj, EXTEND_MODE, use_uv_selection):
|
||||
import bmesh
|
||||
from .uvcalc_transform import is_face_uv_selected_fn_from_context
|
||||
|
||||
me = obj.data
|
||||
|
||||
bm = bmesh.from_edit_mesh(me)
|
||||
|
||||
f_act = bm.faces.active
|
||||
|
||||
if f_act is None:
|
||||
return STATUS_ERR_ACTIVE_FACE # Active face cannot be none.
|
||||
if not f_act.select:
|
||||
return STATUS_ERR_NOT_SELECTED # Active face is not selected.
|
||||
if len(f_act.verts) != 4:
|
||||
return STATUS_ERR_NOT_QUAD # Active face is not a quad
|
||||
uv_act = bm.loops.layers.uv.active # Always use the active UV layer.
|
||||
if uv_act is None:
|
||||
return STATUS_ERR_MISSING_UV_LAYER # Object's mesh doesn't have any UV layers.
|
||||
|
||||
if use_uv_selection:
|
||||
face_select_test_fn = is_face_uv_selected_fn_from_context(scene, bm)
|
||||
faces = [
|
||||
f for f in bm.faces
|
||||
if f.select and len(f.verts) == 4 and face_select_test_fn(f, False)
|
||||
]
|
||||
else:
|
||||
faces = [
|
||||
f for f in bm.faces
|
||||
if f.select and len(f.verts) == 4
|
||||
]
|
||||
|
||||
if not faces:
|
||||
return STATUS_ERR_NO_FACES_SELECTED
|
||||
|
||||
# Our own local walker.
|
||||
|
||||
def walk_face_init(faces, f_act):
|
||||
# First tag all faces True (so we don't UV-map them).
|
||||
for f in bm.faces:
|
||||
f.tag = True
|
||||
# Then tag faces argument False.
|
||||
for f in faces:
|
||||
f.tag = False
|
||||
# Tag the active face True since we begin there.
|
||||
f_act.tag = True
|
||||
|
||||
def walk_face(f):
|
||||
# All faces in this list must be tagged.
|
||||
f.tag = True
|
||||
faces_a = [f]
|
||||
faces_b = []
|
||||
|
||||
while faces_a:
|
||||
for f in faces_a:
|
||||
for l in f.loops:
|
||||
l_edge = l.edge
|
||||
if (l_edge.is_manifold is True) and (l_edge.seam is False):
|
||||
l_other = l.link_loop_radial_next
|
||||
f_other = l_other.face
|
||||
if not f_other.tag:
|
||||
yield (f, l, f_other)
|
||||
f_other.tag = True
|
||||
faces_b.append(f_other)
|
||||
# Swap.
|
||||
faces_a, faces_b = faces_b, faces_a
|
||||
faces_b.clear()
|
||||
|
||||
# Utility, only for `walk_edgeloop_all`.
|
||||
def walk_edgeloop_all_impl_loop(loop_stack, edges_visited, l):
|
||||
l_other = l.link_loop_next.link_loop_next
|
||||
l_other_edge = l_other.edge
|
||||
if l_other_edge not in edges_visited:
|
||||
edges_visited.add(l_other_edge)
|
||||
yield l_other_edge
|
||||
if not l_other_edge.is_boundary:
|
||||
loop_stack.append(l_other)
|
||||
|
||||
def walk_edgeloop_all(e):
|
||||
# Walks over all edge loops connected by quads (even edges with 3+ users).
|
||||
# Could make this a generic function.
|
||||
|
||||
loop_stack = []
|
||||
edges_visited = {e}
|
||||
|
||||
yield e
|
||||
|
||||
# This initial iteration is needed because the loops never walk back over the face they come from.
|
||||
for l in e.link_loops:
|
||||
if len(l.face.verts) != 4:
|
||||
continue
|
||||
yield from walk_edgeloop_all_impl_loop(loop_stack, edges_visited, l)
|
||||
|
||||
while loop_stack and (l_test := loop_stack.pop()):
|
||||
# Walk around the quad and then onto the next face.
|
||||
l = l_test
|
||||
while (l := l.link_loop_radial_next) is not l_test:
|
||||
if len(l.face.verts) != 4:
|
||||
continue
|
||||
yield from walk_edgeloop_all_impl_loop(loop_stack, edges_visited, l)
|
||||
|
||||
def extrapolate_uv(
|
||||
fac,
|
||||
l_a_outer, l_a_inner,
|
||||
l_b_outer, l_b_inner,
|
||||
):
|
||||
l_b_inner[:] = l_a_inner
|
||||
l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac)
|
||||
|
||||
def apply_uv(_f_prev, l_prev, _f_next):
|
||||
l_a = [None, None, None, None]
|
||||
l_b = [None, None, None, None]
|
||||
|
||||
l_a[0] = l_prev
|
||||
l_a[1] = l_a[0].link_loop_next
|
||||
l_a[2] = l_a[1].link_loop_next
|
||||
l_a[3] = l_a[2].link_loop_next
|
||||
|
||||
# l_b
|
||||
# +-----------+
|
||||
# |(3) |(2)
|
||||
# | |
|
||||
# |l_next(0) |(1)
|
||||
# +-----------+
|
||||
# ^
|
||||
# l_a |
|
||||
# +-----------+
|
||||
# |l_prev(0) |(1)
|
||||
# | (f) |
|
||||
# |(3) |(2)
|
||||
# +-----------+
|
||||
# Copy from this face to the one above.
|
||||
|
||||
# Get the other loops.
|
||||
l_next = l_prev.link_loop_radial_next
|
||||
if l_next.vert != l_prev.vert:
|
||||
l_b[1] = l_next
|
||||
l_b[0] = l_b[1].link_loop_next
|
||||
l_b[3] = l_b[0].link_loop_next
|
||||
l_b[2] = l_b[3].link_loop_next
|
||||
else:
|
||||
l_b[0] = l_next
|
||||
l_b[1] = l_b[0].link_loop_next
|
||||
l_b[2] = l_b[1].link_loop_next
|
||||
l_b[3] = l_b[2].link_loop_next
|
||||
|
||||
l_a_uv = [l[uv_act].uv for l in l_a]
|
||||
l_b_uv = [l[uv_act].uv for l in l_b]
|
||||
|
||||
if EXTEND_MODE == 'LENGTH_AVERAGE':
|
||||
d1 = edge_lengths[l_a[1].edge.index][0]
|
||||
d2 = edge_lengths[l_b[2].edge.index][0]
|
||||
try:
|
||||
fac = d2 / d1
|
||||
except ZeroDivisionError:
|
||||
fac = 1.0
|
||||
elif EXTEND_MODE == 'LENGTH':
|
||||
a0, b0, c0 = l_a[3].vert.co, l_a[0].vert.co, l_b[3].vert.co
|
||||
a1, b1, c1 = l_a[2].vert.co, l_a[1].vert.co, l_b[2].vert.co
|
||||
|
||||
d1 = (a0 - b0).length + (a1 - b1).length
|
||||
d2 = (b0 - c0).length + (b1 - c1).length
|
||||
try:
|
||||
fac = d2 / d1
|
||||
except ZeroDivisionError:
|
||||
fac = 1.0
|
||||
else:
|
||||
fac = 1.0
|
||||
|
||||
extrapolate_uv(
|
||||
fac,
|
||||
l_a_uv[3], l_a_uv[0],
|
||||
l_b_uv[3], l_b_uv[0],
|
||||
)
|
||||
|
||||
extrapolate_uv(
|
||||
fac,
|
||||
l_a_uv[2], l_a_uv[1],
|
||||
l_b_uv[2], l_b_uv[1],
|
||||
)
|
||||
|
||||
# -------------------------------------------
|
||||
# Calculate average length per loop if needed.
|
||||
|
||||
if EXTEND_MODE == 'LENGTH_AVERAGE':
|
||||
bm.edges.index_update()
|
||||
edge_lengths = [None] * len(bm.edges)
|
||||
|
||||
for f in faces:
|
||||
# We know it's a quad.
|
||||
l_quad = f.loops[:]
|
||||
|
||||
# The opposite loops `l_quad[2]` & `l_quad[3]` are implicit (walking will handle).
|
||||
for l_init in (l_quad[0], l_quad[1]):
|
||||
# No need to check both because the initializing
|
||||
# one side of the pair will have initialized the second.
|
||||
l_init_edge = l_init.edge
|
||||
if edge_lengths[l_init_edge.index] is not None:
|
||||
continue
|
||||
|
||||
edge_length_store = [-1.0]
|
||||
edge_length_accum = 0.0
|
||||
edge_length_total = 0
|
||||
|
||||
for e in walk_edgeloop_all(l_init_edge):
|
||||
# Any previously met edges should have expanded into `l_init_edge`
|
||||
# (which has no length).
|
||||
assert edge_lengths[e.index] is None
|
||||
|
||||
edge_lengths[e.index] = edge_length_store
|
||||
edge_length_accum += e.calc_length()
|
||||
edge_length_total += 1
|
||||
|
||||
edge_length_store[0] = edge_length_accum / edge_length_total
|
||||
|
||||
# done with average length
|
||||
# ------------------------
|
||||
|
||||
walk_face_init(faces, f_act)
|
||||
for f_triple in walk_face(f_act):
|
||||
apply_uv(*f_triple)
|
||||
|
||||
bmesh.update_edit_mesh(me, loop_triangles=False)
|
||||
return STATUS_OK
|
||||
|
||||
|
||||
def main(context, operator):
|
||||
scene = context.scene
|
||||
use_uv_selection = True
|
||||
if context.space_data and context.space_data.type == 'VIEW_3D':
|
||||
use_uv_selection = False # When called from the 3D editor, UV selection is ignored.
|
||||
|
||||
num_meshes = 0
|
||||
num_errors = 0
|
||||
status = 0
|
||||
|
||||
ob_list = context.objects_in_mode_unique_data
|
||||
for ob in ob_list:
|
||||
num_meshes += 1
|
||||
ret = extend(scene, ob, operator.properties.mode, use_uv_selection)
|
||||
if ret != STATUS_OK:
|
||||
num_errors += 1
|
||||
status |= ret
|
||||
|
||||
if num_errors == num_meshes:
|
||||
if status & STATUS_ERR_NOT_QUAD:
|
||||
operator.report({'ERROR'}, "Active face must be a quad")
|
||||
elif status & STATUS_ERR_NOT_SELECTED:
|
||||
operator.report({'ERROR'}, "Active face not selected")
|
||||
elif status & STATUS_ERR_NO_FACES_SELECTED:
|
||||
operator.report({'ERROR'}, "No selected faces")
|
||||
elif status & STATUS_ERR_MISSING_UV_LAYER:
|
||||
operator.report({'ERROR'}, "No UV layers")
|
||||
else:
|
||||
assert status & STATUS_ERR_ACTIVE_FACE != 0
|
||||
operator.report({'ERROR'}, "No active face")
|
||||
|
||||
|
||||
class FollowActiveQuads(Operator):
|
||||
"""Follow UVs from active quads along continuous face loops"""
|
||||
bl_idname = "uv.follow_active_quads"
|
||||
bl_label = "Follow Active Quads"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode: EnumProperty(
|
||||
name="Edge Length Mode",
|
||||
description="Method to space UV edge loops",
|
||||
items=(
|
||||
('EVEN', "Even", "Space all UVs evenly"),
|
||||
('LENGTH', "Length", "Average space UVs edge length of each loop"),
|
||||
('LENGTH_AVERAGE', "Length Average", "Average space UVs edge length of each loop"),
|
||||
),
|
||||
default='LENGTH_AVERAGE',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
def execute(self, context):
|
||||
main(context, self)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
classes = (
|
||||
FollowActiveQuads,
|
||||
)
|
||||
690
blender-5.2.0/scripts/startup/bl_operators/uvcalc_lightmap.py
Normal file
690
blender-5.2.0/scripts/startup/bl_operators/uvcalc_lightmap.py
Normal file
@@ -0,0 +1,690 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
import mathutils
|
||||
|
||||
|
||||
class prettyface:
|
||||
__slots__ = (
|
||||
"uv",
|
||||
"width",
|
||||
"height",
|
||||
"children",
|
||||
"xoff",
|
||||
"yoff",
|
||||
"has_parent",
|
||||
"rot",
|
||||
)
|
||||
|
||||
def __init__(self, data):
|
||||
self.has_parent = False
|
||||
self.rot = False # only used for triangles
|
||||
self.xoff = 0
|
||||
self.yoff = 0
|
||||
|
||||
if type(data) == list: # list of data
|
||||
self.uv = None
|
||||
|
||||
# join the data
|
||||
if len(data) == 2:
|
||||
# 2 vertical blocks
|
||||
data[1].xoff = data[0].width
|
||||
self.width = data[0].width * 2
|
||||
self.height = data[0].height
|
||||
|
||||
elif len(data) == 4:
|
||||
# 4 blocks all the same size
|
||||
d = data[0].width # dimension x/y are the same
|
||||
|
||||
data[1].xoff += d
|
||||
data[2].yoff += d
|
||||
|
||||
data[3].xoff += d
|
||||
data[3].yoff += d
|
||||
|
||||
self.width = self.height = d * 2
|
||||
|
||||
# else:
|
||||
# print(len(data), data)
|
||||
# raise "Error"
|
||||
|
||||
for pf in data:
|
||||
pf.has_parent = True
|
||||
|
||||
self.children = data
|
||||
|
||||
elif type(data) == tuple:
|
||||
# 2 blender faces
|
||||
# f, (len_min, len_mid, len_max)
|
||||
self.uv = data
|
||||
|
||||
_f1, lens1, lens1ord = data[0]
|
||||
if data[1]:
|
||||
_f2, lens2, lens2ord = data[1]
|
||||
self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0
|
||||
self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0
|
||||
else: # 1 tri :/
|
||||
self.width = lens1[0]
|
||||
self.height = lens1[1]
|
||||
|
||||
self.children = []
|
||||
|
||||
else: # blender face
|
||||
uv_layer = data.id_data.uv_layers.active.data
|
||||
self.uv = [uv_layer[i].uv for i in data.loop_indices]
|
||||
|
||||
# cos = [v.co for v in data]
|
||||
cos = [data.id_data.vertices[v].co for v in data.vertices] # XXX25
|
||||
|
||||
if len(self.uv) == 4:
|
||||
self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0
|
||||
self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0
|
||||
else:
|
||||
# ngon, note:
|
||||
# for ngons to calculate the width/height we need to do the
|
||||
# whole projection, unlike other faces
|
||||
# we store normalized UVs in the faces coords to avoid
|
||||
# calculating the projection and rotating it twice.
|
||||
|
||||
no = data.normal
|
||||
r = no.rotation_difference(mathutils.Vector((0.0, 0.0, 1.0)))
|
||||
cos_2d = [(r @ co).xy for co in cos]
|
||||
# print(cos_2d)
|
||||
angle = mathutils.geometry.box_fit_2d(cos_2d)
|
||||
|
||||
mat = mathutils.Matrix.Rotation(angle, 2)
|
||||
cos_2d = [(mat @ co) for co in cos_2d]
|
||||
xs = [co.x for co in cos_2d]
|
||||
ys = [co.y for co in cos_2d]
|
||||
|
||||
xmin = min(xs)
|
||||
ymin = min(ys)
|
||||
xmax = max(xs)
|
||||
ymax = max(ys)
|
||||
|
||||
xspan = xmax - xmin
|
||||
yspan = ymax - ymin
|
||||
|
||||
self.width = xspan
|
||||
self.height = yspan
|
||||
|
||||
# ngons work different, we store projected result
|
||||
# in UVs to avoid having to re-project later.
|
||||
if xspan < 0.0000001 or yspan < 0.0000001:
|
||||
for i in range(len(cos_2d)):
|
||||
self.uv[i][:] = (0.0, 0.0)
|
||||
else:
|
||||
for i, co in enumerate(cos_2d):
|
||||
self.uv[i][:] = (
|
||||
(co.x - xmin) / xspan,
|
||||
(co.y - ymin) / yspan,
|
||||
)
|
||||
|
||||
self.children = []
|
||||
|
||||
def spin(self):
|
||||
if self.uv and len(self.uv) == 4:
|
||||
self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0]
|
||||
|
||||
self.width, self.height = self.height, self.width
|
||||
self.xoff, self.yoff = self.yoff, self.xoff # not needed?
|
||||
self.rot = not self.rot # only for tri pairs and ngons.
|
||||
# print("spinning")
|
||||
for pf in self.children:
|
||||
pf.spin()
|
||||
|
||||
def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h):
|
||||
from math import pi
|
||||
|
||||
xoff += self.xoff
|
||||
yoff += self.yoff
|
||||
|
||||
for pf in self.children:
|
||||
pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h)
|
||||
|
||||
uv = self.uv
|
||||
if not uv:
|
||||
return
|
||||
|
||||
x1 = xoff
|
||||
y1 = yoff
|
||||
x2 = xoff + self.width
|
||||
y2 = yoff + self.height
|
||||
|
||||
# Scale the values
|
||||
x1 = x1 / xfac + margin_w
|
||||
x2 = x2 / xfac - margin_w
|
||||
y1 = y1 / yfac + margin_h
|
||||
y2 = y2 / yfac - margin_h
|
||||
|
||||
# 2 Tri pairs
|
||||
if len(uv) == 2:
|
||||
# match the order of angle sizes of the 3d verts with the UV angles and rotate.
|
||||
def get_tri_angles(v1, v2, v3):
|
||||
a1 = (v2 - v1).angle(v3 - v1, pi)
|
||||
a2 = (v1 - v2).angle(v3 - v2, pi)
|
||||
a3 = pi - (a1 + a2) # a3= (v2 - v3).angle(v1 - v3)
|
||||
|
||||
return [(a1, 0), (a2, 1), (a3, 2)]
|
||||
|
||||
def set_uv(f, p1, p2, p3):
|
||||
|
||||
# cos =
|
||||
# v1 = cos[0]-cos[1]
|
||||
# v2 = cos[1]-cos[2]
|
||||
# v3 = cos[2]-cos[0]
|
||||
|
||||
# angles_co = get_tri_angles(*[v.co for v in f])
|
||||
angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices]) # XXX25
|
||||
|
||||
angles_co.sort()
|
||||
I = [i for a, i in angles_co]
|
||||
|
||||
uv_layer = f.id_data.uv_layers.active.data
|
||||
fuv = [uv_layer[i].uv for i in f.loop_indices]
|
||||
|
||||
if self.rot:
|
||||
fuv[I[2]][:] = p1
|
||||
fuv[I[1]][:] = p2
|
||||
fuv[I[0]][:] = p3
|
||||
else:
|
||||
fuv[I[2]][:] = p1
|
||||
fuv[I[0]][:] = p2
|
||||
fuv[I[1]][:] = p3
|
||||
|
||||
f = uv[0][0]
|
||||
|
||||
set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1))
|
||||
|
||||
if uv[1]:
|
||||
f = uv[1][0]
|
||||
set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2))
|
||||
|
||||
else: # 1 QUAD
|
||||
if len(uv) == 4:
|
||||
uv[1][:] = x1, y1
|
||||
uv[2][:] = x1, y2
|
||||
uv[3][:] = x2, y2
|
||||
uv[0][:] = x2, y1
|
||||
else:
|
||||
# NGon
|
||||
xspan = x2 - x1
|
||||
yspan = y2 - y1
|
||||
for uvco in uv:
|
||||
x, y = uvco
|
||||
uvco[:] = (
|
||||
(x1 + (x * xspan)),
|
||||
(y1 + (y * yspan))
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
# None unique hash
|
||||
return self.width, self.height
|
||||
|
||||
|
||||
def lightmap_uvpack(
|
||||
meshes,
|
||||
PREF_SEL_ONLY=True,
|
||||
PREF_NEW_UVLAYER=False,
|
||||
PREF_PACK_IN_ONE=False,
|
||||
PREF_BOX_DIV=8,
|
||||
PREF_MARGIN_DIV=512,
|
||||
):
|
||||
"""
|
||||
BOX_DIV if the maximum division of the UV map that
|
||||
a box may be consolidated into.
|
||||
A lower value will create more clumpy boxes and more wasted space,
|
||||
and a higher value will be slower but waste less space
|
||||
"""
|
||||
import time
|
||||
from math import sqrt
|
||||
|
||||
if not meshes:
|
||||
return
|
||||
|
||||
t = time.time()
|
||||
|
||||
if PREF_PACK_IN_ONE:
|
||||
face_groups = [[]]
|
||||
else:
|
||||
face_groups = []
|
||||
|
||||
for me in meshes:
|
||||
if PREF_SEL_ONLY:
|
||||
faces = [f for f in me.polygons if f.select]
|
||||
else:
|
||||
faces = me.polygons[:]
|
||||
|
||||
if PREF_PACK_IN_ONE:
|
||||
face_groups[0].extend(faces)
|
||||
else:
|
||||
face_groups.append(faces)
|
||||
|
||||
if PREF_NEW_UVLAYER:
|
||||
me.uv_layers.new()
|
||||
|
||||
# Add face UV if it does not exist.
|
||||
# All new faces are selected.
|
||||
if not me.uv_layers:
|
||||
me.uv_layers.new()
|
||||
|
||||
for face_sel in face_groups:
|
||||
print("\nStarting unwrap")
|
||||
|
||||
if not face_sel:
|
||||
continue
|
||||
|
||||
pretty_faces = [prettyface(f) for f in face_sel if f.loop_total >= 4]
|
||||
|
||||
# Do we have any triangles?
|
||||
if len(pretty_faces) != len(face_sel):
|
||||
|
||||
# Now add triangles, not so simple because we need to pair them up.
|
||||
def trylens(f):
|
||||
# f must be a tri
|
||||
|
||||
# cos = [v.co for v in f]
|
||||
cos = [f.id_data.vertices[v].co for v in f.vertices] # XXX25
|
||||
|
||||
lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length]
|
||||
|
||||
lens_min = lens.index(min(lens))
|
||||
lens_max = lens.index(max(lens))
|
||||
for i in range(3):
|
||||
if i != lens_min and i != lens_max:
|
||||
lens_mid = i
|
||||
break
|
||||
lens_order = lens_min, lens_mid, lens_max
|
||||
|
||||
return f, lens, lens_order
|
||||
|
||||
tri_lengths = [trylens(f) for f in face_sel if f.loop_total == 3]
|
||||
del trylens
|
||||
|
||||
# To add triangles into the light-map pack triangles are grouped in pairs to fill rectangular areas.
|
||||
# In the following for each triangle we add the sorted triangle edge lengths (3d point) into a KD-Tree
|
||||
# then iterate over all triangles and search for pairs of triangles by looking for the closest
|
||||
# sorted triangle point.
|
||||
# Additionally clusters of similar/equal triangles are parsed by searching for ranges in a second step.
|
||||
kd = mathutils.kdtree.KDTree(len(tri_lengths))
|
||||
for i, (f, lens, o) in enumerate(tri_lengths):
|
||||
vector = (lens[o[0]], lens[o[1]], lens[o[2]])
|
||||
kd.insert(vector, i)
|
||||
kd.balance()
|
||||
|
||||
added_ids = [False] * len(tri_lengths)
|
||||
pairs_added = 0
|
||||
tri_equality_threshold = 0.00001 # Add multiple pairs at once that are within this threshold.
|
||||
for i in range(len(tri_lengths)):
|
||||
if added_ids[i]:
|
||||
continue
|
||||
tri1 = tri_lengths[i]
|
||||
_f1, lens1, lo1 = tri1
|
||||
|
||||
sorted_l = (lens1[lo1[0]], lens1[lo1[1]], lens1[lo1[2]])
|
||||
added_ids[i] = True
|
||||
_vec, nearest, dist = kd.find(sorted_l, filter=lambda idx: not added_ids[idx])
|
||||
if not nearest or nearest < 0:
|
||||
pretty_faces.append(prettyface((tri1, None)))
|
||||
break
|
||||
tri2 = tri_lengths[nearest]
|
||||
pretty_faces.append(prettyface((tri1, tri2)))
|
||||
pairs_added = pairs_added + 1
|
||||
added_ids[nearest] = True
|
||||
|
||||
# Look in threshold proximity to add all similar/equal triangles in one go.
|
||||
# This code is not necessary but acts as a shortcut (~9% performance improvement).
|
||||
if dist < tri_equality_threshold:
|
||||
cluster_tri_ids = [
|
||||
idx for _, idx, _ in kd.find_range(sorted_l, tri_equality_threshold)
|
||||
if not added_ids[idx]
|
||||
]
|
||||
|
||||
if len(cluster_tri_ids) > 1:
|
||||
for ci in range(0, len(cluster_tri_ids) - (len(cluster_tri_ids) % 2), 2):
|
||||
pretty_faces.append(
|
||||
prettyface((tri_lengths[cluster_tri_ids[ci]], tri_lengths[cluster_tri_ids[ci + 1]]))
|
||||
)
|
||||
added_ids[cluster_tri_ids[ci]] = added_ids[cluster_tri_ids[ci + 1]] = True
|
||||
pairs_added = pairs_added + 1
|
||||
|
||||
# Get the min, max and total areas
|
||||
max_area = 0.0
|
||||
min_area = 100000000.0
|
||||
tot_area = 0
|
||||
for f in face_sel:
|
||||
area = f.area
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
if area < min_area:
|
||||
min_area = area
|
||||
tot_area += area
|
||||
|
||||
max_len = sqrt(max_area)
|
||||
min_len = sqrt(min_area)
|
||||
side_len = sqrt(tot_area)
|
||||
|
||||
# Build widths
|
||||
|
||||
curr_len = max_len
|
||||
|
||||
print("\tGenerating lengths...", end="")
|
||||
|
||||
lengths = []
|
||||
while curr_len > min_len:
|
||||
lengths.append(curr_len)
|
||||
curr_len = curr_len / 2.0
|
||||
|
||||
# Don't allow boxes smaller then the margin
|
||||
# since we contract on the margin, boxes that are smaller will create errors
|
||||
# print(curr_len, side_len/MARGIN_DIV)
|
||||
if curr_len / 4.0 < side_len / PREF_MARGIN_DIV:
|
||||
break
|
||||
|
||||
if not lengths:
|
||||
lengths.append(curr_len)
|
||||
|
||||
# convert into ints
|
||||
lengths_to_ints = {}
|
||||
|
||||
l_int = 1
|
||||
for l in reversed(lengths):
|
||||
lengths_to_ints[l] = l_int
|
||||
l_int *= 2
|
||||
|
||||
lengths_to_ints = list(lengths_to_ints.items())
|
||||
lengths_to_ints.sort()
|
||||
print("done")
|
||||
|
||||
# apply quantized values.
|
||||
|
||||
for pf in pretty_faces:
|
||||
w = pf.width
|
||||
h = pf.height
|
||||
bestw_diff = 1000000000.0
|
||||
besth_diff = 1000000000.0
|
||||
new_w = 0.0
|
||||
new_h = 0.0
|
||||
for l, i in lengths_to_ints:
|
||||
d = abs(l - w)
|
||||
if d < bestw_diff:
|
||||
bestw_diff = d
|
||||
new_w = i # assign the int version
|
||||
|
||||
d = abs(l - h)
|
||||
if d < besth_diff:
|
||||
besth_diff = d
|
||||
new_h = i # ditto
|
||||
|
||||
pf.width = new_w
|
||||
pf.height = new_h
|
||||
|
||||
if new_w > new_h:
|
||||
pf.spin()
|
||||
|
||||
print("...done")
|
||||
|
||||
# Since the boxes are sized in powers of 2, we can neatly group them into bigger squares
|
||||
# this is done hierarchically, so that we may avoid running the pack function
|
||||
# on many thousands of boxes, (under 1k is best) because it would get slow.
|
||||
# Using an odd and even dict is useful because they are packed differently
|
||||
# where w/h are the same, their packed in groups of 4
|
||||
# where they are different they are packed in pairs
|
||||
#
|
||||
# After this is done an external pack func is done that packs the whole group.
|
||||
|
||||
print("\tConsolidating Boxes...", end="")
|
||||
even_dict = {} # w/h are the same, the key is an int (w)
|
||||
odd_dict = {} # w/h are different, the key is the (w,h)
|
||||
|
||||
for pf in pretty_faces:
|
||||
w, h = pf.width, pf.height
|
||||
if w == h:
|
||||
even_dict.setdefault(w, []).append(pf)
|
||||
else:
|
||||
odd_dict.setdefault((w, h), []).append(pf)
|
||||
|
||||
# Count the number of boxes consolidated, only used for stats.
|
||||
c = 0
|
||||
|
||||
# This is tricky. the total area of all packed boxes, then sqrt() that to get an estimated size
|
||||
# this is used then converted into out INT space so we can compare it with
|
||||
# the ints assigned to the boxes size
|
||||
# and divided by BOX_DIV, basically if BOX_DIV is 8
|
||||
# ...then the maximum box consolidation (recursive grouping) will have a max width & height
|
||||
# ...1/8th of the UV size.
|
||||
# ...limiting this is needed or you end up with bug unused texture spaces
|
||||
# ...however if its too high, box-packing is way too slow for high poly meshes.
|
||||
float_to_int_factor = lengths_to_ints[0][0]
|
||||
if float_to_int_factor > 0:
|
||||
max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV)
|
||||
ok = True
|
||||
else:
|
||||
max_int_dimension = 0.0 # won't be used
|
||||
ok = False
|
||||
|
||||
# RECURSIVE pretty face grouping
|
||||
while ok:
|
||||
ok = False
|
||||
|
||||
# Tall boxes in groups of 2
|
||||
for d, boxes in list(odd_dict.items()):
|
||||
if d[1] < max_int_dimension:
|
||||
# boxes.sort(key=lambda a: len(a.children))
|
||||
while len(boxes) >= 2:
|
||||
# print("foo", len(boxes))
|
||||
ok = True
|
||||
c += 1
|
||||
pf_parent = prettyface([boxes.pop(), boxes.pop()])
|
||||
pretty_faces.append(pf_parent)
|
||||
|
||||
w, h = pf_parent.width, pf_parent.height
|
||||
assert w <= h
|
||||
|
||||
if w == h:
|
||||
even_dict.setdefault(w, []).append(pf_parent)
|
||||
else:
|
||||
odd_dict.setdefault((w, h), []).append(pf_parent)
|
||||
|
||||
# Even boxes in groups of 4
|
||||
for d, boxes in list(even_dict.items()):
|
||||
if d < max_int_dimension:
|
||||
boxes.sort(key=lambda a: len(a.children))
|
||||
|
||||
while len(boxes) >= 4:
|
||||
# print("bar", len(boxes))
|
||||
ok = True
|
||||
c += 1
|
||||
|
||||
pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()])
|
||||
pretty_faces.append(pf_parent)
|
||||
w = pf_parent.width # width and weight are the same
|
||||
even_dict.setdefault(w, []).append(pf_parent)
|
||||
|
||||
del even_dict
|
||||
del odd_dict
|
||||
|
||||
# orig = len(pretty_faces)
|
||||
|
||||
pretty_faces = [pf for pf in pretty_faces if not pf.has_parent]
|
||||
|
||||
# spin every second pretty-face
|
||||
# if there all vertical you get less efficiently used texture space
|
||||
i = len(pretty_faces)
|
||||
d = 0
|
||||
while i:
|
||||
i -= 1
|
||||
pf = pretty_faces[i]
|
||||
if pf.width != pf.height:
|
||||
d += 1
|
||||
if d % 2: # only pack every second
|
||||
pf.spin()
|
||||
# pass
|
||||
|
||||
print("Consolidated", c, "boxes, done")
|
||||
# print("done", orig, len(pretty_faces))
|
||||
|
||||
# boxes2Pack.append([islandIdx, w,h])
|
||||
print("\tPacking Boxes", len(pretty_faces), end="...")
|
||||
boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)]
|
||||
packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack)
|
||||
|
||||
# print(packWidth, packHeight)
|
||||
|
||||
packWidth = float(packWidth)
|
||||
packHeight = float(packHeight)
|
||||
|
||||
margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth
|
||||
margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight
|
||||
|
||||
# print(margin_w, margin_h)
|
||||
print("done")
|
||||
|
||||
# Apply the boxes back to the UV coords.
|
||||
print("\twriting back UVs", end="")
|
||||
for i, box in enumerate(boxes2Pack):
|
||||
pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h)
|
||||
# pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h)
|
||||
print("done")
|
||||
|
||||
for me in meshes:
|
||||
me.update()
|
||||
|
||||
print("finished all {:.2f} ".format(time.time() - t))
|
||||
|
||||
|
||||
def unwrap(operator, context, **kwargs):
|
||||
# switch to object mode
|
||||
is_editmode = context.object and context.object.mode == 'EDIT'
|
||||
if is_editmode:
|
||||
objects = context.objects_in_mode_unique_data
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
# define list of meshes
|
||||
meshes = list({
|
||||
me for obj in objects
|
||||
if obj.type == 'MESH'
|
||||
if (me := obj.data).polygons and me.is_editable
|
||||
})
|
||||
|
||||
if not meshes:
|
||||
operator.report({'ERROR'}, "No mesh object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
lightmap_uvpack(meshes, **kwargs)
|
||||
|
||||
# switch back to edit mode
|
||||
if is_editmode:
|
||||
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
from bpy.props import BoolProperty, FloatProperty, IntProperty
|
||||
|
||||
|
||||
class LightMapPack(Operator):
|
||||
"""Pack each face's UVs into the UV bounds"""
|
||||
bl_idname = "uv.lightmap_pack"
|
||||
bl_label = "Lightmap Pack"
|
||||
|
||||
# Disable REGISTER flag for now because this operator might create new
|
||||
# images. This leads to non-proper operator redo because current undo
|
||||
# stack is local for edit mode and can not remove images created by this
|
||||
# operator.
|
||||
# Proper solution would be to make undo stack aware of such things,
|
||||
# but for now just disable redo. Keep undo here so unwanted changes to uv
|
||||
# coords might be undone.
|
||||
# NOTE(@sergey): This fixes infinite image creation reported there #30968.
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
PREF_CONTEXT: bpy.props.EnumProperty(
|
||||
name="Selection",
|
||||
items=(
|
||||
('SEL_FACES', "Selected Faces", "Pack only selected faces"),
|
||||
('ALL_FACES', "All Faces", "Pack all faces in the mesh"),
|
||||
),
|
||||
)
|
||||
|
||||
# Image & UVs...
|
||||
PREF_PACK_IN_ONE: BoolProperty(
|
||||
name="Share Texture Space",
|
||||
description=(
|
||||
"Objects share texture space, map all objects "
|
||||
"into a single UV map"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
PREF_NEW_UVLAYER: BoolProperty(
|
||||
name="New UV Map",
|
||||
description="Create a new UV map for every mesh packed",
|
||||
default=False,
|
||||
)
|
||||
# UV Packing...
|
||||
PREF_BOX_DIV: IntProperty(
|
||||
name="Pack Quality",
|
||||
description=(
|
||||
"Quality of the packing. "
|
||||
"Higher values will be slower but waste less space"
|
||||
),
|
||||
min=1, max=48,
|
||||
default=12,
|
||||
)
|
||||
PREF_MARGIN_DIV: FloatProperty(
|
||||
name="Margin",
|
||||
description="Size of the margin as a division of the UV",
|
||||
min=0.001, max=1.0,
|
||||
default=0.1,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
is_editmode = context.active_object.mode == 'EDIT'
|
||||
if is_editmode:
|
||||
layout.prop(self, "PREF_CONTEXT")
|
||||
|
||||
layout.prop(self, "PREF_PACK_IN_ONE")
|
||||
layout.prop(self, "PREF_NEW_UVLAYER")
|
||||
layout.prop(self, "PREF_BOX_DIV")
|
||||
layout.prop(self, "PREF_MARGIN_DIV")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ob = context.active_object
|
||||
return ob and ob.type == 'MESH'
|
||||
|
||||
def execute(self, context):
|
||||
kwargs = self.as_keywords()
|
||||
PREF_CONTEXT = kwargs.pop("PREF_CONTEXT")
|
||||
|
||||
is_editmode = context.active_object.mode == 'EDIT'
|
||||
|
||||
if not is_editmode:
|
||||
kwargs["PREF_SEL_ONLY"] = False
|
||||
elif PREF_CONTEXT == 'SEL_FACES':
|
||||
kwargs["PREF_SEL_ONLY"] = True
|
||||
elif PREF_CONTEXT == 'ALL_FACES':
|
||||
kwargs["PREF_SEL_ONLY"] = False
|
||||
else:
|
||||
raise Exception("invalid context")
|
||||
|
||||
kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0))
|
||||
|
||||
return unwrap(self, context, **kwargs)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
classes = (
|
||||
LightMapPack,
|
||||
)
|
||||
551
blender-5.2.0/scripts/startup/bl_operators/uvcalc_transform.py
Normal file
551
blender-5.2.0/scripts/startup/bl_operators/uvcalc_transform.py
Normal file
@@ -0,0 +1,551 @@
|
||||
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"classes",
|
||||
|
||||
# While "internal" (not for user scripts) it's used by `uvcalc_follow_active`.
|
||||
"is_face_uv_selected_fn_from_context",
|
||||
)
|
||||
|
||||
import math
|
||||
|
||||
from bpy.types import Operator
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Local Utility Functions
|
||||
|
||||
# `sync_valid` functions.
|
||||
def is_face_uv_selected_for_uv_select_sync_valid(face, any_edge):
|
||||
if face.hide:
|
||||
return False
|
||||
if face.uv_select:
|
||||
return True
|
||||
if any_edge:
|
||||
for loop in face.loops:
|
||||
if loop.uv_select_edge:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_loop_edge_uv_selected_for_uv_select_sync_valid(loop):
|
||||
if loop.face.hide:
|
||||
return False
|
||||
if loop.uv_select_edge:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# `sync_invalid` functions.
|
||||
def is_face_uv_selected_for_uv_select_sync_invalid(face, any_edge):
|
||||
if face.hide:
|
||||
return False
|
||||
if face.select:
|
||||
return True
|
||||
if any_edge:
|
||||
for loop in face.loops:
|
||||
if loop.edge.select:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_loop_edge_uv_selected_for_uv_select_sync_invalid(loop):
|
||||
if loop.face.hide:
|
||||
return False
|
||||
if loop.edge.select:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# `no_sync` functions.
|
||||
def is_face_uv_selected_for_uv_select_no_sync(face, any_edge):
|
||||
if face.hide:
|
||||
return False
|
||||
if face.select:
|
||||
if face.uv_select:
|
||||
return True
|
||||
if any_edge:
|
||||
for loop in face.loops:
|
||||
if loop.uv_select_edge:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_loop_edge_uv_selected_for_uv_select_no_sync(loop):
|
||||
if loop.face.hide:
|
||||
return False
|
||||
if loop.face.select:
|
||||
if loop.uv_select_edge:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_face_uv_selected_fn_from_context(scene, bm):
|
||||
if scene.tool_settings.use_uv_select_sync:
|
||||
if bm.uv_select_sync_valid:
|
||||
return is_face_uv_selected_for_uv_select_sync_valid
|
||||
return is_face_uv_selected_for_uv_select_sync_invalid
|
||||
return is_face_uv_selected_for_uv_select_no_sync
|
||||
|
||||
|
||||
def is_loop_edge_uv_selected_fn_from_context(scene, bm):
|
||||
if scene.tool_settings.use_uv_select_sync:
|
||||
if bm.uv_select_sync_valid:
|
||||
return is_loop_edge_uv_selected_for_uv_select_sync_valid
|
||||
return is_loop_edge_uv_selected_for_uv_select_sync_invalid
|
||||
return is_loop_edge_uv_selected_for_uv_select_no_sync
|
||||
|
||||
|
||||
def is_island_uv_selected(island, any_edge, face_select_test_fn):
|
||||
# Returns True if the island is UV selected.
|
||||
#
|
||||
# :param island: list of faces to query.
|
||||
# :type island: Sequence[:class:`BMFace`]
|
||||
# :param any_edge: use edge selection instead of vertex selection.
|
||||
# :type any_edge: bool
|
||||
# :return: list of lists containing polygon indices.
|
||||
# :rtype: bool
|
||||
for face in island:
|
||||
if face_select_test_fn(face, any_edge):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def island_uv_bounds(island, uv_layer):
|
||||
# The UV bounds of UV island.
|
||||
#
|
||||
# :param island: list of faces to query.
|
||||
# :type island: Sequence[:class:`BMFace`]
|
||||
# :param uv_layer: the UV layer to source UVs from.
|
||||
# :return: U-min, V-min, U-max, V-max.
|
||||
# :rtype: list[float]
|
||||
minmax = [1e30, 1e30, -1e30, -1e30]
|
||||
for face in island:
|
||||
for loop in face.loops:
|
||||
u, v = loop[uv_layer].uv
|
||||
minmax[0] = min(minmax[0], u)
|
||||
minmax[1] = min(minmax[1], v)
|
||||
minmax[2] = max(minmax[2], u)
|
||||
minmax[3] = max(minmax[3], v)
|
||||
return minmax
|
||||
|
||||
|
||||
def island_uv_bounds_center(island, uv_layer):
|
||||
# The UV bounds center of UV island.
|
||||
#
|
||||
# :param island: list of faces to query.
|
||||
# :type island: Sequence[:class:`BMFace`]
|
||||
# :param uv_layer: the UV layer to source UVs from.
|
||||
# :return: U, V center.
|
||||
# :rtype: tuple[float, float]
|
||||
minmax = island_uv_bounds(island, uv_layer)
|
||||
return (
|
||||
(minmax[0] + minmax[2]) / 2.0,
|
||||
(minmax[1] + minmax[3]) / 2.0,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Align UV Rotation Operator
|
||||
|
||||
def find_rotation_auto(bm, uv_layer, faces, aspect_y):
|
||||
del bm
|
||||
sum_u = 0.0
|
||||
sum_v = 0.0
|
||||
for face in faces:
|
||||
prev_uv = face.loops[-1][uv_layer].uv
|
||||
for loop in face.loops:
|
||||
uv = loop[uv_layer].uv
|
||||
du = uv[0] - prev_uv[0]
|
||||
dv = uv[1] - prev_uv[1]
|
||||
edge_angle = math.atan2(dv, du * aspect_y)
|
||||
edge_angle *= 4.0 # Wrap 4 times around the circle
|
||||
sum_u += math.cos(edge_angle)
|
||||
sum_v += math.sin(edge_angle)
|
||||
prev_uv = uv
|
||||
|
||||
# Compute angle.
|
||||
return -math.atan2(sum_v, sum_u) / 4.0
|
||||
|
||||
|
||||
def find_rotation_edge(bm, uv_layer, faces, aspect_y, loop_edge_select_test_fn):
|
||||
del bm
|
||||
sum_u = 0.0
|
||||
sum_v = 0.0
|
||||
for face in faces:
|
||||
prev_uv = face.loops[-1][uv_layer].uv
|
||||
prev_select = loop_edge_select_test_fn(face.loops[-1])
|
||||
for loop in face.loops:
|
||||
uv = loop[uv_layer].uv
|
||||
if prev_select:
|
||||
du = uv[0] - prev_uv[0]
|
||||
dv = uv[1] - prev_uv[1]
|
||||
edge_angle = math.atan2(dv, du * aspect_y)
|
||||
edge_angle *= 2.0 # Wrap 2 times around the circle
|
||||
sum_u += math.cos(edge_angle)
|
||||
sum_v += math.sin(edge_angle)
|
||||
|
||||
prev_uv = uv
|
||||
prev_select = loop_edge_select_test_fn(loop)
|
||||
|
||||
# Add 90 degrees to align along V coordinate.
|
||||
# Twice, because we divide by two.
|
||||
sum_u, sum_v = -sum_u, -sum_v
|
||||
|
||||
# Compute angle.
|
||||
return -math.atan2(sum_v, sum_u) / 2.0
|
||||
|
||||
|
||||
def find_rotation_geometry(bm, uv_layer, faces, axis, aspect_y):
|
||||
del bm
|
||||
sum_u_co = Vector((0.0, 0.0, 0.0))
|
||||
sum_v_co = Vector((0.0, 0.0, 0.0))
|
||||
for face in faces:
|
||||
# Triangulate.
|
||||
for fan in range(2, len(face.loops)):
|
||||
delta_uv0 = face.loops[fan - 1][uv_layer].uv - face.loops[0][uv_layer].uv
|
||||
delta_uv1 = face.loops[fan][uv_layer].uv - face.loops[0][uv_layer].uv
|
||||
|
||||
delta_uv0[0] *= aspect_y
|
||||
delta_uv1[0] *= aspect_y
|
||||
|
||||
mat = Matrix((delta_uv0, delta_uv1))
|
||||
mat.invert_safe()
|
||||
|
||||
delta_co0 = face.loops[fan - 1].vert.co - face.loops[0].vert.co
|
||||
delta_co1 = face.loops[fan].vert.co - face.loops[0].vert.co
|
||||
w = delta_co0.cross(delta_co1).length
|
||||
# U direction in geometry coordinates.
|
||||
sum_u_co += (delta_co0 * mat[0][0] + delta_co1 * mat[0][1]) * w
|
||||
# V direction in geometry coordinates.
|
||||
sum_v_co += (delta_co0 * mat[1][0] + delta_co1 * mat[1][1]) * w
|
||||
|
||||
if axis == 'X':
|
||||
axis_index = 0
|
||||
elif axis == 'Y':
|
||||
axis_index = 1
|
||||
elif axis == 'Z':
|
||||
axis_index = 2
|
||||
|
||||
# Compute angle.
|
||||
return math.atan2(sum_u_co[axis_index], sum_v_co[axis_index])
|
||||
|
||||
|
||||
def align_uv_rotation_island(bm, uv_layer, faces, method, axis, aspect_y, loop_edge_select_test_fn):
|
||||
angle = 0.0
|
||||
if method == 'AUTO':
|
||||
angle = find_rotation_auto(bm, uv_layer, faces, aspect_y)
|
||||
elif method == 'EDGE':
|
||||
angle = find_rotation_edge(bm, uv_layer, faces, aspect_y, loop_edge_select_test_fn)
|
||||
elif method == 'GEOMETRY':
|
||||
angle = find_rotation_geometry(bm, uv_layer, faces, axis, aspect_y)
|
||||
|
||||
if angle == 0.0:
|
||||
return False # No change.
|
||||
|
||||
# Find bounding box center.
|
||||
mid_u, mid_v = island_uv_bounds_center(faces, uv_layer)
|
||||
|
||||
cos_angle = math.cos(angle)
|
||||
sin_angle = math.sin(angle)
|
||||
|
||||
delta_u = mid_u - cos_angle * mid_u + sin_angle / aspect_y * mid_v
|
||||
delta_v = mid_v - sin_angle * aspect_y * mid_u - cos_angle * mid_v
|
||||
|
||||
# Apply transform.
|
||||
for face in faces:
|
||||
for loop in face.loops:
|
||||
pre_uv = loop[uv_layer].uv
|
||||
u = cos_angle * pre_uv[0] - sin_angle / aspect_y * pre_uv[1] + delta_u
|
||||
v = sin_angle * aspect_y * pre_uv[0] + cos_angle * pre_uv[1] + delta_v
|
||||
loop[uv_layer].uv = u, v
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def align_uv_rotation_bmesh(bm, method, axis, aspect_y, loop_edge_select_test_fn, face_select_test_fn):
|
||||
import bpy_extras.bmesh_utils
|
||||
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
if not uv_layer:
|
||||
return False
|
||||
|
||||
islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
|
||||
changed = False
|
||||
for island in islands:
|
||||
if is_island_uv_selected(island, method == 'EDGE', face_select_test_fn):
|
||||
if align_uv_rotation_island(bm, uv_layer, island, method, axis, aspect_y, loop_edge_select_test_fn):
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def get_aspect_y(context):
|
||||
area = context.area
|
||||
if not area:
|
||||
return 1.0
|
||||
space_data = context.area.spaces.active
|
||||
if not space_data:
|
||||
return 1.0
|
||||
if not space_data.image:
|
||||
return 1.0
|
||||
image_width = space_data.image.size[0]
|
||||
image_height = space_data.image.size[1]
|
||||
if image_height:
|
||||
return image_width / image_height
|
||||
return 1.0
|
||||
|
||||
|
||||
def align_uv_rotation(context, method, axis, correct_aspect):
|
||||
import bmesh
|
||||
scene = context.scene
|
||||
|
||||
aspect_y = 1.0
|
||||
if correct_aspect:
|
||||
aspect_y = get_aspect_y(context)
|
||||
|
||||
ob_list = context.objects_in_mode_unique_data
|
||||
for ob in ob_list:
|
||||
bm = bmesh.from_edit_mesh(ob.data)
|
||||
if not bm.loops.layers.uv:
|
||||
continue
|
||||
loop_edge_select_test_fn = is_loop_edge_uv_selected_fn_from_context(scene, bm)
|
||||
face_select_test_fn = is_face_uv_selected_fn_from_context(scene, bm)
|
||||
if align_uv_rotation_bmesh(bm, method, axis, aspect_y, loop_edge_select_test_fn, face_select_test_fn):
|
||||
bmesh.update_edit_mesh(ob.data)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AlignUVRotation(Operator):
|
||||
"""Align the UV island's rotation"""
|
||||
bl_idname = "uv.align_rotation"
|
||||
bl_label = "Align Rotation"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
method: EnumProperty(
|
||||
name="Method", description="Method to calculate rotation angle",
|
||||
items=(
|
||||
('AUTO', "Auto", "Align from all edges"),
|
||||
('EDGE', "Edge", "Only selected edges"),
|
||||
('GEOMETRY', "Geometry", "Align to Geometry axis"),
|
||||
),
|
||||
)
|
||||
|
||||
axis: EnumProperty(
|
||||
name="Axis", description="Axis to align to",
|
||||
items=(
|
||||
('X', "X", "X axis"),
|
||||
('Y', "Y", "Y axis"),
|
||||
('Z', "Z", "Z axis"),
|
||||
),
|
||||
)
|
||||
|
||||
correct_aspect: BoolProperty(
|
||||
name="Correct Aspect",
|
||||
description="Take image aspect ratio into account",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
return align_uv_rotation(context, self.method, self.axis, self.correct_aspect)
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "method")
|
||||
if self.method == 'GEOMETRY':
|
||||
layout.prop(self, "axis")
|
||||
layout.prop(self, "correct_aspect")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Randomize UV Operator
|
||||
|
||||
def get_random_transform(transform_params, entropy):
|
||||
from random import uniform
|
||||
from random import seed as random_seed
|
||||
|
||||
(seed, loc, rot, scale, scale_even) = transform_params
|
||||
|
||||
# First, seed the RNG.
|
||||
random_seed(seed + entropy)
|
||||
|
||||
# Next, call uniform a known number of times.
|
||||
offset_u = uniform(0.0, 1.0)
|
||||
offset_v = uniform(0.0, 1.0)
|
||||
angle = uniform(0.0, 1.0)
|
||||
scale_u = uniform(0.0, 1.0)
|
||||
scale_v = uniform(0.0, 1.0)
|
||||
|
||||
# Apply the transform_params.
|
||||
if loc:
|
||||
offset_u *= loc[0]
|
||||
offset_v *= loc[1]
|
||||
else:
|
||||
offset_u = 0.0
|
||||
offset_v = 0.0
|
||||
|
||||
if rot:
|
||||
angle *= rot
|
||||
else:
|
||||
angle = 0.0
|
||||
|
||||
if scale:
|
||||
scale_u = scale_u * (2.0 * scale[0] - 2.0) + 2.0 - scale[0]
|
||||
scale_v = scale_v * (2.0 * scale[1] - 2.0) + 2.0 - scale[1]
|
||||
else:
|
||||
scale_u = 1.0
|
||||
scale_v = 1.0
|
||||
|
||||
if scale_even:
|
||||
scale_v = scale_u
|
||||
|
||||
# Results in homogeneous coordinates.
|
||||
return [[scale_u * math.cos(angle), -scale_v * math.sin(angle), offset_u],
|
||||
[scale_u * math.sin(angle), scale_v * math.cos(angle), offset_v]]
|
||||
|
||||
|
||||
def randomize_uv_transform_island(uv_layer, faces, transform_params):
|
||||
# Ensure consistent random values for island, regardless of selection etc.
|
||||
entropy = min(f.index for f in faces)
|
||||
|
||||
transform = get_random_transform(transform_params, entropy)
|
||||
|
||||
# Find bounding box center.
|
||||
mid_u, mid_v = island_uv_bounds_center(faces, uv_layer)
|
||||
|
||||
del_u = transform[0][2] + mid_u - transform[0][0] * mid_u - transform[0][1] * mid_v
|
||||
del_v = transform[1][2] + mid_v - transform[1][0] * mid_u - transform[1][1] * mid_v
|
||||
|
||||
# Apply transform.
|
||||
for face in faces:
|
||||
for loop in face.loops:
|
||||
pre_uv = loop[uv_layer].uv
|
||||
u = transform[0][0] * pre_uv[0] + transform[0][1] * pre_uv[1] + del_u
|
||||
v = transform[1][0] * pre_uv[0] + transform[1][1] * pre_uv[1] + del_v
|
||||
loop[uv_layer].uv = (u, v)
|
||||
|
||||
|
||||
def randomize_uv_transform_bmesh(bm, transform_params, face_select_test_fn):
|
||||
import bpy_extras.bmesh_utils
|
||||
uv_layer = bm.loops.layers.uv.verify()
|
||||
islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
|
||||
for island in islands:
|
||||
if is_island_uv_selected(island, False, face_select_test_fn):
|
||||
randomize_uv_transform_island(uv_layer, island, transform_params)
|
||||
|
||||
|
||||
def randomize_uv_transform(context, transform_params):
|
||||
import bmesh
|
||||
scene = context.scene
|
||||
ob_list = context.objects_in_mode_unique_data
|
||||
for ob in ob_list:
|
||||
bm = bmesh.from_edit_mesh(ob.data)
|
||||
if not bm.loops.layers.uv:
|
||||
continue
|
||||
|
||||
# Only needed to access the minimum face index of each island.
|
||||
bm.faces.index_update()
|
||||
face_select_test_fn = is_face_uv_selected_fn_from_context(scene, bm)
|
||||
randomize_uv_transform_bmesh(bm, transform_params, face_select_test_fn)
|
||||
|
||||
for ob in ob_list:
|
||||
bmesh.update_edit_mesh(ob.data)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RandomizeUVTransform(Operator):
|
||||
"""Randomize the UV island's location, rotation, and scale"""
|
||||
bl_idname = "uv.randomize_uv_transform"
|
||||
bl_label = "Randomize"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
random_seed: IntProperty(
|
||||
name="Random Seed",
|
||||
description="Seed value for the random generator",
|
||||
min=0,
|
||||
max=10000,
|
||||
default=0,
|
||||
)
|
||||
use_loc: BoolProperty(
|
||||
name="Randomize Location",
|
||||
description="Randomize the location values",
|
||||
default=True,
|
||||
)
|
||||
loc: FloatVectorProperty(
|
||||
name="Location",
|
||||
description="Maximum distance the objects can spread over each axis",
|
||||
min=-100.0,
|
||||
max=100.0,
|
||||
size=2,
|
||||
subtype='TRANSLATION',
|
||||
default=(0.0, 0.0),
|
||||
)
|
||||
use_rot: BoolProperty(
|
||||
name="Randomize Rotation",
|
||||
description="Randomize the rotation value",
|
||||
default=True,
|
||||
)
|
||||
rot: FloatProperty(
|
||||
name="Rotation",
|
||||
description="Maximum rotation",
|
||||
min=-2.0 * math.pi,
|
||||
max=2.0 * math.pi,
|
||||
subtype='ANGLE',
|
||||
default=0.0,
|
||||
)
|
||||
use_scale: BoolProperty(
|
||||
name="Randomize Scale",
|
||||
description="Randomize the scale values",
|
||||
default=True,
|
||||
)
|
||||
scale_even: BoolProperty(
|
||||
name="Scale Even",
|
||||
description="Use the same scale value for both axes",
|
||||
default=False,
|
||||
)
|
||||
|
||||
scale: FloatVectorProperty(
|
||||
name="Scale",
|
||||
description="Maximum scale randomization over each axis",
|
||||
min=-100.0,
|
||||
max=100.0,
|
||||
default=(1.0, 1.0),
|
||||
size=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
def execute(self, context):
|
||||
seed = self.random_seed
|
||||
|
||||
loc = [0.0, 0.0] if not self.use_loc else self.loc
|
||||
rot = 0.0 if not self.use_rot else self.rot
|
||||
scale = None if not self.use_scale else self.scale
|
||||
scale_even = self.scale_even
|
||||
|
||||
transform_params = [seed, loc, rot, scale, scale_even]
|
||||
return randomize_uv_transform(context, transform_params)
|
||||
|
||||
|
||||
classes = (
|
||||
AlignUVRotation,
|
||||
RandomizeUVTransform,
|
||||
)
|
||||
201
blender-5.2.0/scripts/startup/bl_operators/vertexpaint_dirt.py
Normal file
201
blender-5.2.0/scripts/startup/bl_operators/vertexpaint_dirt.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# SPDX-FileCopyrightText: 2009 Campbell Barton
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
def ensure_active_color_attribute(me):
|
||||
if me.attributes.active_color:
|
||||
return me.attributes.active_color
|
||||
return me.color_attributes.new("Color", 'BYTE_COLOR', 'CORNER')
|
||||
|
||||
|
||||
def applyVertexDirt(me, blur_iterations, blur_strength, clamp_dirt, clamp_clean, dirt_only, normalize):
|
||||
from mathutils import Vector
|
||||
from math import acos
|
||||
import array
|
||||
|
||||
# We simulate the accumulation of dirt in the creases of geometric surfaces
|
||||
# by comparing the vertex normal to the average direction of all vertices
|
||||
# connected to that vertex. We can also simulate surfaces being buffed or
|
||||
# worn by testing protruding surfaces.
|
||||
#
|
||||
# So if the angle between the normal and geometric direction is:
|
||||
# < 90 - dirt has accumulated in the crease
|
||||
# > 90 - surface has been worn or buffed
|
||||
# ~ 90 - surface is flat and is generally unworn and clean
|
||||
#
|
||||
# This method is limited by the complexity or lack there of in the geometry.
|
||||
#
|
||||
# Original code and method by Keith "Wahooney" Boshoff.
|
||||
|
||||
vert_tone = array.array("f", [0.0]) * len(me.vertices)
|
||||
|
||||
# create lookup table for each vertex's connected vertices (via edges)
|
||||
con = [[] for i in range(len(me.vertices))]
|
||||
|
||||
# add connected verts
|
||||
for e in me.edges:
|
||||
con[e.vertices[0]].append(e.vertices[1])
|
||||
con[e.vertices[1]].append(e.vertices[0])
|
||||
|
||||
for i, v in enumerate(me.vertices):
|
||||
vec = Vector()
|
||||
no = v.normal
|
||||
co = v.co
|
||||
|
||||
# get the direction of the vectors between the vertex and it's connected vertices
|
||||
for c in con[i]:
|
||||
vec += (me.vertices[c].co - co).normalized()
|
||||
|
||||
# average the vector by dividing by the number of connected verts
|
||||
tot_con = len(con[i])
|
||||
|
||||
if tot_con == 0:
|
||||
ang = pi / 2.0 # assume 90°, i. e. flat
|
||||
else:
|
||||
vec /= tot_con
|
||||
|
||||
# angle is the acos() of the dot product between normal and connected verts.
|
||||
# > 90 degrees: convex
|
||||
# < 90 degrees: concave
|
||||
ang = acos(no.dot(vec))
|
||||
|
||||
# enforce min/max
|
||||
ang = max(clamp_dirt, ang)
|
||||
|
||||
if not dirt_only:
|
||||
ang = min(clamp_clean, ang)
|
||||
|
||||
vert_tone[i] = ang
|
||||
|
||||
# blur tones
|
||||
for i in range(blur_iterations):
|
||||
# backup the original tones
|
||||
orig_vert_tone = vert_tone[:]
|
||||
|
||||
# use connected verts look up for blurring
|
||||
for j, c in enumerate(con):
|
||||
for v in c:
|
||||
vert_tone[j] += blur_strength * orig_vert_tone[v]
|
||||
|
||||
vert_tone[j] /= len(c) * blur_strength + 1
|
||||
del orig_vert_tone
|
||||
|
||||
if normalize:
|
||||
min_tone = min(vert_tone)
|
||||
max_tone = max(vert_tone)
|
||||
else:
|
||||
min_tone = clamp_dirt
|
||||
max_tone = clamp_clean
|
||||
|
||||
tone_range = max_tone - min_tone
|
||||
|
||||
if tone_range < 0.0001:
|
||||
# weak, don't cancel, see #43345
|
||||
tone_range = 0.0
|
||||
else:
|
||||
tone_range = 1.0 / tone_range
|
||||
|
||||
active_color_attribute = ensure_active_color_attribute(me)
|
||||
if not active_color_attribute:
|
||||
return {'CANCELLED'}
|
||||
|
||||
point_domain = active_color_attribute.domain == 'POINT'
|
||||
|
||||
attribute_data = active_color_attribute.data
|
||||
|
||||
use_paint_mask = me.use_paint_mask
|
||||
for i, p in enumerate(me.polygons):
|
||||
if p.hide:
|
||||
continue
|
||||
if use_paint_mask and not p.select:
|
||||
continue
|
||||
for loop_index in p.loop_indices:
|
||||
loop = me.loops[loop_index]
|
||||
v = loop.vertex_index
|
||||
col = attribute_data[v if point_domain else loop_index].color
|
||||
tone = vert_tone[v]
|
||||
tone = (tone - min_tone) * tone_range
|
||||
|
||||
if dirt_only:
|
||||
tone = min(tone, 0.5) * 2.0
|
||||
|
||||
col[0] = tone * col[0]
|
||||
col[1] = tone * col[1]
|
||||
col[2] = tone * col[2]
|
||||
me.update()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty, BoolProperty
|
||||
from math import pi
|
||||
|
||||
|
||||
class VertexPaintDirt(Operator):
|
||||
"""Generate a dirt map gradient based on cavity"""
|
||||
bl_idname = "paint.vertex_color_dirt"
|
||||
bl_label = "Dirty Vertex Colors"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
blur_strength: FloatProperty(
|
||||
name="Blur Strength",
|
||||
description="Blur strength per iteration",
|
||||
min=0.01, max=1.0,
|
||||
default=1.0,
|
||||
)
|
||||
blur_iterations: IntProperty(
|
||||
name="Blur Iterations",
|
||||
description="Number of times to blur the colors (higher blurs more)",
|
||||
min=0, max=40,
|
||||
default=1,
|
||||
)
|
||||
clean_angle: FloatProperty(
|
||||
name="Highlight Angle",
|
||||
description="Less than 90 limits the angle used in the tonal range",
|
||||
min=0.0, max=pi,
|
||||
default=pi,
|
||||
unit='ROTATION',
|
||||
)
|
||||
dirt_angle: FloatProperty(
|
||||
name="Dirt Angle",
|
||||
description="Less than 90 limits the angle used in the tonal range",
|
||||
min=0.0, max=pi,
|
||||
default=0.0,
|
||||
unit='ROTATION',
|
||||
)
|
||||
dirt_only: BoolProperty(
|
||||
name="Dirt Only",
|
||||
description="Don't calculate cleans for convex areas",
|
||||
default=False,
|
||||
)
|
||||
normalize: BoolProperty(
|
||||
name="Normalize",
|
||||
description="Normalize the colors, increasing the contrast",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return (obj and obj.type == 'MESH')
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
|
||||
ret = applyVertexDirt(
|
||||
mesh,
|
||||
self.blur_iterations,
|
||||
self.blur_strength,
|
||||
self.dirt_angle,
|
||||
self.clean_angle,
|
||||
self.dirt_only,
|
||||
self.normalize,
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
classes = (
|
||||
VertexPaintDirt,
|
||||
)
|
||||
316
blender-5.2.0/scripts/startup/bl_operators/view3d.py
Normal file
316
blender-5.2.0/scripts/startup/bl_operators/view3d.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# SPDX-FileCopyrightText: 2011-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
FileHandler,
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
bl_file_extensions_image_and_movie = ";".join((
|
||||
*bpy.path.extensions_image,
|
||||
*bpy.path.extensions_movie,
|
||||
))
|
||||
|
||||
|
||||
class VIEW3D_OT_edit_mesh_extrude_individual_move(Operator):
|
||||
"""Extrude each individual face separately along local normals"""
|
||||
bl_label = "Extrude Individual and Move"
|
||||
bl_idname = "view3d.edit_mesh_extrude_individual_move"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras.object_utils import object_report_if_active_shape_key_is_locked
|
||||
|
||||
ob = context.object
|
||||
if object_report_if_active_shape_key_is_locked(ob, self):
|
||||
return {'CANCELLED'}
|
||||
|
||||
mesh = ob.data
|
||||
select_mode = context.tool_settings.mesh_select_mode
|
||||
|
||||
totface = mesh.total_face_sel
|
||||
totedge = mesh.total_edge_sel
|
||||
# totvert = mesh.total_vert_sel
|
||||
|
||||
if select_mode[2] and totface == 1:
|
||||
bpy.ops.mesh.extrude_region_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
"orient_type": 'NORMAL',
|
||||
"constraint_axis": (False, False, True),
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
elif select_mode[2] and totface > 1:
|
||||
bpy.ops.mesh.extrude_faces_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_shrink_fatten={
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
elif select_mode[1] and totedge >= 1:
|
||||
bpy.ops.mesh.extrude_edges_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
else:
|
||||
bpy.ops.mesh.extrude_vertices_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
|
||||
# ignore return from operators above because they are 'RUNNING_MODAL',
|
||||
# and cause this one not to be freed. #24671.
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class VIEW3D_OT_edit_mesh_extrude_move(Operator):
|
||||
"""Extrude region together along the average normal"""
|
||||
bl_label = "Extrude and Move on Normals"
|
||||
bl_idname = "view3d.edit_mesh_extrude_move_normal"
|
||||
|
||||
dissolve_and_intersect: BoolProperty(
|
||||
name="Dissolve and Intersect",
|
||||
default=False,
|
||||
description="Dissolves adjacent faces and intersects new geometry",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
@staticmethod
|
||||
def extrude_region(operator, context, use_vert_normals, dissolve_and_intersect):
|
||||
from bpy_extras.object_utils import object_report_if_active_shape_key_is_locked
|
||||
|
||||
ob = context.object
|
||||
if object_report_if_active_shape_key_is_locked(ob, operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
mesh = ob.data
|
||||
|
||||
totface = mesh.total_face_sel
|
||||
totedge = mesh.total_edge_sel
|
||||
# totvert = mesh.total_vert_sel
|
||||
|
||||
if totface >= 1:
|
||||
if use_vert_normals:
|
||||
bpy.ops.mesh.extrude_region_shrink_fatten(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_shrink_fatten={
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
elif dissolve_and_intersect:
|
||||
bpy.ops.mesh.extrude_manifold(
|
||||
'INVOKE_REGION_WIN',
|
||||
MESH_OT_extrude_region={
|
||||
"use_dissolve_ortho_edges": True,
|
||||
},
|
||||
TRANSFORM_OT_translate={
|
||||
"orient_type": 'NORMAL',
|
||||
"constraint_axis": (False, False, True),
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
else:
|
||||
bpy.ops.mesh.extrude_region_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
"orient_type": 'NORMAL',
|
||||
"constraint_axis": (False, False, True),
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
|
||||
elif totedge == 1:
|
||||
bpy.ops.mesh.extrude_region_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
# Don't set the constraint axis since users will expect MMB
|
||||
# to use the user setting, see: #61637
|
||||
# "orient_type": 'NORMAL',
|
||||
# Not a popular choice, too restrictive for retopology.
|
||||
# "constraint_axis": (True, True, False),
|
||||
"constraint_axis": (False, False, False),
|
||||
"release_confirm": False,
|
||||
})
|
||||
else:
|
||||
bpy.ops.mesh.extrude_region_move(
|
||||
'INVOKE_REGION_WIN',
|
||||
TRANSFORM_OT_translate={
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
|
||||
# ignore return from operators above because they are 'RUNNING_MODAL',
|
||||
# and cause this one not to be freed. #24671.
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
return VIEW3D_OT_edit_mesh_extrude_move.extrude_region(self, context, False, self.dissolve_and_intersect)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class VIEW3D_OT_edit_mesh_extrude_shrink_fatten(Operator):
|
||||
"""Extrude region together along local normals"""
|
||||
bl_label = "Extrude and Move on Individual Normals"
|
||||
bl_idname = "view3d.edit_mesh_extrude_move_shrink_fatten"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
def execute(self, context):
|
||||
return VIEW3D_OT_edit_mesh_extrude_move.extrude_region(self, context, True, False)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class VIEW3D_OT_edit_mesh_extrude_manifold_normal(Operator):
|
||||
"""Extrude manifold region along normals"""
|
||||
bl_label = "Extrude Manifold Along Normals"
|
||||
bl_idname = "view3d.edit_mesh_extrude_manifold_normal"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'EDIT_MESH'
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras.object_utils import object_report_if_active_shape_key_is_locked
|
||||
|
||||
if object_report_if_active_shape_key_is_locked(context.object, self):
|
||||
return {'CANCELLED'}
|
||||
bpy.ops.mesh.extrude_manifold(
|
||||
'INVOKE_REGION_WIN',
|
||||
MESH_OT_extrude_region={
|
||||
"use_dissolve_ortho_edges": True,
|
||||
},
|
||||
TRANSFORM_OT_translate={
|
||||
"orient_type": 'NORMAL',
|
||||
"constraint_axis": (False, False, True),
|
||||
"release_confirm": False,
|
||||
},
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class VIEW3D_OT_transform_gizmo_set(Operator):
|
||||
"""Set the current transform gizmo"""
|
||||
bl_label = "Transform Gizmo Set"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_idname = "view3d.transform_gizmo_set"
|
||||
|
||||
extend: BoolProperty(
|
||||
name="Extend",
|
||||
default=False,
|
||||
)
|
||||
type: EnumProperty(
|
||||
name="Type",
|
||||
items=(
|
||||
('TRANSLATE', "Move", ""),
|
||||
('ROTATE', "Rotate", ""),
|
||||
('SCALE', "Scale", ""),
|
||||
),
|
||||
options={'ENUM_FLAG'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
area = context.area
|
||||
return area and (area.type == 'VIEW_3D')
|
||||
|
||||
def execute(self, context):
|
||||
space_data = context.space_data
|
||||
space_data.show_gizmo = True
|
||||
attrs = ("show_gizmo_object_translate", "show_gizmo_object_rotate", "show_gizmo_object_scale")
|
||||
attr_active = tuple(
|
||||
attrs[('TRANSLATE', 'ROTATE', 'SCALE').index(t)]
|
||||
for t in self.type
|
||||
)
|
||||
if self.extend:
|
||||
for attr in attrs:
|
||||
if attr in attr_active:
|
||||
setattr(space_data, attr, True)
|
||||
else:
|
||||
for attr in attrs:
|
||||
setattr(space_data, attr, attr in attr_active)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not self.properties.is_property_set("extend"):
|
||||
self.extend = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class VIEW3D_FH_empty_image(FileHandler):
|
||||
bl_idname = "VIEW3D_FH_empty_image"
|
||||
bl_label = "Add empty image"
|
||||
bl_import_operator = "OBJECT_OT_empty_image_add"
|
||||
bl_file_extensions = bl_file_extensions_image_and_movie
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
if not context.space_data or context.space_data.type != 'VIEW_3D':
|
||||
return False
|
||||
rv3d = context.space_data.region_3d
|
||||
return rv3d.view_perspective == 'PERSP' or rv3d.view_perspective == 'ORTHO'
|
||||
|
||||
|
||||
class VIEW3D_FH_camera_background_image(FileHandler):
|
||||
bl_idname = "VIEW3D_FH_camera_background_image"
|
||||
bl_label = "Add camera background image"
|
||||
bl_import_operator = "VIEW3D_OT_camera_background_image_add"
|
||||
bl_file_extensions = bl_file_extensions_image_and_movie
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
if not context.space_data or context.space_data.type != 'VIEW_3D':
|
||||
return False
|
||||
rv3d = context.space_data.region_3d
|
||||
return rv3d.view_perspective == 'CAMERA'
|
||||
|
||||
|
||||
class VIEW3D_FH_vdb_volume(FileHandler):
|
||||
bl_idname = "VIEW3D_FH_vdb_volume"
|
||||
bl_label = "OpenVDB volume"
|
||||
bl_import_operator = "OBJECT_OT_volume_import"
|
||||
bl_file_extensions = ".vdb"
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
return context.space_data and context.space_data.type == 'VIEW_3D'
|
||||
|
||||
|
||||
classes = (
|
||||
VIEW3D_OT_edit_mesh_extrude_individual_move,
|
||||
VIEW3D_OT_edit_mesh_extrude_move,
|
||||
VIEW3D_OT_edit_mesh_extrude_shrink_fatten,
|
||||
VIEW3D_OT_edit_mesh_extrude_manifold_normal,
|
||||
VIEW3D_OT_transform_gizmo_set,
|
||||
VIEW3D_FH_camera_background_image,
|
||||
VIEW3D_FH_empty_image,
|
||||
VIEW3D_FH_vdb_volume,
|
||||
)
|
||||
3743
blender-5.2.0/scripts/startup/bl_operators/wm.py
Normal file
3743
blender-5.2.0/scripts/startup/bl_operators/wm.py
Normal file
File diff suppressed because it is too large
Load Diff
168
blender-5.2.0/scripts/startup/bl_operators/world.py
Normal file
168
blender-5.2.0/scripts/startup/bl_operators/world.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
|
||||
|
||||
class WORLD_OT_convert_volume_to_mesh(bpy.types.Operator):
|
||||
"""Convert the volume of a world to a mesh. """ \
|
||||
"""The world's volume used to be rendered by EEVEE Legacy. Conversion is needed for it to render properly"""
|
||||
bl_label = "Convert Volume"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_idname = "world.convert_volume_to_mesh"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
world = cls._world_get(context)
|
||||
if not world:
|
||||
return False
|
||||
|
||||
ntree = world.node_tree
|
||||
node = ntree.get_output_node('EEVEE')
|
||||
return bool(node.inputs["Volume"].links)
|
||||
|
||||
def execute(self, context):
|
||||
cls = self.__class__
|
||||
world = cls._world_get(context)
|
||||
view_layer = context.view_layer
|
||||
|
||||
world_tree = world.node_tree
|
||||
world_output = world_tree.get_output_node('EEVEE')
|
||||
name = "{:s}_volume".format(world.name)
|
||||
|
||||
collection = bpy.data.collections.new(name)
|
||||
view_layer.layer_collection.collection.children.link(collection)
|
||||
|
||||
# Add World Volume Mesh object to scene
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
object = bpy.data.objects.new(name, mesh)
|
||||
object.display.show_shadows = False
|
||||
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_icosphere(bm, subdivisions=0, radius=1e5)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
# Remove all non-essential attributes
|
||||
for attribute in mesh.attributes:
|
||||
if attribute.is_internal or attribute.is_required:
|
||||
continue
|
||||
mesh.attributes.remove(attribute)
|
||||
|
||||
material = bpy.data.materials.new(name)
|
||||
mesh.materials.append(material)
|
||||
volume_tree = material.node_tree
|
||||
volume_tree.nodes.clear()
|
||||
volume_tree.nodes.new("ShaderNodeOutputMaterial")
|
||||
volume_output = volume_tree.get_output_node('EEVEE')
|
||||
|
||||
links_to_add = []
|
||||
self._sync_rna_properties(volume_output, world_output)
|
||||
self._sync_node_input(
|
||||
volume_tree,
|
||||
volume_output,
|
||||
volume_output.inputs["Volume"],
|
||||
world_output,
|
||||
world_output.inputs["Volume"],
|
||||
links_to_add,
|
||||
)
|
||||
self._sync_links(volume_tree, links_to_add)
|
||||
|
||||
# Add transparent volume for other render engines
|
||||
if volume_output.target == 'EEVEE':
|
||||
all_output = volume_tree.nodes.new(type="ShaderNodeOutputMaterial")
|
||||
transparent = volume_tree.nodes.new(type="ShaderNodeBsdfTransparent")
|
||||
volume_tree.links.new(transparent.outputs[0], all_output.inputs[0])
|
||||
|
||||
# Remove all volume links from the world node tree.
|
||||
for link in world_output.inputs["Volume"].links:
|
||||
world_tree.links.remove(link)
|
||||
|
||||
collection.objects.link(object)
|
||||
object.select_set(True)
|
||||
view_layer.objects.active = object
|
||||
|
||||
world.use_eevee_finite_volume = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def _world_get(context):
|
||||
if world := getattr(context, "world", None):
|
||||
return world
|
||||
return context.scene.world
|
||||
|
||||
def _sync_node_input(
|
||||
self,
|
||||
dst_tree, # bpy.types.NodeTree
|
||||
dst_node, # bpy.types.Node
|
||||
dst_socket, # bpy.types.NodeSocket
|
||||
src_node, # bpy.types.Node
|
||||
src_socket, # bpy.types.NodeSocket
|
||||
links_to_add,
|
||||
): # -> None
|
||||
self._sync_rna_properties(dst_socket, src_socket)
|
||||
for src_link in src_socket.links:
|
||||
src_linked_node = src_link.from_node
|
||||
dst_linked_node = self._sync_node(dst_tree, src_linked_node, links_to_add)
|
||||
|
||||
from_socket_index = src_node.outputs.find(src_link.from_socket.name)
|
||||
dst_tree.links.new(
|
||||
dst_linked_node.outputs[from_socket_index],
|
||||
dst_socket,
|
||||
)
|
||||
|
||||
def _sync_node(
|
||||
self,
|
||||
dst_tree, # bpy.types.NodeTree
|
||||
src_node, # bpy.types.Node
|
||||
links_to_add,
|
||||
): # -> bpy.types.Node
|
||||
"""
|
||||
Find the counter part of the src_node in dst_tree. When found return the counter part. When not found
|
||||
create the counter part, sync it and return the created node.
|
||||
"""
|
||||
if src_node.name in dst_tree.nodes:
|
||||
return dst_tree.nodes[src_node.name]
|
||||
|
||||
dst_node = dst_tree.nodes.new(src_node.bl_idname)
|
||||
|
||||
self._sync_rna_properties(dst_node, src_node)
|
||||
self._sync_node_inputs(dst_tree, dst_node, src_node, links_to_add)
|
||||
return dst_node
|
||||
|
||||
def _sync_rna_properties(self, dst, src): # -> None
|
||||
for rna_prop in src.bl_rna.properties:
|
||||
if rna_prop.is_readonly:
|
||||
continue
|
||||
|
||||
attr_name = rna_prop.identifier
|
||||
if attr_name in {"bl_idname", "bl_static_type"}:
|
||||
continue
|
||||
setattr(dst, attr_name, getattr(src, attr_name))
|
||||
|
||||
def _sync_node_inputs(
|
||||
self,
|
||||
dst_tree, # bpy.types.NodeTree
|
||||
dst_node, # bpy.types.Node
|
||||
src_node, # bpy.types.Node
|
||||
links_to_add,
|
||||
): # -> None
|
||||
for index in range(len(src_node.inputs)):
|
||||
src_socket = src_node.inputs[index]
|
||||
dst_socket = dst_node.inputs[index]
|
||||
self._sync_node_input(dst_tree, dst_node, dst_socket, src_node, src_socket, links_to_add)
|
||||
|
||||
def _sync_links(
|
||||
self,
|
||||
dst_tree, # bpy.types.NodeTree
|
||||
links_to_add,
|
||||
): # -> None
|
||||
pass
|
||||
|
||||
|
||||
classes = (
|
||||
WORLD_OT_convert_volume_to_mesh,
|
||||
)
|
||||
Reference in New Issue
Block a user