chore: checkpoint web Blender migration source, tools and docs
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -3,6 +3,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from contextlib import ExitStack, contextmanager
from bpy.types import Operator
from bpy.props import FloatProperty
from mathutils import (
@@ -415,33 +416,507 @@ class CLIP_OT_set_viewport_background(Operator):
class CLIP_OT_constraint_to_fcurve(Operator):
"""Create F-Curves for object which will copy """ \
"""object's movement caused by this constraint"""
"""Bake movement through this constraint and remove it and all preceding constraints"""
bl_idname = "clip.constraint_to_fcurve"
bl_label = "Constraint to F-Curve"
bl_options = {'UNDO', 'REGISTER'}
def _bake_object(self, scene, ob):
con = None
@classmethod
def poll(cls, context):
con = getattr(context, "constraint", None)
if con is None:
return context.scene is not None
ob = context.object
if con.type not in {'CAMERA_SOLVER', 'FOLLOW_TRACK', 'OBJECT_SOLVER'}:
cls.poll_message_set("A motion tracking constraint is required")
return False
bone = getattr(context, "pose_bone", None)
if ob is None or not ob.is_editable or con.id_data != ob:
cls.poll_message_set("An editable constraint object is required")
return False
if bone is not None and (bone.id_data != ob or ob.pose is None or
ob.pose.bones.get(bone.name) != bone or not ob.data.is_editable):
cls.poll_message_set("An editable pose bone belonging to the constraint object is required")
return False
owner = bone if bone is not None else ob
if con not in owner.constraints.values():
cls.poll_message_set("The constraint must belong to the displayed object or bone")
return False
reason = cls._isolation_error(owner, con)
if reason is None and bone is not None:
reason = cls._pose_bake_error(ob, cls._pose_bake_plan(owner, con))
if reason:
cls.poll_message_set(reason)
return False
for chain_owner in cls._pose_bake_chain(owner, con):
rotation_path = ('rotation_quaternion' if chain_owner.rotation_mode == 'QUATERNION'
else 'rotation_axis_angle' if chain_owner.rotation_mode == 'AXIS_ANGLE'
else 'rotation_euler')
if any(chain_owner.is_property_readonly(path)
for path in ('location', rotation_path, 'scale')):
cls.poll_message_set("A pose chain transform channel is read-only")
return False
if (not context.scene.is_editable or context.scene.objects.get(ob.name) != ob or
context.view_layer.objects.get(ob.name) != ob):
cls.poll_message_set("The constraint object must belong to the current editable scene and view layer")
return False
if ob.animation_data and ob.animation_data.action and not ob.animation_data.action.is_editable:
cls.poll_message_set("The object's Action is read-only")
return False
reason = cls._tracking_error(context.scene, con)
if reason:
cls.poll_message_set(reason)
return False
return True
@staticmethod
def _tracking_error(scene, con):
# Match the resources used by followtrack_context_init and the solver
# evaluators. A native no-op must not become a successful destructive bake.
clip = scene.active_clip if con.use_active_clip else con.clip
if clip is None:
return "Movie clip to use tracking data from isn't set"
if con.type != 'CAMERA_SOLVER' and (getattr(con, "camera", None) or scene.camera) is None:
return "A camera is required to bake this tracking constraint"
if con.type == 'OBJECT_SOLVER' or (con.type == 'FOLLOW_TRACK' and con.object):
tracking_object = clip.tracking.objects.get(con.object)
else:
tracking_object = next((obj for obj in clip.tracking.objects if obj.is_camera), None)
if tracking_object is None:
return "Motion Tracking object not found"
if con.type in {'CAMERA_SOLVER', 'OBJECT_SOLVER'}:
reconstruction = tracking_object.reconstruction
# The native interpolator returns identity for an empty camera array.
# Do not bake that fallback and remove a constraint awaiting a solve.
if not reconstruction.is_valid:
return "Solve the tracking object before baking this constraint"
if not reconstruction.cameras:
return "The tracking reconstruction has no solved cameras to bake"
return None
tracks = tracking_object.tracks
if con.type == 'FOLLOW_TRACK':
track = tracks.get(con.track)
if track is None:
return "Motion Tracking track not found"
if con.use_3d_position:
if not track.has_bundle:
return "The tracking track has no reconstructed 3D position"
return None
tracks = (track,)
if not any(len(track.markers) for track in tracks):
return "The tracking resource has no markers to bake"
return None
@staticmethod
def _isolation_drivers(owner, following):
# RNA supplies the escaped object/pose constraint path. Match the mute
# channel only: drivers of influence, transforms and tracking resources
# must keep evaluating while we sample the selected constraint.
animation_data = owner.id_data.animation_data
if animation_data is None:
return []
paths = {other.path_from_id("mute") for other in following}
return [curve for curve in animation_data.drivers if curve.data_path in paths]
@staticmethod
def _mute_channelbags(action, slot_handle, paths):
# Blender 5.2 evaluates the assigned slot, not the Action's first slot.
for layer in action.layers:
for strip in layer.strips:
if strip.type != 'KEYFRAME':
continue
for bag in strip.channelbags:
if bag.slot_handle == slot_handle:
for curve in bag.fcurves:
if curve.data_path in paths:
yield bag, curve
@staticmethod
def _isolation_action_bindings(owner, following):
animation_data = owner.id_data.animation_data
if animation_data is None or not following:
return []
paths = {other.path_from_id("mute") for other in following}
def strip_bindings(strips):
for strip in strips:
if strip.type == 'CLIP':
yield strip
elif strip.type == 'META':
yield from strip_bindings(strip.strips)
# Include muted/solo-excluded strips too: NLA's evaluation domain visits
# their channels and can reset a property even when the strip contributes
# no value at this frame. Keep timing, blending and track states intact.
bindings = [animation_data]
if animation_data.use_nla:
for track in animation_data.nla_tracks:
bindings.extend(strip_bindings(track.strips))
return [binding for binding in bindings if binding.action is not None and
any(CLIP_OT_constraint_to_fcurve._mute_channelbags(
binding.action, binding.action_slot_handle, paths))]
@staticmethod
def _restore_action_binding(binding, action, slot_handle, last_identifier):
if binding.action != action:
binding.action = action
if binding.action_slot_handle != slot_handle:
binding.action_slot_handle = slot_handle
if last_identifier is not None and binding.last_slot_identifier != last_identifier:
binding.last_slot_identifier = last_identifier
if binding.action != action or binding.action_slot_handle != slot_handle:
raise RuntimeError("Could not restore the constraint owner's Action/slot")
@staticmethod
def _remove_isolation_action(action):
# Never unlink a still-bound copy if RNA restoration failed. The caller
# reports failure and the Web transaction restores Main in that case.
if action.users:
raise RuntimeError("Could not release the temporary bake Action")
bpy.data.actions.remove(action)
@classmethod
@contextmanager
def _isolated_actions(cls, owner, con, view_layer):
constraints = list(owner.constraints)
following = constraints[constraints.index(con) + 1:]
bindings = cls._isolation_action_bindings(owner, following)
paths = {other.path_from_id("mute") for other in following}
try:
with ExitStack() as cleanup:
copies = {}
# Copy once per Action/slot for the entire sampling range. Shared
# objects and other slots keep their original F-Curves untouched.
for binding in bindings:
action = binding.action
handle = binding.action_slot_handle
key = (action.as_pointer(), handle)
if key not in copies:
copy = action.copy()
copy.use_fake_user = False
cleanup.callback(cls._remove_isolation_action, copy)
for bag, curve in list(cls._mute_channelbags(copy, handle, paths)):
bag.fcurves.remove(curve)
copies[key] = copy
for binding in bindings:
action = binding.action
handle = binding.action_slot_handle
cleanup.callback(cls._restore_action_binding, binding, action, handle,
getattr(binding, "last_slot_identifier", None))
binding.action = copies[(action.as_pointer(), handle)]
binding.action_slot_handle = handle
if (binding.action != copies[(action.as_pointer(), handle)] or
binding.action_slot_handle != handle):
raise RuntimeError("Could not isolate the constraint owner's Action/slot")
yield
finally:
# All original bindings are back before evaluating or writing keys.
view_layer.update()
@classmethod
@contextmanager
def _bake_action(cls, owner):
animation_data = owner.id_data.animation_data_create()
action = animation_data.action
if action is None or action.users == 1:
yield
return
# Key insertion must not change another object's Action or a retained
# NLA strip using it. Keep a local copy only when the bake succeeds.
if (animation_data.is_property_readonly("action") or
animation_data.is_property_readonly("action_slot_handle")):
raise RuntimeError("Exit NLA Tweak Mode or make the bake Action binding editable before baking")
handle = animation_data.action_slot_handle
last_identifier = animation_data.last_slot_identifier
with ExitStack() as rollback:
copy = action.copy()
copy.use_fake_user = False
rollback.callback(cls._remove_isolation_action, copy)
rollback.callback(cls._restore_action_binding, animation_data, action, handle, last_identifier)
animation_data.action = copy
animation_data.action_slot_handle = handle
if animation_data.action != copy or animation_data.action_slot_handle != handle:
raise RuntimeError("Could not assign the constraint owner's bake Action/slot")
yield
rollback.pop_all()
@staticmethod
def _isolation_error(owner, con):
# Sample the ordered prefix using Blender's solver, then consume that
# prefix. Trying to invert predecessors (Limit, for example) is lossy;
# retaining them would apply their effects to the baked channels twice.
after = False
following = []
for other in owner.constraints:
if not after:
if other.is_override_data:
return "The baked constraint prefix contains a protected library override constraint"
if other == con:
after = True
elif after:
following.append(other)
if not other.mute and other.is_property_readonly("mute"):
return "A following constraint cannot be temporarily muted for baking"
for curve in CLIP_OT_constraint_to_fcurve._isolation_drivers(owner, following):
if not curve.mute and curve.is_property_readonly("mute"):
return "A following constraint's mute driver is read-only"
for binding in CLIP_OT_constraint_to_fcurve._isolation_action_bindings(owner, following):
if (binding.is_property_readonly("action") or
binding.is_property_readonly("action_slot_handle")):
return "Exit NLA Tweak Mode or make the following constraint's Action binding editable before baking"
return None if after else "Motion Tracking constraint no longer belongs to this owner"
@staticmethod
def _solver_chain(owner, con):
if con.type not in {'IK', 'SPLINE_IK'}:
return ()
# Match the native solver's tip and chain-count semantics. In particular,
# IK without Use Tail starts at the parent; zero means all parents only
# for IK, not Spline IK (which has no segments at zero).
tip = owner
count = con.chain_count
if con.type == 'IK':
if not con.use_tail and tip.parent is not None:
tip = tip.parent
count = min(count or 256, 256)
else:
count = min(count, 255)
chain = []
while tip is not None and len(chain) < count:
chain.append(tip)
tip = tip.parent
return tuple(chain)
@classmethod
def _pose_bake_plan(cls, owner, con):
constraints = list(owner.constraints)
plan = [(owner, constraints[:constraints.index(con) + 1])]
seen = {owner.name}
pose_bones = owner.id_data.pose.bones
def include(bone, consumed):
consumed = list(consumed)
for index, (planned_bone, planned_constraints) in enumerate(plan):
if planned_bone.name == bone.name:
if len(consumed) > len(planned_constraints):
plan[index] = (planned_bone, consumed)
return True
return False
plan.append((bone, consumed))
seen.add(bone.name)
return True
# A parent's visual local matrix includes its own complete stack. Consume
# that stack too, following any of its solvers to their additional parents,
# or those constraints would be applied twice to the baked transform.
# If another branch solver reaches any planned bone, absorb that owner's
# prefix through the last intersecting solver as well. Preserve its suffix
# using the same Action/NLA/driver isolation as the selected owner. Grow
# both the bone set and prefixes until newly included solvers add nothing.
changed = True
while changed:
changed = False
for bone, consumed in tuple(plan):
for other in consumed:
for affected in cls._solver_chain(bone, other):
if affected.name not in seen:
include(affected, affected.constraints)
changed = True
planned_names = {bone.name for bone, _consumed in plan}
for branch in pose_bones:
if branch == owner:
continue
stack = list(branch.constraints)
endpoints = [index for index, other in enumerate(stack)
if any(affected.name in planned_names
for affected in cls._solver_chain(branch, other))]
if endpoints:
changed = include(branch, stack[:endpoints[-1] + 1]) or changed
return plan
@classmethod
def _pose_bake_chain(cls, owner, con):
if not isinstance(owner, bpy.types.PoseBone):
return (owner,)
return tuple(bone for bone, _constraints in cls._pose_bake_plan(owner, con))
@classmethod
def _pose_bake_error(cls, ob, plan):
for bone, constraints in plan:
if constraints:
reason = cls._isolation_error(bone, constraints[-1])
if reason:
return reason
if any(other.is_override_data for other in constraints):
return "The baked pose chain contains a protected library override constraint"
rotation_path = ('rotation_quaternion' if bone.rotation_mode == 'QUATERNION'
else 'rotation_axis_angle' if bone.rotation_mode == 'AXIS_ANGLE'
else 'rotation_euler')
if any(bone.is_property_readonly(path) for path in ('location', rotation_path, 'scale')):
return "A pose chain transform channel is read-only"
return None
def _bake_object(self, scene, ob, con=None, pose_bone=None, view_layer=None):
clip = None
sfra = None
efra = None
frame_current = scene.frame_current
subframe_current = scene.frame_subframe
matrices = []
owner = pose_bone if pose_bone is not None else ob
bake_chain = (owner,)
pose_baker = None
def visual_matrix(evaluated_ob, target_bone=None):
if target_bone is not None:
# Same native conversion as bpy_extras.anim_utils.pose_frame_info:
# capture the evaluated pose in this bone's local transform space,
# including Blender's parent/rest-pose and inheritance semantics.
evaluated_bone = evaluated_ob.pose.bones[target_bone.name]
return evaluated_ob.convert_space(
pose_bone=evaluated_bone, matrix=evaluated_bone.matrix,
from_space='POSE', to_space='LOCAL')
return evaluated_ob.matrix_world.copy()
def isolated_matrix(frame=None):
following = []
drivers = []
for target, endpoint in isolation_targets:
reason = self._isolation_error(target, endpoint)
if reason:
raise RuntimeError(reason)
stack = list(target.constraints)
suffix = stack[stack.index(endpoint) + 1:]
following.extend((target, other, other.mute) for other in suffix)
drivers.extend((curve, curve.mute)
for curve in self._isolation_drivers(target, suffix))
try:
# BKE_animsys_eval_driver skips FCURVE_MUTED. Suspend only this
# planned owners' suffix mute drivers before changing constraint mute;
# otherwise depsgraph evaluation can immediately undo isolation.
# Driver curves belong to the object, unlike potentially shared
# Action channelbags, so this does not mute another Action user.
for curve, muted in drivers:
if not muted:
curve.mute = True
for _target, other, muted in following:
if not muted:
other.mute = True
# Use the caller's actual view layer, including pinned Properties
# owners. RNA updates evaluate Blender's original constraint code;
# no transform/constraint solver is reproduced here.
view_layer.update()
evaluated_ob = ob.evaluated_get(view_layer.depsgraph)
for target, endpoint in isolation_targets:
evaluated_owner = (evaluated_ob.pose.bones[target.name]
if pose_bone is not None else evaluated_ob)
evaluated_constraint = evaluated_owner.constraints.get(endpoint.name)
if evaluated_constraint is None:
raise RuntimeError("The tracking constraint disappeared during evaluation")
reason = self._isolation_error(evaluated_owner, evaluated_constraint)
if reason is None and evaluated_constraint.type in {
'CAMERA_SOLVER', 'FOLLOW_TRACK', 'OBJECT_SOLVER'}:
reason = self._tracking_error(scene, evaluated_constraint)
if reason:
raise RuntimeError(reason)
# Keep checking the evaluated stack after owner-local Action/NLA
# isolation, including drivers that could not be suspended;
# never present a full-stack sample as a single-constraint bake.
if any(not (evaluated_ob.pose.bones[target.name] if pose_bone is not None
else evaluated_ob).constraints[other.name].mute
for target, other, _muted in following):
raise RuntimeError("A following constraint's animation or driver prevents bake isolation")
if pose_bone is None:
return visual_matrix(evaluated_ob)
if pose_baker is not None:
# Sample before restoring suffixes; anim_utils reads the same
# evaluated graph and writes only the planned pose channels.
pose_baker.send(frame)
return None
return {bone.name: visual_matrix(evaluated_ob, bone) for bone in bake_chain}
finally:
# Restore the original constraint values before re-enabling
# drivers, then evaluate with all original channels active. Nested
# cleanup also restores drivers if restoring a constraint raises.
try:
for _target, other, muted in following:
if other.mute != muted:
other.mute = muted
finally:
try:
for curve, muted in drivers:
if curve.mute != muted:
curve.mute = muted
finally:
view_layer.update()
def apply_matrix(matrix):
if pose_bone is not None:
pose_bone.matrix_basis = matrix
else:
ob.matrix_world = matrix
# Find constraint which would be converting
# TODO: several camera solvers and track followers would fail,
# but can't think about real work-flow where it'll be useful
for x in ob.constraints:
if x.type in {'CAMERA_SOLVER', 'FOLLOW_TRACK', 'OBJECT_SOLVER'}:
con = x
# A Properties panel supplies its exact (possibly pinned) constraint.
# Keep the desktop selection-based fallback for callers without that context.
if con is None:
for x in owner.constraints:
if x.type in {'CAMERA_SOLVER', 'FOLLOW_TRACK', 'OBJECT_SOLVER'}:
con = x
if not con:
self.report({'ERROR'}, "Motion Tracking constraint to be converted not found")
return {'CANCELLED'}
# Selection-based callers do not have the Properties constraint poll.
# Recheck here as well, before creating Actions or consuming any prefix.
reason = self._tracking_error(scene, con)
if reason:
self.report({'ERROR'}, reason)
return {'CANCELLED'}
bake_plan = self._pose_bake_plan(owner, con) if pose_bone is not None else None
bake_chain = tuple(bone for bone, _constraints in bake_plan) if bake_plan else (owner,)
isolation_targets = ([(bone, constraints[-1]) for bone, constraints in bake_plan
if constraints] if bake_plan else [(owner, con)])
@contextmanager
def isolated_actions():
# Nested copies restore in reverse order. All owners share the object
# Action binding, so each copy inherits earlier suffix exclusions;
# original/shared Actions and slots stay untouched on success/error.
with ExitStack() as cleanup:
for target, endpoint in isolation_targets:
cleanup.enter_context(self._isolated_actions(target, endpoint, view_layer))
yield
if view_layer is None:
self.report({'ERROR'}, "The constraint owner's view layer is required for baking")
return {'CANCELLED'}
reason = self._isolation_error(owner, con)
if reason is None and bake_plan is not None:
reason = self._pose_bake_error(ob, bake_plan)
if reason:
self.report({'ERROR'}, reason)
return {'CANCELLED'}
constraints = list(owner.constraints)
consumed = constraints[:constraints.index(con) + 1]
def consume_prefix():
# Removal is the final commit, after all sampling, key insertion and
# frame restoration succeed. Keep the context constraint alive until
# last; the native Properties caller clears its RNA context on return.
# Ancestor stacks were included in visual sampling. Commit them first
# and leave the context constraint alive until the final removal.
if bake_plan is not None:
for bone, constraints in reversed(bake_plan[1:]):
for other in constraints:
bone.constraints.remove(other)
for other in consumed:
owner.constraints.remove(other)
# Get clip used for parenting.
if con.use_active_clip:
clip = scene.active_clip
@@ -453,17 +928,43 @@ class CLIP_OT_constraint_to_fcurve(Operator):
return {'CANCELLED'}
if con.type == 'FOLLOW_TRACK' and con.use_3d_position:
mat = ob.matrix_world.copy()
ob.constraints.remove(con)
ob.matrix_world = mat
static_follow = con.type == 'FOLLOW_TRACK' and con.use_3d_position
if static_follow and len(consumed) == 1 and len(bake_chain) == 1:
try:
with isolated_actions():
mat = isolated_matrix()
except (RuntimeError, TypeError, ValueError) as error:
self.report({'ERROR'}, str(error))
return {'CANCELLED'}
# Assign before removal, so failure leaves the constraint available
# to the caller's Main rollback and scoped Properties context.
try:
apply_matrix(mat[pose_bone.name] if pose_bone is not None else mat)
consume_prefix()
except (RuntimeError, TypeError, ValueError) as error:
self.report({'ERROR'}, str(error))
return {'CANCELLED'}
return {'FINISHED'}
# Find start and end frames.
if con.type == 'CAMERA_SOLVER':
# Camera solver constraint is always referring to camera.
tracks = clip.tracking.tracks
if static_follow:
# A 3D track itself is static, but predecessors may animate via RNA,
# drivers or NLA. Consume their motion over the scene bake range, not
# a single current-frame matrix or an unrelated marker interval.
sfra, efra = scene.frame_start, scene.frame_end
tracks = ()
elif con.type in {'CAMERA_SOLVER', 'OBJECT_SOLVER'}:
tracking_object = (next(obj for obj in clip.tracking.objects if obj.is_camera)
if con.type == 'CAMERA_SOLVER'
else clip.tracking.objects[con.object])
# Solver evaluation consumes reconstruction cameras, not 2D markers.
# Markers can be removed or cover a different interval after solving.
# Sample all intervening frames through Blender's native interpolator;
# sparse reconstructed cameras do not imply missing-frame failures.
cameras = tracking_object.reconstruction.cameras
sfra, efra = cameras[0].frame, cameras[-1].frame
tracks = ()
elif con.object:
tracking_object = clip.tracking.objects.get(con.object, None)
if not tracking_object:
@@ -475,7 +976,14 @@ class CLIP_OT_constraint_to_fcurve(Operator):
else:
tracks = clip.tracking.tracks
if con.type == 'FOLLOW_TRACK' and not static_follow:
# The native solver reads only con.track. Other tracks must neither
# extend the bake interval nor make an empty selected track usable.
tracks = (tracks.get(con.track),)
for track in tracks:
if not track.markers:
continue
if sfra is None:
sfra = track.markers[0].frame
else:
@@ -487,44 +995,102 @@ class CLIP_OT_constraint_to_fcurve(Operator):
efra = max(efra, track.markers[-1].frame)
if sfra is None or efra is None:
self.report({'ERROR'}, "Movie clip has no tracking markers to bake")
return {'CANCELLED'}
# Store object matrices.
for x in range(sfra, efra + 1):
scene.frame_set(x)
matrices.append(ob.matrix_world.copy())
# Markers and reconstructed cameras use clip frames; F-Curves use scene frames.
# Inverse of the scene -> clip conversion used by the tracking operators.
if not static_follow:
sfra += clip.frame_start - 1
efra += clip.frame_start - 1
ob.animation_data_create()
# Restore the timeline even if an RNA/keyframe operation raises. The Web
# caller owns Main rollback for cancelled/error results.
try:
with self._bake_action(owner):
if pose_bone is not None:
from bpy_extras.anim_utils import BakeOptions, bake_action_iter
pose_baker = bake_action_iter(
ob, action=ob.animation_data.action,
bake_options=BakeOptions(
only_selected=False, do_pose=True, do_object=False,
do_visual_keying=True, do_constraint_clear=False,
do_parents_clear=False, do_clean=False,
do_location=True, do_rotation=True, do_scale=True,
do_bbone=False, do_custom_props=False),
pose_bone_names=tuple(bone.name for bone in bake_chain),
depsgraph=view_layer.depsgraph)
pose_baker.send(None)
# Apply matrices on object and insert key-frames.
i = 0
for x in range(sfra, efra + 1):
scene.frame_set(x)
ob.matrix_world = matrices[i]
# Sampling finishes before key insertion. Action/NLA isolation
# and per-frame suffix/driver isolation stay shared with the
# object path, including restoration on any generator failure.
with isolated_actions():
for x in range(sfra, efra + 1):
scene.frame_set(x)
matrix = isolated_matrix(x)
if pose_baker is None:
matrices.append(matrix)
ob.keyframe_insert("location")
if pose_baker is not None:
if pose_baker.send(None) is None:
raise RuntimeError("Could not create the baked pose Action")
else:
rotation_previous = None
rotation_path = ('rotation_quaternion' if owner.rotation_mode == 'QUATERNION'
else 'rotation_axis_angle' if owner.rotation_mode == 'AXIS_ANGLE'
else 'rotation_euler')
for x, matrix in zip(range(sfra, efra + 1), matrices):
scene.frame_set(x)
apply_matrix(matrix)
if rotation_path in {'rotation_quaternion', 'rotation_euler'}:
rotation = getattr(owner, rotation_path).copy()
if rotation_previous is not None:
rotation.make_compatible(rotation_previous)
setattr(owner, rotation_path, rotation)
rotation_previous = rotation
for path in ('location', rotation_path, 'scale'):
if not owner.keyframe_insert(path, frame=x, group="Object Transforms"):
raise RuntimeError("Could not insert baked {:s} keyframes".format(path))
if ob.rotation_mode == 'QUATERNION':
ob.keyframe_insert("rotation_quaternion")
else:
ob.keyframe_insert("rotation_euler")
i += 1
ob.constraints.remove(con)
scene.frame_set(frame_current)
except (RuntimeError, TypeError, ValueError) as error:
self.report({'ERROR'}, str(error))
return {'CANCELLED'}
finally:
try:
if pose_baker is not None:
pose_baker.close()
finally:
scene.frame_set(frame_current, subframe=subframe_current)
# Keep the context constraint alive while frame restoration invokes updates.
try:
consume_prefix()
except (RuntimeError, TypeError, ValueError) as error:
self.report({'ERROR'}, str(error))
return {'CANCELLED'}
return {'FINISHED'}
def execute(self, context):
scene = context.scene
# XXX, should probably use context.selected_editable_objects
# since selected objects can be from a lib or in hidden layer!
for ob in scene.objects:
if ob.select_get():
self._bake_object(scene, ob)
con = getattr(context, "constraint", None)
if con is not None:
if not self.poll(context):
self.report({'ERROR'}, "Tracking constraint context or resource is unavailable")
return {'CANCELLED'}
return self._bake_object(scene, context.object, con, getattr(context, "pose_bone", None),
context.view_layer)
# Preserve the desktop selection entry, while propagating failures instead
# of reporting success after a missing clip or empty tracking resource.
objects = [ob for ob in scene.objects if ob.select_get()]
if not objects or any(not ob.is_editable for ob in objects):
self.report({'ERROR'}, "Select editable objects with tracking constraints")
return {'CANCELLED'}
for ob in objects:
result = self._bake_object(scene, ob, view_layer=context.view_layer)
if result != {'FINISHED'}:
return result
return {'FINISHED'}

View File

@@ -286,9 +286,7 @@ class ExecutePreset(Operator):
from os.path import basename, splitext
filepath = self.filepath
# change the menu title to the most recently chosen option
preset_class = getattr(bpy.types, self.menu_idname)
preset_class.bl_label = bpy.path.display_name(basename(filepath), title_case=False)
ext = splitext(filepath)[1].lower()
@@ -303,6 +301,7 @@ class ExecutePreset(Operator):
bpy.utils.execfile(filepath)
except Exception as ex:
self.report({'ERROR'}, rpt_("Failed to execute the preset: {:s}").format(repr(ex)))
return {'CANCELLED'}
elif ext == ".xml":
import _rna_xml as rna_xml
@@ -313,6 +312,9 @@ class ExecutePreset(Operator):
_call_preset_cb(getattr(preset_class, "post_cb", None), context, filepath)
# Python class state is outside Main's failure rollback.
# Publish the chosen title only after the preset has executed successfully.
preset_class.bl_label = bpy.path.display_name(basename(filepath), title_case=False)
return {'FINISHED'}

View File

@@ -493,6 +493,11 @@ class ToolSelectPanelHelper:
@classmethod
def _km_action_simple(cls, kc_default, kc, context_descr, label, keymap_fn):
if (startup := bpy.utils._web_startup_state) is not None:
# Includes dynamic ToolDefs first materialized during startup. Keep
# their callable until the native keymap transaction is published.
startup.track_tool_helper(cls)
startup.track_tool_container(keymap_fn)
km_idname = "{:s} {:s}, {:s}".format(cls.keymap_prefix, context_descr, label)
km = kc.keymaps.get(km_idname)
km_kwargs = dict(space_type=cls.bl_space_type, region_type='WINDOW', tool=True)
@@ -516,6 +521,8 @@ class ToolSelectPanelHelper:
@classmethod
def register(cls):
if (startup := bpy.utils._web_startup_state) is not None:
startup.track_tool_helper(cls)
wm = bpy.context.window_manager
# Write into defaults, users may modify in preferences.
kc_default = wm.keyconfigs.default
@@ -1038,12 +1045,18 @@ def _activate_by_item(context, space_type, item, index, *, as_fallback=False):
idname_fallback = (item_fallback and item_fallback.idname) or ""
keymap_fallback = (item_fallback and item_fallback.keymap and item_fallback.keymap[0]) or ""
# `from_dict` stores a tuple key-map as `[builder_callable]` and only swaps in
# the real key-map name once the tool's key-map is registered. A workspace
# entered before that registration still holds the callable here, so only
# extend a real name instead of raising on `callable + str`.
if keymap_fallback and not isinstance(keymap_fallback, str):
keymap_fallback = ""
if keymap_fallback:
keymap_fallback = keymap_fallback + " (fallback)"
tool.setup(
idname=item.idname,
keymap=item.keymap[0] if item.keymap is not None else "",
keymap=item.keymap[0] if item.keymap is not None and isinstance(item.keymap[0], str) else "",
cursor=item.cursor or 'DEFAULT',
options=item.options or set(),
gizmo_group=gizmo_group,