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