@forgeax/engine-import 0.1.19 → 0.1.20

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.
@@ -2,6 +2,7 @@ import type { ScriptablePackSceneComponent } from '@forgeax/engine-pack/source';
2
2
  import { type AssetOutputProducer, AssetOutputProducerRegistry } from './scriptable-pack.js';
3
3
  export declare const materialAssetOutputProducer: AssetOutputProducer;
4
4
  export declare const meshAssetOutputProducer: AssetOutputProducer;
5
+ export declare const textureAssetOutputProducer: AssetOutputProducer;
5
6
  export declare function createSceneAssetOutputProducer(sceneComponents?: readonly ScriptablePackSceneComponent[]): AssetOutputProducer;
6
7
  export declare function createStandardAssetOutputProducerRegistry(sceneComponents?: readonly ScriptablePackSceneComponent[]): AssetOutputProducerRegistry;
7
8
  //# sourceMappingURL=scriptable-pack-output-producers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scriptable-pack-output-producers.d.ts","sourceRoot":"","sources":["../src/scriptable-pack-output-producers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AA4BhF,OAAO,EAEL,KAAK,mBAAmB,EACxB,2BAA2B,EAE5B,MAAM,sBAAsB,CAAC;AA4X9B,eAAO,MAAM,2BAA2B,qBAIvC,CAAC;AACF,eAAO,MAAM,uBAAuB,qBAA2D,CAAC;AAEhG,wBAAgB,8BAA8B,CAC5C,eAAe,GAAE,SAAS,4BAA4B,EAAO,GAC5D,mBAAmB,CAKrB;AAED,wBAAgB,yCAAyC,CACvD,eAAe,GAAE,SAAS,4BAA4B,EAAO,GAC5D,2BAA2B,CAuB7B"}
1
+ {"version":3,"file":"scriptable-pack-output-producers.d.ts","sourceRoot":"","sources":["../src/scriptable-pack-output-producers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAkChF,OAAO,EAEL,KAAK,mBAAmB,EACxB,2BAA2B,EAE5B,MAAM,sBAAsB,CAAC;AAuZ9B,eAAO,MAAM,2BAA2B,qBAIvC,CAAC;AACF,eAAO,MAAM,uBAAuB,qBAA2D,CAAC;AAChG,eAAO,MAAM,0BAA0B,qBAItC,CAAC;AAEF,wBAAgB,8BAA8B,CAC5C,eAAe,GAAE,SAAS,4BAA4B,EAAO,GAC5D,mBAAmB,CAKrB;AAED,wBAAgB,yCAAyC,CACvD,eAAe,GAAE,SAAS,4BAA4B,EAAO,GAC5D,2BAA2B,CAuB7B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-import",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,12 +27,12 @@
27
27
  "LICENSE"
28
28
  ],
29
29
  "dependencies": {
30
- "@forgeax/engine-ddc": "0.1.19",
31
- "@forgeax/engine-ecs": "0.1.19",
32
- "@forgeax/engine-geometry": "0.1.19",
33
- "@forgeax/engine-pack": "0.1.19",
34
- "@forgeax/engine-scene": "0.1.19",
35
- "@forgeax/engine-types": "0.1.19",
30
+ "@forgeax/engine-ddc": "0.1.20",
31
+ "@forgeax/engine-ecs": "0.1.20",
32
+ "@forgeax/engine-geometry": "0.1.20",
33
+ "@forgeax/engine-pack": "0.1.20",
34
+ "@forgeax/engine-scene": "0.1.20",
35
+ "@forgeax/engine-types": "0.1.20",
36
36
  "@noble/hashes": "^2.2.0"
37
37
  },
38
38
  "devDependencies": {
@@ -21,12 +21,11 @@ describe('ScriptablePack terminal product contract', () => {
21
21
  ok({
22
22
  hero: {
23
23
  kind: 'texture',
24
- width: 1,
25
- height: 1,
26
- format: 'rgba8unorm',
27
- data: new Uint8Array(),
24
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
25
+ format: 'rgba8unorm-srgb',
26
+ data: new Uint8Array(4),
28
27
  colorSpace: 'srgb',
29
- mipmap: false,
28
+ mips: { kind: 'none' },
30
29
  },
31
30
  }),
32
31
  },
@@ -89,12 +89,11 @@ const matrixAssets = [
89
89
  { kind: 'scene', entities: [] } satisfies SceneAsset,
90
90
  {
91
91
  kind: 'texture',
92
- width: 1,
93
- height: 1,
94
- format: 'rgba8unorm',
92
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
93
+ format: 'rgba8unorm-srgb',
95
94
  data: new Uint8Array([255, 255, 255, 255]),
96
95
  colorSpace: 'srgb',
97
- mipmap: false,
96
+ mips: { kind: 'none' },
98
97
  } satisfies TextureAsset,
99
98
  {
100
99
  kind: 'equirect',
@@ -179,7 +178,7 @@ describe('standard ScriptablePack output producers', () => {
179
178
  scene: 'scene-pack/3',
180
179
  skeleton: 'ordinary-pod/1',
181
180
  skin: 'ordinary-pod/1',
182
- texture: 'ordinary-pod/1',
181
+ texture: 'texture-pack/1',
183
182
  tileset: 'ordinary-pod/1',
184
183
  video: 'ordinary-pod/1',
185
184
  });
@@ -0,0 +1,96 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import type { AssetGuid as AssetGuidType, TextureAsset } from '@forgeax/engine-types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { textureAssetOutputProducer } from '../scriptable-pack-output-producers.js';
5
+
6
+ const GUID = '019ffa97-3000-7000-8000-000000000201';
7
+ const SOURCE_KEY = 'density/volume';
8
+
9
+ function guid(value: string): AssetGuidType {
10
+ const parsed = AssetGuid.parse(value);
11
+ if (!parsed.ok) throw parsed.error;
12
+ return parsed.value;
13
+ }
14
+
15
+ function texture(shape: TextureAsset['shape'], data: Uint8Array): TextureAsset {
16
+ return {
17
+ kind: 'texture',
18
+ shape,
19
+ format: 'r8unorm',
20
+ colorSpace: 'linear',
21
+ mips: { kind: 'none' },
22
+ data,
23
+ };
24
+ }
25
+
26
+ describe('ScriptablePack texture output producer', () => {
27
+ it.each([
28
+ {
29
+ label: 'array',
30
+ shape: { viewDimension: '2d-array' as const, extent: { width: 8, height: 4, layers: 3 } },
31
+ bytes: 8 * 4 * 3,
32
+ },
33
+ {
34
+ label: 'volume',
35
+ shape: { viewDimension: '3d' as const, extent: { width: 4, height: 4, depth: 4 } },
36
+ bytes: 4 * 4 * 4,
37
+ },
38
+ ])('matches source producer facts for a $label logical asset', async ({ shape, bytes }) => {
39
+ const payload = texture(shape, new Uint8Array(bytes).fill(23));
40
+ const result = await textureAssetOutputProducer.produce({
41
+ guid: GUID,
42
+ sourceKey: SOURCE_KEY,
43
+ asset: payload,
44
+ });
45
+
46
+ expect(result.ok).toBe(true);
47
+ if (!result.ok) return;
48
+ expect(result.value.payload).toEqual(payload);
49
+ expect(result.value.artifacts.body).toMatchObject({
50
+ mediaType: 'application/x-forgeax-r8',
51
+ assetCodec: { name: 'r8unorm', version: '1' },
52
+ });
53
+ expect(result.value.artifacts.body?.bytes).toEqual(payload.data);
54
+ expect(result.value.refs).toEqual([]);
55
+ });
56
+
57
+ it('keeps one publication GUID and rejects slice or layer identity fabrication', async () => {
58
+ const payload = texture(
59
+ { viewDimension: '3d', extent: { width: 2, height: 2, depth: 2 } },
60
+ new Uint8Array(8),
61
+ );
62
+ const result = await textureAssetOutputProducer.produce({
63
+ guid: AssetGuid.format(guid(GUID)),
64
+ sourceKey: SOURCE_KEY,
65
+ asset: payload,
66
+ });
67
+
68
+ expect(result.ok).toBe(true);
69
+ if (!result.ok) return;
70
+ expect((result.value.payload as TextureAsset).shape).toEqual(payload.shape);
71
+ expect(result.value.artifacts.body?.bytes.byteLength).toBe(8);
72
+ expect(result.value.refs).toEqual([]);
73
+ expect(result.value).not.toHaveProperty('sliceGuids');
74
+ expect(result.value).not.toHaveProperty('layerGuids');
75
+ });
76
+
77
+ it('fails fast when the payload data is not a canonical byte stream', async () => {
78
+ const payload = texture(
79
+ { viewDimension: '3d', extent: { width: 2, height: 2, depth: 2 } },
80
+ new Uint8Array(7),
81
+ );
82
+ const result = await textureAssetOutputProducer.produce({
83
+ guid: GUID,
84
+ sourceKey: SOURCE_KEY,
85
+ asset: payload,
86
+ });
87
+
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error.code).toBe('import-internal-error');
91
+ expect(result.error.detail).toMatchObject({
92
+ reason: expect.stringContaining('texture data is not canonical'),
93
+ });
94
+ }
95
+ });
96
+ });
package/src/index.ts CHANGED
@@ -126,6 +126,7 @@ export {
126
126
  createStandardAssetOutputProducerRegistry,
127
127
  materialAssetOutputProducer,
128
128
  meshAssetOutputProducer,
129
+ textureAssetOutputProducer,
129
130
  } from './scriptable-pack-output-producers.js';
130
131
  export {
131
132
  createScriptablePackStagedAssetSnapshotSource,
@@ -23,7 +23,13 @@ import type {
23
23
  TilesetAsset,
24
24
  VideoAsset,
25
25
  } from '@forgeax/engine-types';
26
- import { err, ImportError, MATERIAL_TEXTURE_SLOTS, ok } from '@forgeax/engine-types';
26
+ import {
27
+ deriveTextureLayout,
28
+ err,
29
+ ImportError,
30
+ MATERIAL_TEXTURE_SLOTS,
31
+ ok,
32
+ } from '@forgeax/engine-types';
27
33
  import { sha256 } from '@noble/hashes/sha2.js';
28
34
  import { bytesToHex } from '@noble/hashes/utils.js';
29
35
  import { packMeshBinV4 } from './mesh-bin.js';
@@ -246,23 +252,50 @@ function bytes(value: Uint8Array | Uint8ClampedArray): Uint8Array {
246
252
  return Uint8Array.from(value);
247
253
  }
248
254
 
255
+ function textureProduct(input: AssetOutputInput): AssetOutputProduct {
256
+ if (input.asset.kind !== 'texture') throw new TypeError('expected TextureAsset');
257
+ const texture = input.asset as TextureAsset;
258
+ const layout = deriveTextureLayout({
259
+ shape: texture.shape,
260
+ format: texture.format,
261
+ mips: texture.mips,
262
+ actualByteLength: texture.data.byteLength,
263
+ order: 'mip-major,image-major,row-major',
264
+ });
265
+ if (!layout.ok) {
266
+ const expectedBytes =
267
+ layout.error.code === 'texture-packing-invalid'
268
+ ? layout.error.detail.expectedBytes
269
+ : texture.data.byteLength;
270
+ const actualBytes =
271
+ layout.error.code === 'texture-packing-invalid'
272
+ ? layout.error.detail.actualBytes
273
+ : texture.data.byteLength;
274
+ throw new TypeError(
275
+ `texture data is not canonical: expected ${expectedBytes} bytes, got ${actualBytes}`,
276
+ );
277
+ }
278
+ return {
279
+ payload: texture,
280
+ refs: [],
281
+ artifacts: {
282
+ body: {
283
+ mediaType:
284
+ texture.format === 'r8unorm'
285
+ ? 'application/x-forgeax-r8'
286
+ : `application/x-forgeax-${texture.format}`,
287
+ assetCodec: { name: texture.format, version: '1' },
288
+ bytes: bytes(texture.data),
289
+ },
290
+ },
291
+ };
292
+ }
293
+
249
294
  function ordinaryPodProduct(input: AssetOutputInput): AssetOutputProduct {
250
295
  const asset = input.asset;
251
296
  switch (asset.kind) {
252
- case 'texture': {
253
- const texture = asset as TextureAsset;
254
- return {
255
- payload: texture,
256
- refs: [],
257
- artifacts: {
258
- body: {
259
- mediaType: 'image/raw',
260
- assetCodec: { name: 'raw-image', version: '1' },
261
- bytes: bytes(texture.data),
262
- },
263
- },
264
- };
265
- }
297
+ case 'texture':
298
+ return textureProduct(input);
266
299
  case 'equirect': {
267
300
  const equirect = asset as EquirectAsset;
268
301
  return {
@@ -418,6 +451,11 @@ export const materialAssetOutputProducer = createSafeProducer(
418
451
  materialProduct,
419
452
  );
420
453
  export const meshAssetOutputProducer = createSafeProducer('mesh', 'mesh-binary/4', meshProduct);
454
+ export const textureAssetOutputProducer = createSafeProducer(
455
+ 'texture',
456
+ 'texture-pack/1',
457
+ textureProduct,
458
+ );
421
459
 
422
460
  export function createSceneAssetOutputProducer(
423
461
  sceneComponents: readonly ScriptablePackSceneComponent[] = [],
@@ -434,9 +472,9 @@ export function createStandardAssetOutputProducerRegistry(
434
472
  const registry = new AssetOutputProducerRegistry();
435
473
  registry.register(materialAssetOutputProducer);
436
474
  registry.register(meshAssetOutputProducer);
475
+ registry.register(textureAssetOutputProducer);
437
476
  registry.register(createSceneAssetOutputProducer(sceneComponents));
438
477
  for (const kind of [
439
- 'texture',
440
478
  'equirect',
441
479
  'sampler',
442
480
  'font',