@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.15",
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.15"
14
+ "@deepblend/dsh-blender-contracts": "0.2.16"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -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
- for keyframe in keyframes:
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({"id": track.get("id"), "targetKind": kind, "targetEntityId": target_id, "action": action.name})
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)