@forgeax/engine-geometry 0.1.20 → 0.1.21
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/README.md +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/mesh-builder.integration.test.d.ts +2 -0
- package/dist/__tests__/mesh-builder.integration.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-builder.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-builder.unit.test.d.ts.map +1 -0
- package/dist/assets/mesh-binary.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +366 -2
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-builder.d.ts +28 -0
- package/dist/mesh-builder.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/mesh-builder.integration.test.ts +58 -0
- package/src/__tests__/mesh-builder.unit.test.ts +96 -0
- package/src/assets/mesh-binary.ts +12 -1
- package/src/index.ts +6 -1
- package/src/mesh-builder.ts +457 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AssetError, type MeshAsset, type MeshMaterialSlot, type PrimitiveTopology, type Result, type Submesh, type VertexAttributeMap } from '@forgeax/engine-types';
|
|
2
|
+
/** Input shape for one independently drawn submesh. */
|
|
3
|
+
export type MeshBuilderSubmesh = Partial<Submesh> & {
|
|
4
|
+
readonly topology?: PrimitiveTopology;
|
|
5
|
+
};
|
|
6
|
+
/** All authoring facts accepted by {@link createMeshBuilder}. */
|
|
7
|
+
export interface MeshBuilderOptions {
|
|
8
|
+
readonly attributes?: VertexAttributeMap;
|
|
9
|
+
readonly indices?: ArrayLike<number>;
|
|
10
|
+
readonly submeshes?: readonly MeshBuilderSubmesh[];
|
|
11
|
+
readonly materialSlots?: readonly MeshMaterialSlot[];
|
|
12
|
+
}
|
|
13
|
+
export interface MeshBuilder {
|
|
14
|
+
/** Append one equally-cardinal attribute batch to the accumulated source. */
|
|
15
|
+
appendVertices(attributes: VertexAttributeMap): Result<void, AssetError>;
|
|
16
|
+
/** Append raw index values; width is derived during build. */
|
|
17
|
+
appendIndices(indices: ArrayLike<number>): Result<void, AssetError>;
|
|
18
|
+
/** Add a draw range; omitted ranges are completed from the final source. */
|
|
19
|
+
addSubmesh(submesh: MeshBuilderSubmesh): Result<void, AssetError>;
|
|
20
|
+
/** Derive one immutable MeshAsset from the accumulated source. */
|
|
21
|
+
build(): Result<MeshAsset, AssetError>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a geometry-owned builder. All derived facts are computed once in
|
|
25
|
+
* `build()` from the accumulated canonical attribute source.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createMeshBuilder(options?: MeshBuilderOptions): MeshBuilder;
|
|
28
|
+
//# sourceMappingURL=mesh-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-builder.d.ts","sourceRoot":"","sources":["../src/mesh-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,UAAU,EAEV,KAAK,SAAS,EACd,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAiD/B,uDAAuD;AACvD,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CACvC,CAAC;AAEF,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACzE,8DAA8D;IAC9D,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACpE,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAClE,kEAAkE;IAClE,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;CACxC;AAyND;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAwJ/E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-geometry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-ecs": "0.1.
|
|
26
|
-
"@forgeax/engine-math": "0.1.
|
|
27
|
-
"@forgeax/engine-pack": "0.1.
|
|
28
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-ecs": "0.1.21",
|
|
26
|
+
"@forgeax/engine-math": "0.1.21",
|
|
27
|
+
"@forgeax/engine-pack": "0.1.21",
|
|
28
|
+
"@forgeax/engine-types": "0.1.21"
|
|
29
29
|
},
|
|
30
30
|
"forgeax": {
|
|
31
31
|
"metrics": {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AssetDecoderInput, MeshAsset } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { meshAssetDecoder } from '../assets/mesh-decoder.js';
|
|
4
|
+
import { createMeshBuilder } from '../mesh-builder.js';
|
|
5
|
+
|
|
6
|
+
function decode(mesh: MeshAsset) {
|
|
7
|
+
const context = {
|
|
8
|
+
envelope: { guid: 'mesh-builder', kind: 'mesh', payload: mesh, refs: [], artifacts: {} },
|
|
9
|
+
artifacts: { read: async () => ({ ok: false as const, error: new Error('not used') }) },
|
|
10
|
+
} as unknown as AssetDecoderInput<MeshAsset>;
|
|
11
|
+
return meshAssetDecoder.decode(context);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('MeshBuilder -> MeshAsset decoder', () => {
|
|
15
|
+
it('feeds a position-only builder output through the existing decoder', async () => {
|
|
16
|
+
const built = createMeshBuilder({
|
|
17
|
+
attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
|
|
18
|
+
}).build();
|
|
19
|
+
expect(built.ok).toBe(true);
|
|
20
|
+
if (!built.ok) return;
|
|
21
|
+
const decoded = await decode(built.value);
|
|
22
|
+
expect(decoded.ok).toBe(true);
|
|
23
|
+
if (decoded.ok) expect(decoded.value.aabb).toEqual(built.value.aabb);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('preserves multiple submesh topology and material slots at the consumer boundary', async () => {
|
|
27
|
+
const built = createMeshBuilder({
|
|
28
|
+
attributes: {
|
|
29
|
+
position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0, 0, 3, 0, 0, 2, 1, 0]),
|
|
30
|
+
},
|
|
31
|
+
indices: [0, 1, 2, 3, 4, 5],
|
|
32
|
+
materialSlots: [{ slotName: 'base' }, { slotName: 'accent' }],
|
|
33
|
+
submeshes: [
|
|
34
|
+
{
|
|
35
|
+
indexOffset: 0,
|
|
36
|
+
indexCount: 3,
|
|
37
|
+
vertexCount: 3,
|
|
38
|
+
topology: 'triangle-list',
|
|
39
|
+
materialSlot: 0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
indexOffset: 3,
|
|
43
|
+
indexCount: 3,
|
|
44
|
+
vertexCount: 3,
|
|
45
|
+
topology: 'triangle-list',
|
|
46
|
+
materialSlot: 1,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}).build();
|
|
50
|
+
expect(built.ok).toBe(true);
|
|
51
|
+
if (!built.ok) return;
|
|
52
|
+
const decoded = await decode(built.value);
|
|
53
|
+
expect(decoded.ok).toBe(true);
|
|
54
|
+
if (!decoded.ok) return;
|
|
55
|
+
expect(decoded.value.submeshes).toEqual(built.value.submeshes);
|
|
56
|
+
expect(decoded.value.materialSlots).toEqual(built.value.materialSlots);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createMeshBuilder } from '../mesh-builder.js';
|
|
3
|
+
|
|
4
|
+
const trianglePositions = new Float32Array([-1, -2, -3, 4, 5, 6, 0, 2, 1]);
|
|
5
|
+
|
|
6
|
+
describe('createMeshBuilder', () => {
|
|
7
|
+
it('derives position-only layout, AABB, index width, and default submesh', () => {
|
|
8
|
+
const result = createMeshBuilder({
|
|
9
|
+
attributes: { position: trianglePositions },
|
|
10
|
+
indices: [0, 1, 2],
|
|
11
|
+
}).build();
|
|
12
|
+
expect(result.ok).toBe(true);
|
|
13
|
+
if (!result.ok) return;
|
|
14
|
+
expect(result.value.attributes).toEqual({ position: trianglePositions });
|
|
15
|
+
expect(result.value.indices).toBeInstanceOf(Uint16Array);
|
|
16
|
+
expect(result.value.aabb).toEqual(Float32Array.of(-1, -2, -3, 4, 5, 6));
|
|
17
|
+
expect(result.value.submeshes).toEqual([
|
|
18
|
+
{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
|
|
19
|
+
]);
|
|
20
|
+
expect(result.value.materialSlots).toEqual([{ slotName: 'Default' }]);
|
|
21
|
+
expect(result.value.vertices).toEqual(trianglePositions);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('packs the complete canonical attribute set without a caller-owned layout', () => {
|
|
25
|
+
const result = createMeshBuilder({
|
|
26
|
+
attributes: {
|
|
27
|
+
position: trianglePositions,
|
|
28
|
+
normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
|
|
29
|
+
uv: new Float32Array([0, 0, 1, 0, 0, 1]),
|
|
30
|
+
tangent: new Float32Array([1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]),
|
|
31
|
+
color: new Float32Array([1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1]),
|
|
32
|
+
},
|
|
33
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
34
|
+
}).build();
|
|
35
|
+
expect(result.ok).toBe(true);
|
|
36
|
+
if (!result.ok) return;
|
|
37
|
+
expect(result.value.vertices.byteLength).toBe(3 * 64);
|
|
38
|
+
expect(result.value.attributes.color).toEqual(
|
|
39
|
+
new Float32Array([1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1]),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('chooses Uint32 at the first index beyond the Uint16 range', () => {
|
|
44
|
+
const positions = new Float32Array(65_537 * 3);
|
|
45
|
+
positions[65_536 * 3] = 10;
|
|
46
|
+
const result = createMeshBuilder({
|
|
47
|
+
attributes: { position: positions },
|
|
48
|
+
indices: [0, 65_535, 65_536],
|
|
49
|
+
}).build();
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
if (result.ok) expect(result.value.indices).toBeInstanceOf(Uint32Array);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns structured errors for missing attributes, cardinality, indices, and ranges', () => {
|
|
55
|
+
const missingPosition = createMeshBuilder({
|
|
56
|
+
attributes: { normal: new Float32Array(3) },
|
|
57
|
+
}).build();
|
|
58
|
+
expect(missingPosition.ok).toBe(false);
|
|
59
|
+
if (!missingPosition.ok) expect(missingPosition.error.code).toBe('asset-invalid-value');
|
|
60
|
+
|
|
61
|
+
const mismatched = createMeshBuilder({
|
|
62
|
+
attributes: { position: trianglePositions, normal: new Float32Array(3) },
|
|
63
|
+
}).build();
|
|
64
|
+
expect(mismatched.ok).toBe(false);
|
|
65
|
+
if (!mismatched.ok) expect(mismatched.error.detail).toMatchObject({ field: 'normal' });
|
|
66
|
+
|
|
67
|
+
const outOfBounds = createMeshBuilder({
|
|
68
|
+
attributes: { position: trianglePositions },
|
|
69
|
+
indices: [0, 1, 3],
|
|
70
|
+
}).build();
|
|
71
|
+
expect(outOfBounds.ok).toBe(false);
|
|
72
|
+
if (!outOfBounds.ok) expect(outOfBounds.error.detail).toMatchObject({ field: 'indices' });
|
|
73
|
+
|
|
74
|
+
const invalidRange = createMeshBuilder({
|
|
75
|
+
attributes: { position: trianglePositions },
|
|
76
|
+
indices: [0, 1, 2],
|
|
77
|
+
submeshes: [{ indexOffset: 2, indexCount: 2, vertexCount: 3, topology: 'triangle-list' }],
|
|
78
|
+
}).build();
|
|
79
|
+
expect(invalidRange.ok).toBe(false);
|
|
80
|
+
if (!invalidRange.ok)
|
|
81
|
+
expect(invalidRange.error.detail).toMatchObject({ field: 'submeshes[0]' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('copies accumulated source data and preserves built output immutability', () => {
|
|
85
|
+
const source = new Float32Array(trianglePositions);
|
|
86
|
+
const builder = createMeshBuilder({ attributes: { position: source } });
|
|
87
|
+
const first = builder.build();
|
|
88
|
+
expect(first.ok).toBe(true);
|
|
89
|
+
source[0] = 99;
|
|
90
|
+
const second = builder.build();
|
|
91
|
+
expect(second.ok).toBe(true);
|
|
92
|
+
if (!first.ok || !second.ok) return;
|
|
93
|
+
expect(first.value.attributes.position).toEqual(trianglePositions);
|
|
94
|
+
expect(second.value.attributes.position).toEqual(trianglePositions);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from '@forgeax/engine-types';
|
|
15
15
|
import { PROCEDURAL_FLOATS_PER_VERTEX } from '../box';
|
|
16
16
|
import {
|
|
17
|
+
deriveVertexLayoutProjection,
|
|
17
18
|
deriveVertexLayoutProjectionFromMask,
|
|
18
19
|
type VertexLayoutProjection,
|
|
19
20
|
} from '../vertex-attribute-layout';
|
|
@@ -400,6 +401,16 @@ export function normalizeMeshPayload(
|
|
|
400
401
|
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return undefined;
|
|
401
402
|
const source = payload as Record<string, unknown>;
|
|
402
403
|
if (source.kind !== 'mesh') return undefined;
|
|
404
|
+
const sourceAttributes =
|
|
405
|
+
source.attributes !== null && typeof source.attributes === 'object'
|
|
406
|
+
? (source.attributes as VertexAttributeMap)
|
|
407
|
+
: undefined;
|
|
408
|
+
const projection =
|
|
409
|
+
sourceAttributes === undefined ? undefined : deriveVertexLayoutProjection(sourceAttributes);
|
|
410
|
+
const floatsPerVertex =
|
|
411
|
+
projection === undefined || projection.attributes.length === 0
|
|
412
|
+
? PROCEDURAL_FLOATS_PER_VERTEX
|
|
413
|
+
: projection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
|
|
403
414
|
return meshFromParts(
|
|
404
415
|
{
|
|
405
416
|
vertices: source.vertices,
|
|
@@ -414,7 +425,7 @@ export function normalizeMeshPayload(
|
|
|
414
425
|
: {}),
|
|
415
426
|
...(source.morphTargets === undefined ? {} : { morphTargets: source.morphTargets }),
|
|
416
427
|
...(source.morphWeights === undefined ? {} : { morphWeights: source.morphWeights }),
|
|
417
|
-
floatsPerVertex
|
|
428
|
+
floatsPerVertex,
|
|
418
429
|
},
|
|
419
430
|
refs,
|
|
420
431
|
);
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ export type { VertexAttributePackDetail } from '@forgeax/engine-types';
|
|
|
15
15
|
|
|
16
16
|
export { meshAssetContribution, meshAssetDecoder, meshAssetKind } from './assets/mesh-decoder';
|
|
17
17
|
export { createPrimitiveMesh, type PrimitiveMeshKind } from './assets/primitive-mesh';
|
|
18
|
-
|
|
19
18
|
export {
|
|
20
19
|
createBoxGeometry,
|
|
21
20
|
meshFromInterleaved,
|
|
@@ -36,6 +35,12 @@ export {
|
|
|
36
35
|
} from './dim2';
|
|
37
36
|
// Edge factories return Result<MeshAsset, AssetError>; threshold units are degrees.
|
|
38
37
|
export { createEdgesGeometry, createWireframeGeometry } from './edges';
|
|
38
|
+
export {
|
|
39
|
+
createMeshBuilder,
|
|
40
|
+
type MeshBuilder,
|
|
41
|
+
type MeshBuilderOptions,
|
|
42
|
+
type MeshBuilderSubmesh,
|
|
43
|
+
} from './mesh-builder';
|
|
39
44
|
export { createPlaneGeometry } from './plane';
|
|
40
45
|
export { createSphereGeometry } from './sphere';
|
|
41
46
|
export { computeTangentVec4 } from './tangent';
|