@forgeax/engine-import 0.1.7 → 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.
Files changed (38) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/scriptable-pack-texture.unit.test.d.ts +2 -0
  3. package/dist/__tests__/scriptable-pack-texture.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/source-package-errors.unit.test.d.ts +2 -0
  5. package/dist/__tests__/source-package-errors.unit.test.d.ts.map +1 -0
  6. package/dist/browser.mjs +5 -2
  7. package/dist/browser.mjs.map +1 -1
  8. package/dist/build-production.d.ts.map +1 -1
  9. package/dist/catalog-inventory.d.ts +1 -1
  10. package/dist/catalog-inventory.d.ts.map +1 -1
  11. package/dist/import-runner.d.ts +9 -0
  12. package/dist/import-runner.d.ts.map +1 -1
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.mjs +140 -45
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/scriptable-pack-host.d.ts +1 -1
  18. package/dist/scriptable-pack-host.d.ts.map +1 -1
  19. package/dist/scriptable-pack-output-producers.d.ts +1 -0
  20. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  21. package/dist/source-package-errors.d.ts +2 -0
  22. package/dist/source-package-errors.d.ts.map +1 -1
  23. package/dist/source-path.d.ts +12 -0
  24. package/dist/source-path.d.ts.map +1 -0
  25. package/package.json +7 -7
  26. package/src/__tests__/import-dependencies.test.ts +34 -0
  27. package/src/__tests__/scriptable-pack-product.contract.test.ts +4 -5
  28. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +4 -5
  29. package/src/__tests__/scriptable-pack-texture.unit.test.ts +96 -0
  30. package/src/__tests__/source-package-errors.unit.test.ts +62 -0
  31. package/src/build-production.ts +38 -14
  32. package/src/catalog-inventory.ts +2 -0
  33. package/src/import-runner.ts +15 -2
  34. package/src/index.ts +6 -0
  35. package/src/scriptable-pack-host.ts +6 -2
  36. package/src/scriptable-pack-output-producers.ts +54 -16
  37. package/src/source-package-errors.ts +37 -0
  38. package/src/source-path.ts +36 -0
@@ -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',
@@ -118,10 +118,45 @@ function importFailureCode(error: ImportError): SourcePackageErrorCode {
118
118
  }
119
119
  }
120
120
 
121
+ const IMPORT_ERROR_CODES = new Set([
122
+ 'importer-not-registered',
123
+ 'source-read-failed',
124
+ 'import-produced-no-assets',
125
+ 'guid-mismatch',
126
+ 'mesh-material-slot-topology-change',
127
+ 'import-internal-error',
128
+ 'source-validation-failed',
129
+ 'unknown-source-key',
130
+ 'duplicate-source-key',
131
+ 'invalid-source-overrides',
132
+ 'invalid-source-override-payload',
133
+ ]);
134
+
135
+ function findNormalizableCause(error: unknown, seen = new Set<unknown>()): unknown {
136
+ if (error === null || typeof error !== 'object' || seen.has(error)) return undefined;
137
+ seen.add(error);
138
+ if (isSourcePackageError(error) || isScriptablePackFailure(error) || isImportError(error)) {
139
+ return error;
140
+ }
141
+ const cause = (error as { readonly cause?: unknown }).cause;
142
+ return cause === undefined ? undefined : findNormalizableCause(cause, seen);
143
+ }
144
+
145
+ /** Return whether an error chain contains a source-package/import contract error. */
146
+ export function containsSourcePackageError(error: unknown, seen = new Set<unknown>()): boolean {
147
+ if (error === null || typeof error !== 'object' || seen.has(error)) return false;
148
+ seen.add(error);
149
+ if (isSourcePackageError(error) || isImportError(error)) return true;
150
+ const cause = (error as { readonly cause?: unknown }).cause;
151
+ return cause !== undefined && containsSourcePackageError(cause, seen);
152
+ }
153
+
121
154
  export function normalizeSourcePackageError(
122
155
  error: unknown,
123
156
  context: SourcePackageErrorContext,
124
157
  ): SourcePackageError {
158
+ const cause = findNormalizableCause(error);
159
+ if (cause !== undefined && cause !== error) return normalizeSourcePackageError(cause, context);
125
160
  if (isSourcePackageError(error)) return error;
126
161
  if (isScriptablePackFailure(error)) {
127
162
  return sourcePackageError('source-package-conversion-failed', context, {
@@ -174,6 +209,8 @@ function isImportError(error: unknown): error is ImportError {
174
209
  error !== null &&
175
210
  typeof error === 'object' &&
176
211
  'code' in error &&
212
+ typeof error.code === 'string' &&
213
+ IMPORT_ERROR_CODES.has(error.code) &&
177
214
  'expected' in error &&
178
215
  'hint' in error &&
179
216
  'detail' in error
@@ -0,0 +1,36 @@
1
+ import { resolve } from 'node:path';
2
+ import { catalogSourcePathFor } from '@forgeax/engine-pack/build';
3
+ import type { ScanSourceDeclaration } from '@forgeax/engine-pack/scanner';
4
+
5
+ function normalizeCatalogPath(path: string): string {
6
+ return path.replaceAll('\\', '/').replace(/^\.\//, '');
7
+ }
8
+
9
+ export interface CatalogSourceDeclaration {
10
+ readonly sourcePath: string;
11
+ readonly declaration: ScanSourceDeclaration;
12
+ }
13
+
14
+ /**
15
+ * Resolve a Catalog row's logical source locator back to its physical scanner
16
+ * declaration. Catalog rows are host identities; declaration maps remain
17
+ * physical because production still has to read those files from disk.
18
+ */
19
+ export function sourceDeclarationForCatalogPath(
20
+ sourcePath: string,
21
+ declarations: ReadonlyMap<string, ScanSourceDeclaration>,
22
+ sourceIdentityFor?: (sourcePath: string) => string,
23
+ cwd = process.cwd(),
24
+ ): CatalogSourceDeclaration | undefined {
25
+ const normalized = normalizeCatalogPath(sourcePath);
26
+ const direct = declarations.get(resolve(cwd, sourcePath));
27
+ if (direct !== undefined) {
28
+ return { sourcePath: resolve(cwd, sourcePath), declaration: direct };
29
+ }
30
+ for (const [physicalPath, declaration] of declarations) {
31
+ if (catalogSourcePathFor(cwd, physicalPath, sourceIdentityFor) === normalized) {
32
+ return { sourcePath: physicalPath, declaration };
33
+ }
34
+ }
35
+ return undefined;
36
+ }