@forgeax/engine-image 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.
- package/README.md +47 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts +2 -0
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts.map +1 -0
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +220 -15
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/asset-decoders.d.ts.map +1 -1
- package/dist/texture/importer.d.ts +42 -0
- package/dist/texture/importer.d.ts.map +1 -0
- package/dist/texture/source-descriptor.d.ts +24 -0
- package/dist/texture/source-descriptor.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/image-importer-conversion-failure.unit.test.ts +10 -2
- package/src/__tests__/image-importer-topology.unit.test.ts +2 -2
- package/src/__tests__/image.unit.test.ts +2 -4
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +2 -3
- package/src/__tests__/texture-source-descriptor.integration.test.ts +117 -0
- package/src/image-importer.ts +24 -14
- package/src/runtime/asset-decoders.ts +40 -11
- package/src/texture/importer.ts +198 -0
- package/src/texture/source-descriptor.ts +123 -0
package/dist/image-importer.mjs
CHANGED
|
@@ -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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
-
|
|
817
|
-
|
|
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
|
-
|
|
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,
|