@forgeax/engine-skinning 0.1.2

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.
Files changed (36) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +24 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/binding.unit.test.d.ts +2 -0
  5. package/dist/__tests__/binding.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  7. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/plugin.integration.test.d.ts +2 -0
  9. package/dist/__tests__/plugin.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/skin-error-code-owner.test-d.d.ts +27 -0
  11. package/dist/__tests__/skin-error-code-owner.test-d.d.ts.map +1 -0
  12. package/dist/assets/skin-decoder.d.ts +4 -0
  13. package/dist/assets/skin-decoder.d.ts.map +1 -0
  14. package/dist/errors.d.ts +232 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.mjs +375 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/plugin.d.ts +4 -0
  21. package/dist/plugin.d.ts.map +1 -0
  22. package/dist/resolve-skin-joints.d.ts +10 -0
  23. package/dist/resolve-skin-joints.d.ts.map +1 -0
  24. package/dist/skin.d.ts +5 -0
  25. package/dist/skin.d.ts.map +1 -0
  26. package/package.json +57 -0
  27. package/src/__tests__/binding.unit.test.ts +44 -0
  28. package/src/__tests__/errors.unit.test.ts +13 -0
  29. package/src/__tests__/plugin.integration.test.ts +23 -0
  30. package/src/__tests__/skin-error-code-owner.test-d.ts +91 -0
  31. package/src/assets/skin-decoder.ts +69 -0
  32. package/src/errors.ts +356 -0
  33. package/src/index.ts +15 -0
  34. package/src/plugin.ts +25 -0
  35. package/src/resolve-skin-joints.ts +24 -0
  36. package/src/skin.ts +68 -0
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@forgeax/engine-plugin';
2
+ /** Install skeletal binding components in a World that consumes skinned scenes. */
3
+ export declare function skinningPlugin(): Plugin;
4
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAcrD,mFAAmF;AACnF,wBAAgB,cAAc,IAAI,MAAM,CAQvC"}
@@ -0,0 +1,10 @@
1
+ import type { EntityHandle } from '@forgeax/engine-ecs';
2
+ import { type SkinError } from './errors.js';
3
+ export declare function resolveSkinJoints(jointPaths: readonly string[], names: ReadonlyMap<string, EntityHandle>, skinEntity: EntityHandle): {
4
+ ok: true;
5
+ value: Uint32Array;
6
+ } | {
7
+ ok: false;
8
+ error: SkinError;
9
+ };
10
+ //# sourceMappingURL=resolve-skin-joints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-skin-joints.d.ts","sourceRoot":"","sources":["../src/resolve-skin-joints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,KAAK,SAAS,EAAgC,MAAM,aAAa,CAAC;AAE3E,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,EACxC,UAAU,EAAE,YAAY,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAgBpE"}
package/dist/skin.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare const Skin: import("@forgeax/engine-ecs").Component<"Skin", {
2
+ readonly skeleton: "shared<SkeletonAsset>";
3
+ readonly joints: "array<entity>";
4
+ }>;
5
+ //# sourceMappingURL=skin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin.d.ts","sourceRoot":"","sources":["../src/skin.ts"],"names":[],"mappings":"AA8DA,eAAO,MAAM,IAAI;;;EAKf,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@forgeax/engine-skinning",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "sideEffects": false,
8
+ "description": "Optional skin and joint binding domain.",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "dependencies": {
25
+ "@forgeax/engine-ecs": "0.1.2",
26
+ "@forgeax/engine-plugin": "0.1.2"
27
+ },
28
+ "forgeax": {
29
+ "metrics": {
30
+ "bundle-size": {
31
+ "enabled": true,
32
+ "path": "dist/index.mjs",
33
+ "compression": "gzip"
34
+ },
35
+ "fps": {
36
+ "enabled": false,
37
+ "reason": "headless binding package"
38
+ },
39
+ "bench": {
40
+ "enabled": false,
41
+ "reason": "binding is validated by unit tests"
42
+ },
43
+ "gate": {
44
+ "enabled": false,
45
+ "reason": "no package binary gate"
46
+ },
47
+ "spike-report": {
48
+ "enabled": false,
49
+ "reason": "not a spike package"
50
+ }
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "test": "vitest run"
56
+ }
57
+ }
@@ -0,0 +1,44 @@
1
+ import { componentSchema } from '@forgeax/engine-ecs/internal';
2
+ import { resolveSkinJoints, Skin, SkinJointPathUnresolvedError } from '@forgeax/engine-skinning';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { skinContribution } from '../assets/skin-decoder';
5
+
6
+ describe('skinning binding contract', () => {
7
+ it('exposes the skin owner contribution for the shared decoder map', () => {
8
+ expect(skinContribution.kind.kind).toBe('skin');
9
+ expect(skinContribution.consumer).toBe('resolveSkinJoints');
10
+ });
11
+ it('exposes Skin as the optional binding component', () => {
12
+ expect(Skin.name).toBe('Skin');
13
+ expect(componentSchema(Skin).skeleton).toBe('shared<SkeletonAsset>');
14
+ expect(componentSchema(Skin).joints).toBe('array<entity>');
15
+ });
16
+
17
+ it('returns the canonical error and retries with only the Name map repaired', () => {
18
+ const jointPaths = ['Root/Arm', 'Root/Missing', 'Root/Hand'];
19
+ const names = new Map([
20
+ ['Arm', 4 as never],
21
+ ['Root', 5 as never],
22
+ ]);
23
+ const result = resolveSkinJoints(jointPaths, names, 7 as never);
24
+ expect(result.ok).toBe(false);
25
+ if (result.ok) return;
26
+ const expected = new SkinJointPathUnresolvedError(7, ['Root', 'Missing'], 1);
27
+ expect(result.error).toBeInstanceOf(SkinJointPathUnresolvedError);
28
+ expect(result.error.code).toBe('skin-joint-path-unresolved');
29
+ expect(result.error.expected).toBe(expected.expected);
30
+ expect(result.error.hint).toBe(expected.hint);
31
+ expect(result.error.detail).toEqual(expected.detail);
32
+
33
+ names.set('Missing', 6 as never);
34
+ names.set('Hand', 8 as never);
35
+ const retry = resolveSkinJoints(jointPaths, names, 7 as never);
36
+ expect(retry).toEqual({ ok: true, value: new Uint32Array([4, 6, 8]) });
37
+ });
38
+
39
+ it('returns a concrete joint payload for the render consumer', () => {
40
+ const result = resolveSkinJoints(['Root/Arm'], new Map([['Arm', 4 as never]]), 7 as never);
41
+
42
+ expect(result).toEqual({ ok: true, value: new Uint32Array([4]) });
43
+ });
44
+ });
@@ -0,0 +1,13 @@
1
+ import { type SkinError, SkinJointPathUnresolvedError } from '@forgeax/engine-skinning';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('skinning error boundary', () => {
5
+ it('keeps binding errors structured and exhaustive', () => {
6
+ const error: SkinError = new SkinJointPathUnresolvedError(2, ['Root', 'Arm'], 1);
7
+ expect(error).toBeInstanceOf(SkinJointPathUnresolvedError);
8
+ expect(error.code).toBe('skin-joint-path-unresolved');
9
+ expect(error.expected).toBe('joint entity with Name="Arm" exists in the world');
10
+ expect(error.detail.failedAtIndex).toBe(1);
11
+ expect(error.hint.length).toBeGreaterThan(0);
12
+ });
13
+ });
@@ -0,0 +1,23 @@
1
+ import { createWorldContext, World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Skin, skinningPlugin } from '../index';
4
+
5
+ describe('skinningPlugin', () => {
6
+ it('installs only the optional Skin component vocabulary', async () => {
7
+ const world = new World();
8
+ const context = await createWorldContext(world, [skinningPlugin()]);
9
+
10
+ expect([...world.components.entries()].map(([name]) => name)).toEqual(['Skin']);
11
+ const skeleton = world.allocSharedRef('SkeletonAsset', {
12
+ kind: 'skeleton',
13
+ jointCount: 0,
14
+ inverseBindMatrices: new Float32Array(),
15
+ });
16
+ const entity = world.spawn({ component: Skin, data: { skeleton, joints: [] } }).unwrap();
17
+ expect(world.get(entity, Skin).ok).toBe(true);
18
+
19
+ world.despawn(entity).unwrap();
20
+ await context.fiber.dispose();
21
+ expect(world.components.entries()).toHaveLength(0);
22
+ });
23
+ });
@@ -0,0 +1,91 @@
1
+ import type { resolveSkinJoints } from '@forgeax/engine-skinning';
2
+ import type { SkinError, SkinErrorCode, SkinExtractErrorCode } from '../errors.js';
3
+
4
+ type Equal<Left, Right> =
5
+ (<Type>() => Type extends Left ? 1 : 2) extends <Type>() => Type extends Right ? 1 : 2
6
+ ? true
7
+ : false;
8
+
9
+ type Assert<Value extends true> = Value;
10
+
11
+ type ExpectedSkinErrorCodes =
12
+ | 'skin-joint-count-exceeded'
13
+ | 'skin-joint-despawned'
14
+ | 'skin-joint-path-unresolved'
15
+ | 'skin-instances-coexist-forbidden'
16
+ | 'skeleton-resolve-failed'
17
+ | 'joint-count-mismatch'
18
+ | 'joint-entity-dangling';
19
+
20
+ type _SkinErrorCodeDerivesFromSkinError = Assert<Equal<SkinErrorCode, SkinError['code']>>;
21
+
22
+ type _SkinErrorCodeHasExactMembership = Assert<Equal<SkinErrorCode, ExpectedSkinErrorCodes>>;
23
+
24
+ type _SkinExtractErrorCodeIsASkinErrorCode = Assert<
25
+ SkinExtractErrorCode extends SkinErrorCode ? true : false
26
+ >;
27
+
28
+ type ResolveSkinJointsError = Extract<ReturnType<typeof resolveSkinJoints>, { ok: false }>['error'];
29
+ type _ResolveSkinJointsUsesSkinErrorAuthority = Assert<Equal<ResolveSkinJointsError, SkinError>>;
30
+
31
+ declare const extractCode: SkinExtractErrorCode;
32
+ const acceptsSkinErrorCode = (code: SkinErrorCode): SkinErrorCode => code;
33
+ acceptsSkinErrorCode(extractCode);
34
+
35
+ // @ts-expect-error invalid codes must not be accepted.
36
+ acceptsSkinErrorCode('not-a-skin-error');
37
+
38
+ function narrowSkinErrorDetail(error: SkinError): void {
39
+ switch (error.code) {
40
+ case 'skin-joint-path-unresolved': {
41
+ const path: readonly string[] = error.detail.path;
42
+ void path;
43
+ // @ts-expect-error code-driven narrowing excludes unrelated detail fields.
44
+ void error.detail.jointCount;
45
+ break;
46
+ }
47
+ case 'joint-count-mismatch': {
48
+ const actual: number = error.detail.actual;
49
+ void actual;
50
+ break;
51
+ }
52
+ case 'skeleton-resolve-failed': {
53
+ const skeletonHandle: number = error.detail.skeletonHandle;
54
+ void skeletonHandle;
55
+ break;
56
+ }
57
+ case 'skin-joint-count-exceeded': {
58
+ void error.detail.max;
59
+ break;
60
+ }
61
+ case 'skin-joint-despawned': {
62
+ void error.detail.meshEntity;
63
+ break;
64
+ }
65
+ case 'skin-instances-coexist-forbidden': {
66
+ void error.detail.entity;
67
+ break;
68
+ }
69
+ case 'joint-entity-dangling': {
70
+ void error.detail.jointIndex;
71
+ break;
72
+ }
73
+ default: {
74
+ const exhaustive: never = error;
75
+ void exhaustive;
76
+ }
77
+ }
78
+ }
79
+
80
+ export type _SkinErrorCodeOwnerChecks = {
81
+ /** @internal */
82
+ _derived: _SkinErrorCodeDerivesFromSkinError;
83
+ /** @internal */
84
+ _membership: _SkinErrorCodeHasExactMembership;
85
+ /** @internal */
86
+ _extractSubset: _SkinExtractErrorCodeIsASkinErrorCode;
87
+ /** @internal */
88
+ _resolveAuthority: _ResolveSkinJointsUsesSkinErrorAuthority;
89
+ /** @internal */
90
+ _narrowDetail: typeof narrowSkinErrorDetail;
91
+ };
@@ -0,0 +1,69 @@
1
+ import {
2
+ type AssetDecoderContribution,
3
+ type AssetKind,
4
+ err,
5
+ ok,
6
+ type SkeletonAsset,
7
+ type SkinAsset,
8
+ } from '@forgeax/engine-types';
9
+
10
+ function floatArray(value: unknown): Float32Array | undefined {
11
+ if (value instanceof Float32Array) return value;
12
+ if (Array.isArray(value) && value.every((item) => typeof item === 'number')) {
13
+ return Float32Array.from(value);
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ export const skinContribution: AssetDecoderContribution<SkinAsset, 'skin'> = {
19
+ kind: { kind: 'skin' } as AssetKind<SkinAsset, 'skin'>,
20
+ consumer: 'resolveSkinJoints',
21
+ decoder: {
22
+ async decode({ envelope }) {
23
+ const payload = envelope.payload;
24
+ if (
25
+ payload.kind === 'skin' &&
26
+ payload.skeletonGuid.length > 0 &&
27
+ payload.jointPaths.length > 0
28
+ ) {
29
+ return ok(payload);
30
+ }
31
+ return err({
32
+ code: 'asset-package-invalid',
33
+ expected: 'a skin payload with a skeleton GUID and joint paths',
34
+ hint: 'recook the skin binding and publish its skeleton reference',
35
+ detail: { guid: envelope.guid, reason: 'skin owner validation failed' },
36
+ });
37
+ },
38
+ },
39
+ };
40
+
41
+ export const skeletonContribution: AssetDecoderContribution<SkeletonAsset, 'skeleton'> = {
42
+ kind: { kind: 'skeleton' } as AssetKind<SkeletonAsset, 'skeleton'>,
43
+ consumer: 'resolveSkinJoints',
44
+ decoder: {
45
+ async decode({ envelope }) {
46
+ const payload = envelope.payload as unknown;
47
+ if (payload !== null && typeof payload === 'object') {
48
+ const source = payload as Record<string, unknown>;
49
+ const inverseBindMatrices = floatArray(source.inverseBindMatrices);
50
+ const jointCount = source.jointCount;
51
+ if (
52
+ source.kind === 'skeleton' &&
53
+ inverseBindMatrices !== undefined &&
54
+ Number.isSafeInteger(jointCount) &&
55
+ (jointCount as number) >= 0 &&
56
+ inverseBindMatrices.length === (jointCount as number) * 16
57
+ ) {
58
+ return ok({ kind: 'skeleton', inverseBindMatrices, jointCount: jointCount as number });
59
+ }
60
+ }
61
+ return err({
62
+ code: 'asset-package-invalid',
63
+ expected: 'a skeleton payload with one inverse-bind matrix per joint',
64
+ hint: 'recook the skeleton and publish its complete joint data',
65
+ detail: { guid: envelope.guid, reason: 'skeleton owner validation failed' },
66
+ });
67
+ },
68
+ },
69
+ };