@deepblend/dsh-blender-provider-local 0.2.14 → 0.2.15

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.14",
3
+ "version": "0.2.15",
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"
14
+ "@deepblend/dsh-blender-contracts": "0.2.15"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -230,6 +230,17 @@ def action_compile_scene(request, options):
230
230
  # THE RIGS THIS SCENE DECLARES, with their bones. A count alone would say an armature exists;
231
231
  # a rig is only useful if the bones are the ones the spec named, so the names travel with it and
232
232
  # a test can read them back rather than trust the count.
233
+ # WHICH ENTITIES ARE SKINNED, AND TO WHAT. The vertex groups are the evidence that weights
234
+ # exist at all: an entity bound to an armature with no groups renders as a static object while
235
+ # the file says it is rigged, and the reading is where that difference becomes visible.
236
+ "skinned": [
237
+ {
238
+ "entityId": entity_id,
239
+ "armatureId": entry["armatureId"],
240
+ "vertexGroups": entry["vertexGroups"],
241
+ }
242
+ for entity_id, entry in sorted((report.get("skinned") or {}).items())
243
+ ],
233
244
  "armatures": [
234
245
  {
235
246
  "id": armature_id,
@@ -1375,6 +1375,45 @@ def build_armature(entry, guard):
1375
1375
  return obj
1376
1376
 
1377
1377
 
1378
+ def skin_entity(obj, armature_obj, armature_id, guard):
1379
+ """Bind a mesh to an armature with Blender's own automatic weights.
1380
+
1381
+ WHY AUTOMATIC WEIGHTS RATHER THAN A WEIGHT TABLE. A per-vertex weight table in the SceneSpec would
1382
+ be a second, hand-written copy of something Blender already computes from the geometry — and the
1383
+ copy would be the one that goes stale, because nothing regenerates it when a generator's parameters
1384
+ change. What the spec states is the RELATIONSHIP ("this entity is skinned to that armature"); the
1385
+ weights are Blender's answer to it.
1386
+
1387
+ Bone heat can fail on a degenerate mesh (Blender reports "failed to find solution for one or more
1388
+ bones"), and that is a WARNING rather than a refusal: the binding still exists and the bones that
1389
+ did solve still deform, so the honest outcome is a scene that mostly works with the failure named.
1390
+ """
1391
+ for target in bpy.context.selected_objects:
1392
+ target.select_set(False)
1393
+ obj.select_set(True)
1394
+ bpy.context.view_layer.objects.active = armature_obj
1395
+ try:
1396
+ bpy.ops.object.parent_set(type="ARMATURE_AUTO")
1397
+ except Exception as exc:
1398
+ guard.warnings.append({
1399
+ "code": "SKIN_WEIGHTS_FAILED",
1400
+ "message": 'entity "%s" could not be skinned to armature "%s": %s'
1401
+ % (obj.get("deepblend_id"), armature_id, error_text(exc)),
1402
+ })
1403
+ return False
1404
+ finally:
1405
+ obj.select_set(False)
1406
+
1407
+ if len(obj.vertex_groups) == 0:
1408
+ guard.warnings.append({
1409
+ "code": "SKIN_WEIGHTS_EMPTY",
1410
+ "message": 'entity "%s" is bound to armature "%s" but Blender produced no vertex groups, so '
1411
+ "it will not deform" % (obj.get("deepblend_id"), armature_id),
1412
+ })
1413
+ return False
1414
+ return True
1415
+
1416
+
1378
1417
  def build_scene(spec, options, guard):
1379
1418
  """Compile a SceneSpec into a live Blender scene.
1380
1419
 
@@ -1550,6 +1589,25 @@ def build_scene(spec, options, guard):
1550
1589
  bpy.context.view_layer.update()
1551
1590
 
1552
1591
  report_progress("compile_done", 90)
1592
+ # SKINNING LAST: it needs both the mesh and the rig, and doing it before either exists would mean
1593
+ # ordering the compile around a step that is about the RELATIONSHIP between them.
1594
+ report_progress("skin_entities", 60)
1595
+ skinned_entities = {}
1596
+ for entity in spec.get("entities") or []:
1597
+ armature_id = entity.get("armatureId")
1598
+ if armature_id is None:
1599
+ continue
1600
+ obj = entity_objects.get(entity["id"])
1601
+ armature_obj = armature_objects.get(armature_id)
1602
+ if obj is None or armature_obj is None:
1603
+ raise ActionError(
1604
+ "SCENE_SPEC_INVALID",
1605
+ 'entity "%s" is skinned to armature "%s", which the same SceneSpec does not declare'
1606
+ % (entity["id"], armature_id),
1607
+ )
1608
+ skin_entity(obj, armature_obj, armature_id, guard)
1609
+ skinned_entities[entity["id"]] = entity
1610
+
1553
1611
  return {
1554
1612
  "objects": describe_objects(),
1555
1613
  "actions": actions,
@@ -1560,6 +1618,16 @@ def build_scene(spec, options, guard):
1560
1618
  "profileName": profile_name,
1561
1619
  "entityObjectNames": {key: value.name for key, value in entity_objects.items()},
1562
1620
  "armatureNames": {key: value.name for key, value in armature_objects.items()},
1621
+ "skinned": {
1622
+ key: {
1623
+ "armatureId": entry["armatureId"],
1624
+ # THE GROUPS COME FROM THE OBJECT, not from the spec entry: what the file asked for is
1625
+ # the relationship, and what Blender produced is the weights. Reading the latter back is
1626
+ # the whole point — an entity bound with no groups renders as a static object.
1627
+ "vertexGroups": sorted(group.name for group in entity_objects[key].vertex_groups),
1628
+ }
1629
+ for key, entry in skinned_entities.items()
1630
+ },
1563
1631
  "cameraNames": {key: value.name for key, value in cameras.items()},
1564
1632
  }
1565
1633