@forgeax/engine-image 0.1.19 → 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.
Files changed (42) hide show
  1. package/README.md +61 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/pixel-surface.unit.test.d.ts +2 -0
  4. package/dist/__tests__/pixel-surface.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/texture-source-descriptor.integration.test.d.ts +2 -0
  6. package/dist/__tests__/texture-source-descriptor.integration.test.d.ts.map +1 -0
  7. package/dist/decode-image-from-file.mjs +2 -1
  8. package/dist/decode-image-from-file.mjs.map +1 -1
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/hdr-decoder.mjs +2 -1
  11. package/dist/hdr-decoder.mjs.map +1 -1
  12. package/dist/image-importer.d.ts.map +1 -1
  13. package/dist/image-importer.mjs +222 -16
  14. package/dist/image-importer.mjs.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.mjs +341 -43
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/parse-image.mjs +2 -1
  20. package/dist/parse-image.mjs.map +1 -1
  21. package/dist/pixel-surface.d.ts +53 -0
  22. package/dist/pixel-surface.d.ts.map +1 -0
  23. package/dist/runtime/asset-decoders.d.ts.map +1 -1
  24. package/dist/texture/importer.d.ts +42 -0
  25. package/dist/texture/importer.d.ts.map +1 -0
  26. package/dist/texture/source-descriptor.d.ts +24 -0
  27. package/dist/texture/source-descriptor.d.ts.map +1 -0
  28. package/package.json +6 -6
  29. package/src/__tests__/errors.test-d.ts +11 -3
  30. package/src/__tests__/image-importer-conversion-failure.unit.test.ts +31 -2
  31. package/src/__tests__/image-importer-topology.unit.test.ts +2 -2
  32. package/src/__tests__/image.unit.test.ts +2 -4
  33. package/src/__tests__/ktx2-basis-importer.unit.test.ts +2 -3
  34. package/src/__tests__/pixel-surface.unit.test.ts +94 -0
  35. package/src/__tests__/texture-source-descriptor.integration.test.ts +117 -0
  36. package/src/errors.ts +2 -0
  37. package/src/image-importer.ts +24 -14
  38. package/src/index.ts +8 -0
  39. package/src/pixel-surface.ts +428 -0
  40. package/src/runtime/asset-decoders.ts +40 -11
  41. package/src/texture/importer.ts +198 -0
  42. package/src/texture/source-descriptor.ts +123 -0
@@ -1,4 +1,4 @@
1
- import { err, ok, ImportError, IMPORT_ERROR_HINTS, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
1
+ import { err, ok, ImportError, IMPORT_ERROR_HINTS, deriveTextureLayout, validateTextureShape, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
2
2
  import { parseKtx2, ktx2ColorSpace } from '@forgeax/engine-codec';
3
3
  import { basisEncode } from '@forgeax/engine-codec/encode';
4
4
  import * as jpeg from 'jpeg-js';
@@ -34,7 +34,8 @@ var init_errors = __esm({
34
34
  // .hint after switch (err.code) without parsing the message).
35
35
  "atlas-empty-input": "images.length >= 1",
36
36
  "atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
37
- "atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
37
+ "atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight",
38
+ "image-surface-invalid": "PixelSurface dimensions and authoring inputs satisfy the RGBA8 contract"
38
39
  };
39
40
  ImageErrorImpl = class extends Error {
40
41
  code;
@@ -441,6 +442,208 @@ function parseImage(bytes, mime, opts = {}) {
441
442
  mipmap: opts.mipmap ?? true
442
443
  });
443
444
  }
445
+ function record(value) {
446
+ return value !== null && typeof value === "object" && !Array.isArray(value);
447
+ }
448
+ function positiveInteger(value) {
449
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
450
+ }
451
+ function parseShape(value) {
452
+ if (!record(value) || !record(value.extent)) return void 0;
453
+ const extent = value.extent;
454
+ if (value.viewDimension === "2d" && positiveInteger(extent.width) && positiveInteger(extent.height)) {
455
+ if (extent.layers === void 0 && extent.depth === void 0) {
456
+ return { viewDimension: "2d", extent: { width: extent.width, height: extent.height } };
457
+ }
458
+ }
459
+ if (value.viewDimension === "2d-array" && positiveInteger(extent.width) && positiveInteger(extent.height) && positiveInteger(extent.layers) && extent.depth === void 0) {
460
+ return {
461
+ viewDimension: "2d-array",
462
+ extent: { width: extent.width, height: extent.height, layers: extent.layers }
463
+ };
464
+ }
465
+ if (value.viewDimension === "3d" && positiveInteger(extent.width) && positiveInteger(extent.height) && positiveInteger(extent.depth) && extent.layers === void 0) {
466
+ return {
467
+ viewDimension: "3d",
468
+ extent: { width: extent.width, height: extent.height, depth: extent.depth }
469
+ };
470
+ }
471
+ return void 0;
472
+ }
473
+ function parseMips(value) {
474
+ if (!record(value)) return void 0;
475
+ if (value.kind === "none" || value.kind === "generate") return { kind: value.kind };
476
+ if (value.kind === "packed" && positiveInteger(value.levelCount)) {
477
+ return { kind: "packed", levelCount: value.levelCount };
478
+ }
479
+ return void 0;
480
+ }
481
+ function descriptorError(field, actual) {
482
+ return {
483
+ code: "texture-source-descriptor-invalid",
484
+ expected: "version 1 descriptor with shape, format, colorSpace, mips, and rawSibling",
485
+ hint: "repair the texture descriptor and re-import the same texture GUID",
486
+ detail: { field, actual }
487
+ };
488
+ }
489
+ function parseTextureSourceDescriptor(value) {
490
+ if (!record(value)) return err(descriptorError("descriptor", value));
491
+ if (value.schemaVersion !== "1")
492
+ return err(descriptorError("schemaVersion", value.schemaVersion));
493
+ const shape = parseShape(value.shape);
494
+ if (shape === void 0) return err(descriptorError("shape", value.shape));
495
+ const mips = parseMips(value.mips);
496
+ if (mips === void 0) return err(descriptorError("mips", value.mips));
497
+ if (typeof value.format !== "string") return err(descriptorError("format", value.format));
498
+ if (value.colorSpace !== "srgb" && value.colorSpace !== "linear") {
499
+ return err(descriptorError("colorSpace", value.colorSpace));
500
+ }
501
+ if (typeof value.rawSibling !== "string" || value.rawSibling.trim().length === 0) {
502
+ return err(descriptorError("rawSibling", value.rawSibling));
503
+ }
504
+ const shapeResult = validateTextureShape(shape, mips, value.format);
505
+ if (!shapeResult.ok) return shapeResult;
506
+ return ok({
507
+ schemaVersion: "1",
508
+ shape,
509
+ format: value.format,
510
+ colorSpace: value.colorSpace,
511
+ mips,
512
+ rawSibling: value.rawSibling
513
+ });
514
+ }
515
+
516
+ // src/texture/importer.ts
517
+ function sourceReadError(input, reason) {
518
+ return {
519
+ code: "source-read-failed",
520
+ expected: `readable raw sibling "${input.descriptor && typeof input.descriptor === "object" && "rawSibling" in input.descriptor ? input.descriptor.rawSibling : "rawSibling"}"`,
521
+ hint: "repair the raw sibling path or bytes and re-import the same texture GUID",
522
+ detail: {
523
+ sourceKey: input.sourceKey,
524
+ sibling: input.descriptor && typeof input.descriptor === "object" && "rawSibling" in input.descriptor ? String(input.descriptor.rawSibling) : "rawSibling",
525
+ reason: reason instanceof Error ? reason.message : String(reason)
526
+ }
527
+ };
528
+ }
529
+ function bodyMediaType(format) {
530
+ return format === "r8unorm" ? "application/x-forgeax-r8" : `application/x-forgeax-${format}`;
531
+ }
532
+ async function produceTextureSource(input) {
533
+ const descriptorResult = parseTextureSourceDescriptor(input.descriptor);
534
+ if (!descriptorResult.ok) return descriptorResult;
535
+ const descriptor = descriptorResult.value;
536
+ const sibling = await input.readSibling(descriptor.rawSibling);
537
+ if (!sibling.ok) return err(sourceReadError(input, sibling.error));
538
+ const layout = deriveTextureLayout({
539
+ shape: descriptor.shape,
540
+ format: descriptor.format,
541
+ mips: descriptor.mips,
542
+ actualByteLength: sibling.value.byteLength,
543
+ order: "mip-major,image-major,row-major"
544
+ });
545
+ if (!layout.ok) return layout;
546
+ const data = new Uint8Array(sibling.value);
547
+ return ok({
548
+ guid: input.guid,
549
+ kind: "texture",
550
+ payload: {
551
+ kind: "texture",
552
+ shape: descriptor.shape,
553
+ format: descriptor.format,
554
+ colorSpace: descriptor.colorSpace,
555
+ mips: descriptor.mips,
556
+ data
557
+ },
558
+ refs: [],
559
+ artifacts: {
560
+ body: {
561
+ mediaType: bodyMediaType(descriptor.format),
562
+ assetCodec: { name: descriptor.format, version: "1" },
563
+ bytes: data
564
+ }
565
+ }
566
+ });
567
+ }
568
+ function sourceValidationError(ctx, error) {
569
+ const detail = "detail" in error ? error.detail : { field: "descriptor", actual: error };
570
+ return new ImportError({
571
+ code: "source-validation-failed",
572
+ expected: error.expected,
573
+ hint: error.hint,
574
+ detail: {
575
+ diagnostics: [
576
+ {
577
+ code: `texture-source-${error.code}`,
578
+ severity: "error",
579
+ sourcePath: `${ctx.source}#${"field" in detail ? detail.field : "rawSibling"}`,
580
+ sourceRange: { start: 0, end: 0, line: 1, column: 1 },
581
+ rule: "texture-source-descriptor",
582
+ expected: error.expected,
583
+ actual: JSON.stringify(detail),
584
+ hint: error.hint
585
+ }
586
+ ]
587
+ }
588
+ });
589
+ }
590
+ async function importTextureSource(ctx) {
591
+ const source = await ctx.readSource();
592
+ if (!source.ok) {
593
+ const reason = String(source.error);
594
+ return {
595
+ ok: false,
596
+ error: new ImportError({
597
+ code: "source-read-failed",
598
+ expected: `readable texture descriptor at "${ctx.source}"`,
599
+ hint: "repair the texture descriptor path and retry the import",
600
+ detail: { source: ctx.source, reason }
601
+ })
602
+ };
603
+ }
604
+ let descriptor;
605
+ try {
606
+ descriptor = JSON.parse(new TextDecoder().decode(source.value));
607
+ } catch (error) {
608
+ return {
609
+ ok: false,
610
+ error: sourceValidationError(ctx, {
611
+ code: "texture-source-descriptor-invalid",
612
+ expected: "JSON texture source descriptor",
613
+ hint: "repair the descriptor JSON and retry the import",
614
+ detail: {
615
+ field: "descriptor",
616
+ actual: error instanceof Error ? error.message : String(error)
617
+ }
618
+ })
619
+ };
620
+ }
621
+ const subAsset = ctx.subAssets.length === 1 ? ctx.subAssets[0] : void 0;
622
+ if (subAsset === void 0 || subAsset.kind !== "texture" || subAsset.sourceIndex !== 0) {
623
+ return {
624
+ ok: false,
625
+ error: sourceValidationError(ctx, {
626
+ code: "texture-source-descriptor-invalid",
627
+ expected: "one texture subAsset at sourceIndex 0",
628
+ hint: "repair Meta subAssets and retry the same texture GUID",
629
+ detail: { field: "subAssets", actual: ctx.subAssets }
630
+ })
631
+ };
632
+ }
633
+ const produced = await produceTextureSource({
634
+ descriptor,
635
+ guid: subAsset.guid,
636
+ sourceKey: subAsset.sourceKey ?? `${ctx.source}:texture`,
637
+ readSibling: ctx.readSibling
638
+ });
639
+ if (!produced.ok) return { ok: false, error: sourceValidationError(ctx, produced.error) };
640
+ const parsed = parseTextureSourceDescriptor(descriptor);
641
+ const sibling = parsed.ok ? parsed.value.rawSibling : ctx.source;
642
+ return {
643
+ ok: true,
644
+ value: { assets: [produced.value], sourceDependencies: [ctx.source, sibling] }
645
+ };
646
+ }
444
647
 
445
648
  // src/image-importer.ts
446
649
  function mimeFromSource(source) {
@@ -658,13 +861,14 @@ async function importKtx2Source(ctx, bytes) {
658
861
  kind: "texture",
659
862
  payload: {
660
863
  kind: "texture",
661
- width: inspection.width,
662
- height: inspection.height,
864
+ shape: {
865
+ viewDimension: "2d",
866
+ extent: { width: inspection.width, height: inspection.height }
867
+ },
663
868
  format: colorSpaceToFormat(inspection.colorSpace),
664
869
  data: bytes,
665
870
  colorSpace: inspection.colorSpace,
666
- mipmap: inspection.levelCount > 1,
667
- mipLevelCount: inspection.levelCount
871
+ mips: inspection.levelCount > 1 ? { kind: "packed", levelCount: inspection.levelCount } : { kind: "none" }
668
872
  },
669
873
  refs: [],
670
874
  artifacts: {
@@ -813,13 +1017,14 @@ async function importBasisSource(ctx, bytes) {
813
1017
  kind: "texture",
814
1018
  payload: {
815
1019
  kind: "texture",
816
- width: inspection.width,
817
- height: inspection.height,
1020
+ shape: {
1021
+ viewDimension: "2d",
1022
+ extent: { width: inspection.width, height: inspection.height }
1023
+ },
818
1024
  format: colorSpaceToFormat(colorSpace),
819
1025
  data: bytes,
820
1026
  colorSpace,
821
- mipmap: inspection.levelCount > 1,
822
- mipLevelCount: inspection.levelCount
1027
+ mips: inspection.levelCount > 1 ? { kind: "packed", levelCount: inspection.levelCount } : { kind: "none" }
823
1028
  },
824
1029
  refs: [],
825
1030
  artifacts: {
@@ -869,6 +1074,9 @@ async function maybeEncodeTextureBytes(ctx, pixels, width, height, compressionMo
869
1074
  return { ok: true, value: result.value.ktx2 };
870
1075
  }
871
1076
  async function importImage(ctx) {
1077
+ if (ctx.source.toLowerCase().endsWith(".texture.json")) {
1078
+ return importTextureSource(ctx);
1079
+ }
872
1080
  const requiredKind = requiredImageOutputKind(ctx.source);
873
1081
  if (requiredKind !== void 0) {
874
1082
  const topologyError = validateImageOutputTopology(ctx, requiredKind);
@@ -990,12 +1198,11 @@ async function importImage(ctx) {
990
1198
  if (sub.kind !== "texture") continue;
991
1199
  const payload = {
992
1200
  kind: "texture",
993
- width: dec.width,
994
- height: dec.height,
1201
+ shape: { viewDimension: "2d", extent: { width: dec.width, height: dec.height } },
995
1202
  format: colorSpaceToFormat(colorSpace),
996
1203
  data: encodedBytes ?? dec.bytes,
997
1204
  colorSpace,
998
- mipmap
1205
+ mips: mipmap ? { kind: "generate" } : { kind: "none" }
999
1206
  };
1000
1207
  out.push({
1001
1208
  guid: sub.guid,
@@ -1062,11 +1269,10 @@ var decodeImageForImport = async (bytes, mimeType, importSettings) => {
1062
1269
  texture: {
1063
1270
  kind: "texture",
1064
1271
  data: cookedBytes,
1065
- width: tex.width,
1066
- height: tex.height,
1272
+ shape: { viewDimension: "2d", extent: { width: tex.width, height: tex.height } },
1067
1273
  format: colorSpaceToFormat(colorSpace),
1068
1274
  colorSpace,
1069
- mipmap
1275
+ mips: mipmap ? { kind: "generate" } : { kind: "none" }
1070
1276
  },
1071
1277
  bytes: cookedBytes,
1072
1278
  mediaType,