@deepblend/dsh-blender-contracts 0.2.14 → 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/lib/scene-spec.js CHANGED
@@ -96,6 +96,8 @@ export const ANIMATION_PROPERTIES_BY_KIND = Object.freeze({
96
96
  export function collectionNameForKind(kind) {
97
97
  if (kind === 'camera') return 'cameras'
98
98
  if (kind === 'material') return 'materials'
99
+ // A BONE TRACK RESOLVES AGAINST THE ARMATURES, because that is the collection its id belongs to.
100
+ if (kind === 'bone') return 'armatures'
99
101
  return 'entities'
100
102
  }
101
103
 
@@ -185,6 +187,10 @@ export function validateSceneSpec(spec) {
185
187
  // ---- uniqueness ---------------------------------------------------------
186
188
  const collections = [
187
189
  ['assets', document.assets ?? []],
190
+ // ARMATURES ARE A COLLECTION LIKE ANY OTHER: they have ids, they can collide, and other things
191
+ // reference them by id. Leaving them out of this list meant the cross-reference check had nothing to
192
+ // look in, so a skinned entity was reported as naming a rig that does not exist — its own rig.
193
+ ['armatures', document.armatures ?? []],
188
194
  ['materials', document.materials ?? []],
189
195
  ['entities', document.entities],
190
196
  ['lights', document.lights ?? []],
@@ -279,6 +285,11 @@ export function validateSceneSpec(spec) {
279
285
  })
280
286
  }
281
287
  requiresId('materials', entity.materialId, `${at}.materialId`, 'material')
288
+ // AND THE RIG IT IS SKINNED TO. MEASURED: this check was missing at first, and the contract suite
289
+ // caught it — the schema can say `armatureId` is a string, but only a cross-reference can say it
290
+ // names something the scene declares. An entity skinned to an armature nobody declares would render
291
+ // as a static object while the file says it is rigged, which is the failure this code exists for.
292
+ requiresId('armatures', entity.armatureId, `${at}.armatureId`, 'armature')
282
293
  if (entity.type !== 'empty' && entity.materialId === undefined) {
283
294
  notices.push({
284
295
  severity: 'notice', code: 'SCENE_ENTITY_MATERIAL_DEFAULTED', path: at,
@@ -351,6 +362,25 @@ export function validateSceneSpec(spec) {
351
362
  // is what every track written before kinds existed meant.
352
363
  const kind = track.targetKind ?? 'entity'
353
364
  requiresId(collectionNameForKind(kind), track.targetEntityId, `${at}.targetEntityId`, kind)
365
+ // AND THE BONE INSIDE IT. MEASURED: this is the same shape as `armatureId` last round — the schema
366
+ // can say `boneName` is a string, and only a cross-reference can say the armature HAS that bone.
367
+ // A track pointing at a bone nobody declares animates nothing while the file says the rig moves.
368
+ if (kind === 'bone') {
369
+ if (typeof track.boneName !== 'string' || track.boneName.length === 0) {
370
+ errors.push({
371
+ severity: 'error', code: 'SCENE_REFERENCE_MISSING', path: `${at}.boneName`,
372
+ message: 'a bone track must name the bone it drives',
373
+ })
374
+ } else {
375
+ const armature = (document.armatures ?? []).find(entry => entry?.id === track.targetEntityId)
376
+ if (armature !== undefined && !(armature.bones ?? []).some(bone => bone?.name === track.boneName)) {
377
+ errors.push({
378
+ severity: 'error', code: 'SCENE_REFERENCE_MISSING', path: `${at}.boneName`,
379
+ message: `bone "${track.boneName}" does not exist in armature "${track.targetEntityId}"`,
380
+ })
381
+ }
382
+ }
383
+ }
354
384
 
355
385
  // A property that does not belong to the target's kind is not a typo the
356
386
  // compiler can shrug off: `emissionStrength` on an entity, or `location.x` on a
@@ -31,7 +31,7 @@
31
31
  "items": {
32
32
  "$ref": "#/$defs/armature"
33
33
  },
34
- "description": "Armatures this scene declares (SPEC \u00a720 M6, character animation). Each is one skeleton, created as its own object and named `<prefix><id>` like every other product object. WHAT THIS SLICE DOES NOT DO YET: bind a mesh to a bone (skinning) and animate a bone \u2014 those are the next two slices, and a rig that exists but drives nothing is honest about being exactly that."
34
+ "description": "Armatures this scene declares (SPEC \u00a720 M6, character animation). Each is one skeleton, created as its own object and named `<prefix><id>` like every other product object. Entities are skinned to an armature (`entity.armatureId`, with Blender's automatic weights) and animation tracks can drive a bone of it (`targetKind: \"bone\"` with `boneName`), so a rig both moves and moves its mesh."
35
35
  },
36
36
  "materials": {
37
37
  "type": "array",
@@ -473,6 +473,10 @@
473
473
  "type": "string",
474
474
  "description": "Required when type is asset-instance."
475
475
  },
476
+ "armatureId": {
477
+ "type": "string",
478
+ "description": "The armature this entity is skinned to, named by `armatures[].id`. The compiler binds the mesh with Blender's own automatic weights, so the entity follows the bones with no weight table to write by hand. A name the scene does not declare is refused rather than ignored, because an entity that is silently unskinned renders as a static object while the file says it is rigged."
479
+ },
476
480
  "generator": {
477
481
  "$ref": "#/$defs/generator"
478
482
  },
@@ -673,15 +677,21 @@
673
677
  "enum": [
674
678
  "entity",
675
679
  "camera",
676
- "material"
680
+ "material",
681
+ "bone"
677
682
  ],
678
683
  "default": "entity",
679
- "description": "What `targetEntityId` names: an entity (default), a camera, or a material. Only entity tracks existed before, which is why \"the camera orbits the product\" and \"the dial lights up\" both had to be faked with entity transforms."
684
+ "description": "What `targetEntityId` names: an entity (default), a camera, a material, or AN ARMATURE whose bone this track drives (with `boneName`). Ids are unique per collection, so the kind is the only thing that says whether \"watch-dial\" means the mesh or the material \u2014 and `bone` is what made a rig able to move rather than merely exist."
680
685
  },
681
686
  "targetEntityId": {
682
687
  "type": "string",
683
688
  "description": "The id of the target named by `targetKind`. The field name says Entity because every track written before target kinds existed used it and revisions are immutable; renaming it would make r0001-r0023 unreadable. Resolution is against the collection `targetKind` names, which matters because ids are unique per collection, not globally."
684
689
  },
690
+ "boneName": {
691
+ "type": "string",
692
+ "maxLength": 64,
693
+ "description": "Required when targetKind is `bone`: the name of the bone inside the named armature. A name the armature does not have is refused \u2014 a track that silently animates nothing is a scene that claims to move and does not."
694
+ },
685
695
  "property": {
686
696
  "type": "string",
687
697
  "enum": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-contracts",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "description": "DeepBlend Studio Service Definition, protocol contracts, and stable error codes.",
6
6
  "main": "lib/index.js",