@deepblend/dsh-blender-provider-local 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-provider-local",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local Blender Provider: the BlenderRuntime Service over ctx.subprocess (batch execution).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@deepblend/dsh-blender-contracts": "0.2.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.2.16"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
@@ -1030,6 +1030,82 @@ def _apply_interpolation(action, data_path, axis, keyframes, track, guard):
|
|
|
1030
1030
|
)
|
|
1031
1031
|
|
|
1032
1032
|
|
|
1033
|
+
def _insert_keyframes(target, bpy_attr, axis, keyframes):
|
|
1034
|
+
"""Insert one component's keyframes on a target that has `keyframe_insert`.
|
|
1035
|
+
|
|
1036
|
+
EXTRACTED RATHER THAN COPIED: an object and a pose bone are keyframed by the same three lines, and
|
|
1037
|
+
a second copy is the one that would miss the next fix — the identity-first rule below is exactly the
|
|
1038
|
+
kind of thing that gets learned once and applied in one place.
|
|
1039
|
+
"""
|
|
1040
|
+
for keyframe in keyframes:
|
|
1041
|
+
frame = int(keyframe["frame"])
|
|
1042
|
+
value = float(keyframe["value"])
|
|
1043
|
+
setattr(target, bpy_attr, _with_component(getattr(target, bpy_attr), axis, value))
|
|
1044
|
+
target.keyframe_insert(data_path=bpy_attr, index=axis, frame=frame)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def build_bone_animation(armature_obj, track, guard):
|
|
1048
|
+
"""Animate ONE pose bone of an armature.
|
|
1049
|
+
|
|
1050
|
+
WHY A BONE IS NOT AN OBJECT: the values live on the ARMATURE's pose, so the keyframes are inserted
|
|
1051
|
+
through the pose bone while the action belongs to the armature. Everything else — the property
|
|
1052
|
+
vocabulary, the two-keyframe minimum, the interpolation — is the same, which is why the insertion
|
|
1053
|
+
itself is shared rather than re-implemented.
|
|
1054
|
+
|
|
1055
|
+
The bone is named by the track; a name the armature does not have is refused, because a track that
|
|
1056
|
+
silently animates nothing is a scene that claims to move and does not.
|
|
1057
|
+
"""
|
|
1058
|
+
bone_name = track.get("boneName")
|
|
1059
|
+
if not isinstance(bone_name, str) or bone_name == "":
|
|
1060
|
+
raise ActionError(
|
|
1061
|
+
"SCENE_VALIDATION_FAILED",
|
|
1062
|
+
'animation track "%s" targets a bone but names none' % (track.get("id"),),
|
|
1063
|
+
)
|
|
1064
|
+
pose_bone = armature_obj.pose.bones.get(bone_name)
|
|
1065
|
+
if pose_bone is None:
|
|
1066
|
+
raise ActionError(
|
|
1067
|
+
"SCENE_VALIDATION_FAILED",
|
|
1068
|
+
'animation track "%s" targets bone "%s", which armature "%s" does not have'
|
|
1069
|
+
% (track.get("id"), bone_name, armature_obj.get("deepblend_id")),
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
property_name = track.get("property", "")
|
|
1073
|
+
if "." not in property_name:
|
|
1074
|
+
raise ActionError(
|
|
1075
|
+
"SCENE_VALIDATION_FAILED",
|
|
1076
|
+
'animation track "%s" names property "%s" without a component' % (track.get("id"), property_name),
|
|
1077
|
+
)
|
|
1078
|
+
attr, _, component = property_name.rpartition(".")
|
|
1079
|
+
bpy_attr = TRACK_PROPERTY_ATTR.get(attr)
|
|
1080
|
+
if bpy_attr is None or component not in ("x", "y", "z"):
|
|
1081
|
+
raise ActionError(
|
|
1082
|
+
"SCENE_VALIDATION_FAILED",
|
|
1083
|
+
'animation track "%s" names unsupported property "%s"' % (track.get("id"), property_name),
|
|
1084
|
+
{"supported": sorted("%s.%s" % (name, axis) for name in TRACK_PROPERTY_ATTR for axis in "xyz")},
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
keyframes = track.get("keyframes") or []
|
|
1088
|
+
if len(keyframes) < 2:
|
|
1089
|
+
raise ActionError(
|
|
1090
|
+
"SCENE_VALIDATION_FAILED",
|
|
1091
|
+
'animation track "%s" has %d keyframe(s); at least 2 are required to describe motion'
|
|
1092
|
+
% (track.get("id"), len(keyframes)),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
axis = "xyz".index(component)
|
|
1096
|
+
pose_bone.rotation_mode = "XYZ"
|
|
1097
|
+
_insert_keyframes(pose_bone, bpy_attr, axis, keyframes)
|
|
1098
|
+
|
|
1099
|
+
action = armature_obj.animation_data.action if armature_obj.animation_data is not None else None
|
|
1100
|
+
if action is None:
|
|
1101
|
+
raise ActionError(
|
|
1102
|
+
"BLENDER_SCRIPT_ERROR",
|
|
1103
|
+
'animation track "%s" inserted keyframes but produced no action' % (track.get("id"),),
|
|
1104
|
+
)
|
|
1105
|
+
_apply_interpolation(action, bpy_attr, axis, keyframes, track, guard)
|
|
1106
|
+
return action
|
|
1107
|
+
|
|
1108
|
+
|
|
1033
1109
|
def build_animation(obj, track, guard):
|
|
1034
1110
|
"""Key one animation track onto an object, returning the action name.
|
|
1035
1111
|
|
|
@@ -1072,11 +1148,7 @@ def build_animation(obj, track, guard):
|
|
|
1072
1148
|
# values, or the first insert would capture a value from a different property.
|
|
1073
1149
|
obj.rotation_mode = "XYZ"
|
|
1074
1150
|
|
|
1075
|
-
|
|
1076
|
-
frame = int(keyframe["frame"])
|
|
1077
|
-
value = float(keyframe["value"])
|
|
1078
|
-
setattr(obj, bpy_attr, _with_component(getattr(obj, bpy_attr), axis, value))
|
|
1079
|
-
obj.keyframe_insert(data_path=bpy_attr, index=axis, frame=frame)
|
|
1151
|
+
_insert_keyframes(obj, bpy_attr, axis, keyframes)
|
|
1080
1152
|
|
|
1081
1153
|
action = obj.animation_data.action if obj.animation_data is not None else None
|
|
1082
1154
|
if action is None:
|
|
@@ -1560,6 +1632,10 @@ def build_scene(spec, options, guard):
|
|
|
1560
1632
|
target = cameras.get(target_id)
|
|
1561
1633
|
elif kind == "material":
|
|
1562
1634
|
target = materials.get(target_id)
|
|
1635
|
+
elif kind == "bone":
|
|
1636
|
+
# A BONE TRACK TARGETS THE ARMATURE, because the bones belong to it: the armature id is
|
|
1637
|
+
# what resolves, and `boneName` picks the bone inside it.
|
|
1638
|
+
target = armature_objects.get(target_id)
|
|
1563
1639
|
else:
|
|
1564
1640
|
target = entity_objects.get(target_id)
|
|
1565
1641
|
if target is None:
|
|
@@ -1570,9 +1646,23 @@ def build_scene(spec, options, guard):
|
|
|
1570
1646
|
)
|
|
1571
1647
|
if kind == "material":
|
|
1572
1648
|
action = build_material_animation(target, track, guard)
|
|
1649
|
+
elif kind == "bone":
|
|
1650
|
+
action = build_bone_animation(target, track, guard)
|
|
1573
1651
|
else:
|
|
1574
1652
|
action = build_animation(target, track, guard)
|
|
1575
|
-
actions.append({
|
|
1653
|
+
actions.append({
|
|
1654
|
+
"id": track.get("id"),
|
|
1655
|
+
"targetKind": kind,
|
|
1656
|
+
"targetEntityId": target_id,
|
|
1657
|
+
"action": action.name,
|
|
1658
|
+
**({"boneName": track.get("boneName")} if kind == "bone" else {}),
|
|
1659
|
+
# WHAT THE ACTION ACTUALLY DROVE, read off its own fcurves. MEASURED, and a surviving
|
|
1660
|
+
# mutation is why: keying the ARMATURE OBJECT instead of the pose bone still moves the mesh
|
|
1661
|
+
# — the mesh is parented to the armature, so the two frames differ either way — and the
|
|
1662
|
+
# two-frame proof passed. The data paths are the difference: `pose.bones["chest"]…` is the
|
|
1663
|
+
# bone, `rotation_euler` on the object is not.
|
|
1664
|
+
"dataPaths": sorted({curve.data_path for curve in action_fcurves(action)}),
|
|
1665
|
+
})
|
|
1576
1666
|
|
|
1577
1667
|
report_progress("configure_render", 85)
|
|
1578
1668
|
render_config = configure_scene(scene, spec, profile, guard)
|