@forgeax/engine-skinning 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +24 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/binding.unit.test.d.ts +2 -0
- package/dist/__tests__/binding.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin.integration.test.d.ts +2 -0
- package/dist/__tests__/plugin.integration.test.d.ts.map +1 -0
- package/dist/__tests__/skin-error-code-owner.test-d.d.ts +27 -0
- package/dist/__tests__/skin-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/assets/skin-decoder.d.ts +4 -0
- package/dist/assets/skin-decoder.d.ts.map +1 -0
- package/dist/errors.d.ts +232 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +375 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/resolve-skin-joints.d.ts +10 -0
- package/dist/resolve-skin-joints.d.ts.map +1 -0
- package/dist/skin.d.ts +5 -0
- package/dist/skin.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/binding.unit.test.ts +33 -0
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/plugin.integration.test.ts +23 -0
- package/src/__tests__/skin-error-code-owner.test-d.ts +91 -0
- package/src/assets/skin-decoder.ts +69 -0
- package/src/errors.ts +356 -0
- package/src/index.ts +15 -0
- package/src/plugin.ts +25 -0
- package/src/resolve-skin-joints.ts +24 -0
- package/src/skin.ts +68 -0
package/dist/plugin.d.ts
ADDED
|
@@ -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 @@
|
|
|
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.0.0-dev.8d955ade1c79",
|
|
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.0.0-dev.8d955ade1c79",
|
|
26
|
+
"@forgeax/engine-plugin": "0.0.0-dev.8d955ade1c79"
|
|
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,33 @@
|
|
|
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
|
+
|
|
5
|
+
describe('skinning binding contract', () => {
|
|
6
|
+
it('exposes Skin as the optional binding component', () => {
|
|
7
|
+
expect(Skin.name).toBe('Skin');
|
|
8
|
+
expect(componentSchema(Skin).skeleton).toBe('shared<SkeletonAsset>');
|
|
9
|
+
expect(componentSchema(Skin).joints).toBe('array<entity>');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns the canonical error and retries with only the Name map repaired', () => {
|
|
13
|
+
const jointPaths = ['Root/Arm', 'Root/Missing', 'Root/Hand'];
|
|
14
|
+
const names = new Map([
|
|
15
|
+
['Arm', 4 as never],
|
|
16
|
+
['Root', 5 as never],
|
|
17
|
+
]);
|
|
18
|
+
const result = resolveSkinJoints(jointPaths, names, 7 as never);
|
|
19
|
+
expect(result.ok).toBe(false);
|
|
20
|
+
if (result.ok) return;
|
|
21
|
+
const expected = new SkinJointPathUnresolvedError(7, ['Root', 'Missing'], 1);
|
|
22
|
+
expect(result.error).toBeInstanceOf(SkinJointPathUnresolvedError);
|
|
23
|
+
expect(result.error.code).toBe('skin-joint-path-unresolved');
|
|
24
|
+
expect(result.error.expected).toBe(expected.expected);
|
|
25
|
+
expect(result.error.hint).toBe(expected.hint);
|
|
26
|
+
expect(result.error.detail).toEqual(expected.detail);
|
|
27
|
+
|
|
28
|
+
names.set('Missing', 6 as never);
|
|
29
|
+
names.set('Hand', 8 as never);
|
|
30
|
+
const retry = resolveSkinJoints(jointPaths, names, 7 as never);
|
|
31
|
+
expect(retry).toEqual({ ok: true, value: new Uint32Array([4, 6, 8]) });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -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
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// @forgeax/engine-runtime -- skin cluster error classes.
|
|
2
|
+
//
|
|
3
|
+
// feat-20260704-runtime-tier1-decomposition M2 / w8 (D-3): skin / skeleton
|
|
4
|
+
// animation cluster -- joint count / despawn / path / coexistence and
|
|
5
|
+
// extract-stage binding failures. Palette/material/GPU failures stay in the
|
|
6
|
+
// render error union because render owns the emitting frame stages. Binding
|
|
7
|
+
// class names, .code literals, and .detail shapes are preserved byte-for-byte
|
|
8
|
+
// (OOS-4).
|
|
9
|
+
//
|
|
10
|
+
// SkinExtractErrorCode (the 3-member extract-stage subset union) is kept as a
|
|
11
|
+
// named export and folded into SkinErrorCode, preserving the pre-existing
|
|
12
|
+
// public symbol (OOS-4).
|
|
13
|
+
|
|
14
|
+
// ── SkinExtractErrorCode subset union ───────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* feat-20260612-skin-palette-per-frame-upload M2 / m2-5 subset union.
|
|
18
|
+
*
|
|
19
|
+
* Covers the three new fail-fast extract-stage errors that fire from
|
|
20
|
+
* `render-system-extract.ts` `hasSkin` segment when the per-frame palette
|
|
21
|
+
* upload pipeline cannot resolve a slice for an entity. Single-entity
|
|
22
|
+
* `continue` semantics: the entity is skipped, sibling entities in the
|
|
23
|
+
* same frame keep extracting (plan-strategy D-5).
|
|
24
|
+
*
|
|
25
|
+
* | code | class | trigger |
|
|
26
|
+
* |:--|:--|:--|
|
|
27
|
+
* | `'skeleton-resolve-failed'` | `SkeletonResolveFailedError` | `assets.get<SkeletonAsset>(skin.skeleton)` returns null/undefined |
|
|
28
|
+
* | `'joint-count-mismatch'` | `JointCountMismatchError` | `Skin.joints.length !== SkeletonAsset.jointCount` |
|
|
29
|
+
* | `'joint-entity-dangling'` | `JointEntityDanglingError` | `Skin.joints[i]` Entity is despawned (Transform.world view undefined) |
|
|
30
|
+
*
|
|
31
|
+
* AI users discriminate via `switch (err.code)` over `RuntimeErrorCode`;
|
|
32
|
+
* each member narrows to its `*Error` class with structured `.detail`.
|
|
33
|
+
*
|
|
34
|
+
* NOTE: distinct from the pre-existing `'skin-joint-despawned'` /
|
|
35
|
+
* `'skin-joint-path-unresolved'` / `'skin-joint-count-exceeded'`
|
|
36
|
+
* (advanceAnimationPlayer + post-spawn jointPath resolution); plan-strategy
|
|
37
|
+
* D-4 forbids reusing those codes for the new extract-stage triggers.
|
|
38
|
+
*/
|
|
39
|
+
export type SkinExtractErrorCode =
|
|
40
|
+
| 'skeleton-resolve-failed'
|
|
41
|
+
| 'joint-count-mismatch'
|
|
42
|
+
| 'joint-entity-dangling';
|
|
43
|
+
|
|
44
|
+
// ── SkinJointCountExceededError ────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detail for `RuntimeErrorCode 'skin-joint-count-exceeded'`.
|
|
48
|
+
*
|
|
49
|
+
* Emitted when a glTF skin has more than MAX_JOINTS (256) joints.
|
|
50
|
+
*/
|
|
51
|
+
export interface SkinJointCountExceededDetail {
|
|
52
|
+
readonly jointCount: number;
|
|
53
|
+
readonly max: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Structured error for skin joint count exceeding the engine cap.
|
|
58
|
+
*
|
|
59
|
+
* Emitted during skin import/validation. Four-field surface:
|
|
60
|
+
* - `.code = 'skin-joint-count-exceeded'`
|
|
61
|
+
* - `.expected` — max allowed (256)
|
|
62
|
+
* - `.hint` — reduce joint count in the source asset
|
|
63
|
+
* - `.detail = { jointCount, max }` — actual vs limit
|
|
64
|
+
*/
|
|
65
|
+
export class SkinJointCountExceededError extends Error {
|
|
66
|
+
readonly code = 'skin-joint-count-exceeded' as const;
|
|
67
|
+
readonly expected: string;
|
|
68
|
+
readonly hint: string;
|
|
69
|
+
readonly detail: SkinJointCountExceededDetail;
|
|
70
|
+
|
|
71
|
+
constructor(jointCount: number, max = 256) {
|
|
72
|
+
const expected = `jointCount <= ${max}`;
|
|
73
|
+
const hint = `skin has ${jointCount} joints (max ${max}); reduce joint count in the source glTF asset (OOS-skin-many-joints)`;
|
|
74
|
+
super(`skin joint count ${jointCount} exceeds max ${max}`);
|
|
75
|
+
this.name = 'SkinJointCountExceededError';
|
|
76
|
+
this.expected = expected;
|
|
77
|
+
this.hint = hint;
|
|
78
|
+
this.detail = { jointCount, max };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── SkinJointDespawnedError ─────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detail for `RuntimeErrorCode 'skin-joint-despawned'`.
|
|
86
|
+
*
|
|
87
|
+
* Emitted at extract time when a Skin.joints[i] Entity has been despawned.
|
|
88
|
+
*/
|
|
89
|
+
export interface SkinJointDespawnedDetail {
|
|
90
|
+
readonly meshEntity: number;
|
|
91
|
+
readonly jointIndex: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Structured error for despawned skin joint Entity.
|
|
96
|
+
*
|
|
97
|
+
* Emitted at extract time; the mesh draw is fully skipped.
|
|
98
|
+
* - `.code = 'skin-joint-despawned'`
|
|
99
|
+
* - `.expected` — all Skin.joints alive
|
|
100
|
+
* - `.hint` — remove the Skin component or re-spawn joints
|
|
101
|
+
* - `.detail = { meshEntity, jointIndex }`
|
|
102
|
+
*/
|
|
103
|
+
export class SkinJointDespawnedError extends Error {
|
|
104
|
+
readonly code = 'skin-joint-despawned' as const;
|
|
105
|
+
readonly expected: string;
|
|
106
|
+
readonly hint: string;
|
|
107
|
+
readonly detail: SkinJointDespawnedDetail;
|
|
108
|
+
|
|
109
|
+
constructor(meshEntity: number, jointIndex: number) {
|
|
110
|
+
const expected = `Skin.joints[${jointIndex}] references a live entity`;
|
|
111
|
+
const hint = `joint[${jointIndex}] of entity ${meshEntity} has been despawned; remove Skin component or re-spawn the joint entity (OOS-skin-joint-respawn)`;
|
|
112
|
+
super(`skin joint[${jointIndex}] despawned for entity ${meshEntity}`);
|
|
113
|
+
this.name = 'SkinJointDespawnedError';
|
|
114
|
+
this.expected = expected;
|
|
115
|
+
this.hint = hint;
|
|
116
|
+
this.detail = { meshEntity, jointIndex };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── SkinJointPathUnresolvedError ────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detail for `RuntimeErrorCode 'skin-joint-path-unresolved'`.
|
|
124
|
+
*
|
|
125
|
+
* Emitted at post-spawn time when a jointPath leaf name cannot be found.
|
|
126
|
+
*/
|
|
127
|
+
export interface SkinJointPathUnresolvedDetail {
|
|
128
|
+
readonly skinEntity: number;
|
|
129
|
+
readonly path: readonly string[];
|
|
130
|
+
readonly failedAtIndex: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Structured error for unresolved jointPath post-spawn.
|
|
135
|
+
*
|
|
136
|
+
* Emitted by resolveSkinJoints when Name lookup fails.
|
|
137
|
+
* - `.code = 'skin-joint-path-unresolved'`
|
|
138
|
+
* - `.expected` — Name-bearing entity exists for each jointPath leaf
|
|
139
|
+
* - `.hint` — verify glTF node Name preservation in the importer
|
|
140
|
+
* - `.detail = { skinEntity, path, failedAtIndex }`
|
|
141
|
+
*/
|
|
142
|
+
export class SkinJointPathUnresolvedError extends Error {
|
|
143
|
+
readonly code = 'skin-joint-path-unresolved' as const;
|
|
144
|
+
readonly expected: string;
|
|
145
|
+
readonly hint: string;
|
|
146
|
+
readonly detail: SkinJointPathUnresolvedDetail;
|
|
147
|
+
|
|
148
|
+
constructor(skinEntity: number, path: readonly string[], failedAtIndex: number) {
|
|
149
|
+
const leafName = path[failedAtIndex] ?? '<unknown>';
|
|
150
|
+
const expected = `joint entity with Name="${leafName}" exists in the world`;
|
|
151
|
+
const hint = `joint path "${path.join('/')}" for skin entity ${skinEntity} could not be resolved; verify glTF node names are preserved`;
|
|
152
|
+
super(
|
|
153
|
+
`joint path "${path.join('/')}" unresolved at index ${failedAtIndex} for entity ${skinEntity}`,
|
|
154
|
+
);
|
|
155
|
+
this.name = 'SkinJointPathUnresolvedError';
|
|
156
|
+
this.expected = expected;
|
|
157
|
+
this.hint = hint;
|
|
158
|
+
this.detail = { skinEntity, path, failedAtIndex };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── SkinInstancesCoexistForbiddenError ──────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Detail for `RuntimeErrorCode 'skin-instances-coexist-forbidden'`.
|
|
166
|
+
*
|
|
167
|
+
* Emitted at extract time when Skin + Instances coexist on the same entity.
|
|
168
|
+
*/
|
|
169
|
+
export interface SkinInstancesCoexistForbiddenDetail {
|
|
170
|
+
readonly entity: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Structured error for Skin + Instances coexistence on same entity.
|
|
175
|
+
*
|
|
176
|
+
* Emitted at extract time; the entity draw is skipped.
|
|
177
|
+
* - `.code = 'skin-instances-coexist-forbidden'`
|
|
178
|
+
* - `.expected` — Skin and Instances on separate entities
|
|
179
|
+
* - `.hint` — split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)
|
|
180
|
+
* - `.detail = { entity }`
|
|
181
|
+
*/
|
|
182
|
+
export class SkinInstancesCoexistForbiddenError extends Error {
|
|
183
|
+
readonly code = 'skin-instances-coexist-forbidden' as const;
|
|
184
|
+
readonly expected: string;
|
|
185
|
+
readonly hint: string;
|
|
186
|
+
readonly detail: SkinInstancesCoexistForbiddenDetail;
|
|
187
|
+
|
|
188
|
+
constructor(entity: number) {
|
|
189
|
+
const expected = 'Skin and Instances must not coexist on the same entity';
|
|
190
|
+
const hint = `entity ${entity} has both Skin and Instances; split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)`;
|
|
191
|
+
super(`Skin + Instances coexistence forbidden on entity ${entity}`);
|
|
192
|
+
this.name = 'SkinInstancesCoexistForbiddenError';
|
|
193
|
+
this.expected = expected;
|
|
194
|
+
this.hint = hint;
|
|
195
|
+
this.detail = { entity };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── SkeletonResolveFailedError ─────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Detail for `RuntimeErrorCode 'skeleton-resolve-failed'`.
|
|
203
|
+
*
|
|
204
|
+
* Emitted at extract time when `assets.get<SkeletonAsset>(skin.skeleton)`
|
|
205
|
+
* returns `null` / `undefined`. The skeleton handle is non-zero (the entity
|
|
206
|
+
* declared a Skin) but the asset is not registered (importer drift /
|
|
207
|
+
* AssetRegistry not warmed).
|
|
208
|
+
*/
|
|
209
|
+
export interface SkeletonResolveFailedDetail {
|
|
210
|
+
readonly entity: number;
|
|
211
|
+
readonly skeletonHandle: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Structured error for unresolved SkeletonAsset handle at extract time
|
|
216
|
+
* (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).
|
|
217
|
+
*
|
|
218
|
+
* Emitted at extract time; the entity draw is skipped (continue), other
|
|
219
|
+
* entities in the same frame keep extracting.
|
|
220
|
+
* - `.code = 'skeleton-resolve-failed'`
|
|
221
|
+
* - `.expected` — Skin.skeleton handle resolves to a registered SkeletonAsset
|
|
222
|
+
* - `.hint` — verify SkeletonAsset is imported into pack-index AND registered
|
|
223
|
+
* via AssetRegistry.register(handle, asset) before extractFrame
|
|
224
|
+
* - `.detail = { entity, skeletonHandle }`
|
|
225
|
+
*/
|
|
226
|
+
export class SkeletonResolveFailedError extends Error {
|
|
227
|
+
readonly code = 'skeleton-resolve-failed' as const;
|
|
228
|
+
readonly expected: string;
|
|
229
|
+
readonly hint: string;
|
|
230
|
+
readonly detail: SkeletonResolveFailedDetail;
|
|
231
|
+
|
|
232
|
+
constructor(entity: number, skeletonHandle: number) {
|
|
233
|
+
const expected = `Skin.skeleton handle ${skeletonHandle} resolves to a registered SkeletonAsset`;
|
|
234
|
+
const hint = `entity ${entity} Skin.skeleton handle ${skeletonHandle} is not registered; check that the SkeletonAsset went through the gltf importer into pack-index AND that AssetRegistry.register was called for the handle before extractFrame runs`;
|
|
235
|
+
super(`Skin skeleton resolve failed on entity ${entity}: handle ${skeletonHandle}`);
|
|
236
|
+
this.name = 'SkeletonResolveFailedError';
|
|
237
|
+
this.expected = expected;
|
|
238
|
+
this.hint = hint;
|
|
239
|
+
this.detail = { entity, skeletonHandle };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── JointCountMismatchError ────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Detail for `RuntimeErrorCode 'joint-count-mismatch'`.
|
|
247
|
+
*
|
|
248
|
+
* Emitted at extract time when `Skin.joints.length !== SkeletonAsset.jointCount`.
|
|
249
|
+
* `expected` is the SkeletonAsset's jointCount (the source of truth);
|
|
250
|
+
* `actual` is the entity's `Skin.joints.length` (the runtime entity reference
|
|
251
|
+
* list materialized at post-spawn time).
|
|
252
|
+
*/
|
|
253
|
+
export interface JointCountMismatchDetail {
|
|
254
|
+
readonly entity: number;
|
|
255
|
+
readonly expected: number;
|
|
256
|
+
readonly actual: number;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Structured error for SkinAsset.joints[] vs SkeletonAsset.jointCount disagreement
|
|
261
|
+
* (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).
|
|
262
|
+
*
|
|
263
|
+
* Emitted at extract time; the entity draw is skipped (continue).
|
|
264
|
+
* - `.code = 'joint-count-mismatch'`
|
|
265
|
+
* - `.expected` — Skin.joints.length === SkeletonAsset.jointCount
|
|
266
|
+
* - `.hint` — verify SkinAsset.joints[] and SkeletonAsset jointPaths[]
|
|
267
|
+
* come from the same glTF skin node
|
|
268
|
+
* - `.detail = { entity, expected, actual }`
|
|
269
|
+
*/
|
|
270
|
+
export class JointCountMismatchError extends Error {
|
|
271
|
+
readonly code = 'joint-count-mismatch' as const;
|
|
272
|
+
readonly expected: string;
|
|
273
|
+
readonly hint: string;
|
|
274
|
+
readonly detail: JointCountMismatchDetail;
|
|
275
|
+
|
|
276
|
+
constructor(entity: number, expected: number, actual: number) {
|
|
277
|
+
const expectedStr = `Skin.joints.length === SkeletonAsset.jointCount (=${expected})`;
|
|
278
|
+
const hint = `entity ${entity}: Skin.joints.length=${actual} disagrees with SkeletonAsset.jointCount=${expected}; verify SkinAsset.joints[] and SkeletonAsset jointPaths[] come from the same glTF skin node`;
|
|
279
|
+
super(
|
|
280
|
+
`joint count mismatch on entity ${entity}: SkeletonAsset.jointCount=${expected}, Skin.joints.length=${actual}`,
|
|
281
|
+
);
|
|
282
|
+
this.name = 'JointCountMismatchError';
|
|
283
|
+
this.expected = expectedStr;
|
|
284
|
+
this.hint = hint;
|
|
285
|
+
this.detail = { entity, expected, actual };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── JointEntityDanglingError ──────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Detail for `RuntimeErrorCode 'joint-entity-dangling'`.
|
|
293
|
+
*
|
|
294
|
+
* Emitted at extract time when `Skin.joints[i]` points at an Entity that has
|
|
295
|
+
* been despawned (or lost its Transform component) so
|
|
296
|
+
* `worldInternal._getArrayView(jointEntity, Transform, 'world')` returns
|
|
297
|
+
* undefined. `jointIndex` is the position within `Skin.joints[]`.
|
|
298
|
+
*/
|
|
299
|
+
export interface JointEntityDanglingDetail {
|
|
300
|
+
readonly entity: number;
|
|
301
|
+
readonly jointIndex: number;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Structured error for despawned (or Transform-less) joint Entity at extract
|
|
306
|
+
* time (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).
|
|
307
|
+
*
|
|
308
|
+
* Distinct from the pre-existing `SkinJointDespawnedError` which fires from
|
|
309
|
+
* advanceAnimationPlayer (animation-stage); this one fires from extractFrame
|
|
310
|
+
* (palette-upload stage) when the per-joint world mat4 view is missing.
|
|
311
|
+
*
|
|
312
|
+
* Emitted at extract time; the entity draw is skipped (continue).
|
|
313
|
+
* - `.code = 'joint-entity-dangling'`
|
|
314
|
+
* - `.expected` — Skin.joints[i] references a live Entity with Transform
|
|
315
|
+
* - `.hint` — sync Skin.joints[] when joint entities are despawned, or
|
|
316
|
+
* re-import the scene through the gltf importer to refresh Entity refs
|
|
317
|
+
* - `.detail = { entity, jointIndex }`
|
|
318
|
+
*/
|
|
319
|
+
export class JointEntityDanglingError extends Error {
|
|
320
|
+
readonly code = 'joint-entity-dangling' as const;
|
|
321
|
+
readonly expected: string;
|
|
322
|
+
readonly hint: string;
|
|
323
|
+
readonly detail: JointEntityDanglingDetail;
|
|
324
|
+
|
|
325
|
+
constructor(entity: number, jointIndex: number) {
|
|
326
|
+
const expected = `Skin.joints[${jointIndex}] references a live Entity with Transform`;
|
|
327
|
+
const hint = `entity ${entity} Skin.joints[${jointIndex}] points at a despawned (or Transform-less) Entity; sync Skin.joints[] when joint entities are despawned, or re-import the scene through the gltf importer to refresh Entity references`;
|
|
328
|
+
super(`joint entity dangling on entity ${entity} at jointIndex ${jointIndex}`);
|
|
329
|
+
this.name = 'JointEntityDanglingError';
|
|
330
|
+
this.expected = expected;
|
|
331
|
+
this.hint = hint;
|
|
332
|
+
this.detail = { entity, jointIndex };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// -- SkinErrorCode / SkinError closed unions ------------------------------------
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Closed union of skin-cluster error codes derived from the correlated error
|
|
340
|
+
* union. AI users perform exhaustive `switch (err.code)` without default; TS
|
|
341
|
+
* guards completeness.
|
|
342
|
+
*/
|
|
343
|
+
export type SkinErrorCode = SkinError['code'];
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Closed union of the skin-cluster structured error classes, each carrying a
|
|
347
|
+
* `SkinErrorCode` discriminant on `.code`.
|
|
348
|
+
*/
|
|
349
|
+
export type SkinError =
|
|
350
|
+
| SkinJointCountExceededError
|
|
351
|
+
| SkinJointDespawnedError
|
|
352
|
+
| SkinJointPathUnresolvedError
|
|
353
|
+
| SkinInstancesCoexistForbiddenError
|
|
354
|
+
| SkeletonResolveFailedError
|
|
355
|
+
| JointCountMismatchError
|
|
356
|
+
| JointEntityDanglingError;
|