@forgeax/engine-image 0.1.27 → 0.1.29
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 +12 -2
- package/dist/__tests__/cube-parser.unit.test.d.ts +2 -0
- package/dist/__tests__/cube-parser.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cube-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/cube-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/cube-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/cube-recovery.integration.test.d.ts.map +1 -0
- package/dist/decode-image-from-file.d.ts +1 -1
- package/dist/decode-image-from-file.mjs.map +1 -1
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/hdr-decoder.mjs.map +1 -1
- package/dist/image-importer.d.ts +2 -0
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +286 -2
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lut/cube-parser.d.ts +18 -0
- package/dist/lut/cube-parser.d.ts.map +1 -0
- package/dist/lut/cube-producer.d.ts +17 -0
- package/dist/lut/cube-producer.d.ts.map +1 -0
- package/dist/parse-image.mjs.map +1 -1
- package/dist/to-asset-pack.d.ts +1 -1
- package/package.json +6 -6
- package/src/__tests__/cube-parser.unit.test.ts +84 -0
- package/src/__tests__/cube-producer.integration.test.ts +94 -0
- package/src/__tests__/cube-recovery.integration.test.ts +52 -0
- package/src/__tests__/errors.test-d.ts +2 -2
- package/src/__tests__/image-importer-topology.unit.test.ts +27 -0
- package/src/__tests__/image.unit.test.ts +1 -1
- package/src/decode-image-from-file.ts +1 -1
- package/src/errors.ts +35 -0
- package/src/image-importer.ts +13 -1
- package/src/lut/cube-parser.ts +185 -0
- package/src/lut/cube-producer.ts +135 -0
- package/src/to-asset-pack.ts +1 -1
package/dist/image-importer.mjs
CHANGED
|
@@ -16,6 +16,15 @@ var __export = (target, all) => {
|
|
|
16
16
|
function imageError(detail) {
|
|
17
17
|
return new ImageErrorImpl(detail);
|
|
18
18
|
}
|
|
19
|
+
function cubeParserError(code, sourceKey, field, actual, line) {
|
|
20
|
+
const lineDetail = line === void 0 ? "" : ` at line ${line}`;
|
|
21
|
+
return {
|
|
22
|
+
code,
|
|
23
|
+
expected: `valid .cube source with LUT_3D_SIZE, DOMAIN_MIN/MAX, and exactly N^3 RGB rows${lineDetail}`,
|
|
24
|
+
hint: `repair field ${field} in sourceKey ${sourceKey} and re-run the Node image producer`,
|
|
25
|
+
detail: { sourceKey, field, actual, ...line === void 0 ? {} : { line } }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
19
28
|
var IMAGE_ERROR_EXPECTED, ImageErrorImpl;
|
|
20
29
|
var init_errors = __esm({
|
|
21
30
|
"src/errors.ts"() {
|
|
@@ -331,6 +340,278 @@ async function encodeTextureToKtx2(pixels, width, height, mode, source) {
|
|
|
331
340
|
return { ok: true, value: { ktx2: result.value, mode: resolved } };
|
|
332
341
|
}
|
|
333
342
|
|
|
343
|
+
// src/lut/cube-parser.ts
|
|
344
|
+
init_errors();
|
|
345
|
+
var ALLOWED_SIZES = /* @__PURE__ */ new Set([16, 32, 64]);
|
|
346
|
+
function finite(value) {
|
|
347
|
+
return Number.isFinite(value);
|
|
348
|
+
}
|
|
349
|
+
function parseTriple(tokens) {
|
|
350
|
+
if (tokens.length !== 3) return void 0;
|
|
351
|
+
const values = tokens.map(Number);
|
|
352
|
+
if (!values.every(finite)) return void 0;
|
|
353
|
+
const [red, green, blue] = values;
|
|
354
|
+
if (red === void 0 || green === void 0 || blue === void 0) return void 0;
|
|
355
|
+
return [red, green, blue];
|
|
356
|
+
}
|
|
357
|
+
function float32ToFloat16(value) {
|
|
358
|
+
const input = new Float32Array([value]);
|
|
359
|
+
const bits = new Uint32Array(input.buffer).at(0) ?? 0;
|
|
360
|
+
const sign = bits >>> 16 & 32768;
|
|
361
|
+
const exponent = bits >>> 23 & 255;
|
|
362
|
+
const fraction = bits & 8388607;
|
|
363
|
+
if (exponent === 255) return sign | (fraction === 0 ? 31744 : 32256);
|
|
364
|
+
const halfExponent = exponent - 127 + 15;
|
|
365
|
+
if (halfExponent >= 31) return sign | 31744;
|
|
366
|
+
if (halfExponent <= 0) {
|
|
367
|
+
if (halfExponent < -10) return sign;
|
|
368
|
+
const mantissa = (fraction | 8388608) >>> 1 - halfExponent;
|
|
369
|
+
return sign | mantissa + 4096 >>> 13;
|
|
370
|
+
}
|
|
371
|
+
return sign | halfExponent << 10 | fraction + 4096 >>> 13;
|
|
372
|
+
}
|
|
373
|
+
function parseCubeLut(source, sourceKey = "<inline .cube>") {
|
|
374
|
+
let size;
|
|
375
|
+
let domainMin;
|
|
376
|
+
let domainMax;
|
|
377
|
+
const values = [];
|
|
378
|
+
const lines = source.split(/\r?\n/);
|
|
379
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
380
|
+
const line = rawLine.replace(/#.*/, "").trim();
|
|
381
|
+
const lineNumber = index + 1;
|
|
382
|
+
if (line.length === 0) continue;
|
|
383
|
+
const tokens = line.split(/\s+/);
|
|
384
|
+
const directive = tokens.shift();
|
|
385
|
+
if (directive === void 0) continue;
|
|
386
|
+
if (directive === "TITLE") continue;
|
|
387
|
+
if (directive === "LUT_3D_SIZE") {
|
|
388
|
+
const sizeToken = tokens[0];
|
|
389
|
+
if (size !== void 0 || tokens.length !== 1 || sizeToken === void 0 || !/^\d+$/.test(sizeToken)) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
error: cubeParserError("cube-size-invalid", sourceKey, "LUT_3D_SIZE", line, lineNumber)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
size = Number(sizeToken);
|
|
396
|
+
if (!ALLOWED_SIZES.has(size)) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
error: cubeParserError("cube-size-invalid", sourceKey, "LUT_3D_SIZE", size, lineNumber)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (directive === "DOMAIN_MIN" || directive === "DOMAIN_MAX") {
|
|
405
|
+
const triple = parseTriple(tokens);
|
|
406
|
+
if (triple === void 0) {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
error: cubeParserError("cube-domain-invalid", sourceKey, directive, line, lineNumber)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (directive === "DOMAIN_MIN") {
|
|
413
|
+
if (domainMin !== void 0)
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: cubeParserError("cube-domain-invalid", sourceKey, directive, line, lineNumber)
|
|
417
|
+
};
|
|
418
|
+
domainMin = triple;
|
|
419
|
+
} else {
|
|
420
|
+
if (domainMax !== void 0)
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
error: cubeParserError("cube-domain-invalid", sourceKey, directive, line, lineNumber)
|
|
424
|
+
};
|
|
425
|
+
domainMax = triple;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (directive === "LUT_1D_SIZE") {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: cubeParserError("cube-header-invalid", sourceKey, directive, line, lineNumber)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const row = parseTriple([directive, ...tokens]);
|
|
436
|
+
if (row === void 0 || row.some((value) => value < 0 || value > 1)) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
error: cubeParserError("cube-row-invalid", sourceKey, "RGB row", line, lineNumber)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
values.push(...row);
|
|
443
|
+
}
|
|
444
|
+
if (size === void 0)
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
error: cubeParserError("cube-header-invalid", sourceKey, "LUT_3D_SIZE", "missing")
|
|
448
|
+
};
|
|
449
|
+
if (domainMin === void 0 || domainMax === void 0) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
error: cubeParserError("cube-domain-invalid", sourceKey, "DOMAIN_MIN/MAX", "missing")
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (domainMin.some((value, index) => {
|
|
456
|
+
const maxValue = domainMax[index];
|
|
457
|
+
return maxValue !== void 0 && value >= maxValue;
|
|
458
|
+
})) {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
error: cubeParserError("cube-domain-invalid", sourceKey, "DOMAIN_MIN/MAX", {
|
|
462
|
+
domainMin,
|
|
463
|
+
domainMax
|
|
464
|
+
})
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const expectedValues = size ** 3 * 3;
|
|
468
|
+
if (values.length !== expectedValues) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
error: cubeParserError("cube-data-count-invalid", sourceKey, "RGB rows", {
|
|
472
|
+
expected: expectedValues / 3,
|
|
473
|
+
actual: values.length / 3
|
|
474
|
+
})
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
value: {
|
|
480
|
+
size,
|
|
481
|
+
domainMin,
|
|
482
|
+
domainMax,
|
|
483
|
+
values: new Float32Array(values),
|
|
484
|
+
sourceKey
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function cubeLutBytes(lut) {
|
|
489
|
+
const bytes = new Uint8Array(lut.size ** 3 * 4 * 2);
|
|
490
|
+
const view = new DataView(bytes.buffer);
|
|
491
|
+
for (let index = 0; index < lut.size ** 3; index += 1) {
|
|
492
|
+
const valueOffset = index * 3;
|
|
493
|
+
const byteOffset = index * 8;
|
|
494
|
+
view.setUint16(byteOffset, float32ToFloat16(lut.values[valueOffset] ?? 0), true);
|
|
495
|
+
view.setUint16(byteOffset + 2, float32ToFloat16(lut.values[valueOffset + 1] ?? 0), true);
|
|
496
|
+
view.setUint16(byteOffset + 4, float32ToFloat16(lut.values[valueOffset + 2] ?? 0), true);
|
|
497
|
+
view.setUint16(byteOffset + 6, 15360, true);
|
|
498
|
+
}
|
|
499
|
+
return bytes;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/lut/cube-producer.ts
|
|
503
|
+
function produceCubeTexture(input, source) {
|
|
504
|
+
const parsed = parseCubeLut(source, input.sourceKey);
|
|
505
|
+
if (!parsed.ok) return parsed;
|
|
506
|
+
const bytes = cubeLutBytes(parsed.value);
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
value: {
|
|
510
|
+
guid: input.guid,
|
|
511
|
+
kind: "texture",
|
|
512
|
+
payload: {
|
|
513
|
+
kind: "texture",
|
|
514
|
+
shape: {
|
|
515
|
+
viewDimension: "3d",
|
|
516
|
+
extent: { width: parsed.value.size, height: parsed.value.size, depth: parsed.value.size }
|
|
517
|
+
},
|
|
518
|
+
format: "rgba16float",
|
|
519
|
+
data: bytes,
|
|
520
|
+
colorSpace: "linear",
|
|
521
|
+
mips: { kind: "none" }
|
|
522
|
+
},
|
|
523
|
+
refs: [],
|
|
524
|
+
artifacts: {
|
|
525
|
+
body: {
|
|
526
|
+
mediaType: "application/x-forgeax-rgba16float",
|
|
527
|
+
assetCodec: { name: "rgba16float", version: "1" },
|
|
528
|
+
bytes
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async function importCubeSource(ctx) {
|
|
535
|
+
const subAsset = ctx.subAssets.length === 1 ? ctx.subAssets[0] : void 0;
|
|
536
|
+
if (subAsset === void 0 || subAsset.kind !== "texture" || subAsset.sourceIndex !== 0) {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: new ImportError({
|
|
540
|
+
code: "source-validation-failed",
|
|
541
|
+
expected: "one texture subAsset at sourceIndex 0",
|
|
542
|
+
actual: JSON.stringify(ctx.subAssets),
|
|
543
|
+
hint: IMPORT_ERROR_HINTS["source-validation-failed"],
|
|
544
|
+
detail: {
|
|
545
|
+
diagnostics: [
|
|
546
|
+
{
|
|
547
|
+
code: "cube-subasset-topology",
|
|
548
|
+
severity: "error",
|
|
549
|
+
sourcePath: `${ctx.source}#subAssets`,
|
|
550
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
551
|
+
rule: "cube-required-single-texture",
|
|
552
|
+
expected: "one texture subAsset at sourceIndex 0",
|
|
553
|
+
actual: JSON.stringify(ctx.subAssets),
|
|
554
|
+
hint: "declare one ordinary texture subAsset and retry the same sourceKey"
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const source = await ctx.readSource();
|
|
562
|
+
if (!source.ok) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: new ImportError({
|
|
566
|
+
code: "source-read-failed",
|
|
567
|
+
expected: `readable .cube source at "${ctx.source}"`,
|
|
568
|
+
actual: String(source.error),
|
|
569
|
+
hint: IMPORT_ERROR_HINTS["source-read-failed"],
|
|
570
|
+
detail: { source: ctx.source, reason: String(source.error) }
|
|
571
|
+
})
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const produced = produceCubeTexture(
|
|
575
|
+
{
|
|
576
|
+
source: ctx.source,
|
|
577
|
+
sourceKey: subAsset.sourceKey ?? `${ctx.source}:texture`,
|
|
578
|
+
guid: subAsset.guid
|
|
579
|
+
},
|
|
580
|
+
new TextDecoder().decode(source.value)
|
|
581
|
+
);
|
|
582
|
+
if (!produced.ok) {
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
error: new ImportError({
|
|
586
|
+
code: "source-validation-failed",
|
|
587
|
+
expected: produced.error.expected,
|
|
588
|
+
actual: JSON.stringify(produced.error.detail),
|
|
589
|
+
hint: produced.error.hint,
|
|
590
|
+
detail: {
|
|
591
|
+
diagnostics: [
|
|
592
|
+
{
|
|
593
|
+
code: produced.error.code,
|
|
594
|
+
severity: "error",
|
|
595
|
+
sourcePath: `${ctx.source}#${produced.error.detail.field}`,
|
|
596
|
+
sourceRange: {
|
|
597
|
+
start: 0,
|
|
598
|
+
end: 0,
|
|
599
|
+
line: produced.error.detail.line ?? 1,
|
|
600
|
+
column: 1
|
|
601
|
+
},
|
|
602
|
+
rule: "cube-source-parser",
|
|
603
|
+
expected: produced.error.expected,
|
|
604
|
+
actual: JSON.stringify(produced.error.detail.actual),
|
|
605
|
+
hint: produced.error.hint
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return { ok: true, value: { assets: [produced.value], sourceDependencies: [ctx.source] } };
|
|
613
|
+
}
|
|
614
|
+
|
|
334
615
|
// src/parse-image.ts
|
|
335
616
|
init_errors();
|
|
336
617
|
|
|
@@ -655,7 +936,7 @@ function mimeFromSource(source) {
|
|
|
655
936
|
function requiredImageOutputKind(source) {
|
|
656
937
|
const lower = source.toLowerCase();
|
|
657
938
|
if (lower.endsWith(".hdr")) return "equirect";
|
|
658
|
-
if (mimeFromSource(source) !== void 0 || lower.endsWith(".basis") || lower.endsWith(".ktx2")) {
|
|
939
|
+
if (mimeFromSource(source) !== void 0 || lower.endsWith(".basis") || lower.endsWith(".ktx2") || lower.endsWith(".cube")) {
|
|
659
940
|
return "texture";
|
|
660
941
|
}
|
|
661
942
|
return void 0;
|
|
@@ -1075,6 +1356,9 @@ async function importImage(ctx) {
|
|
|
1075
1356
|
if (ctx.source.toLowerCase().endsWith(".texture.json")) {
|
|
1076
1357
|
return importTextureSource(ctx);
|
|
1077
1358
|
}
|
|
1359
|
+
if (ctx.source.toLowerCase().endsWith(".cube")) {
|
|
1360
|
+
return importCubeSource(ctx);
|
|
1361
|
+
}
|
|
1078
1362
|
const requiredKind = requiredImageOutputKind(ctx.source);
|
|
1079
1363
|
if (requiredKind !== void 0) {
|
|
1080
1364
|
const topologyError = validateImageOutputTopology(ctx, requiredKind);
|
|
@@ -1284,6 +1568,6 @@ var imageImporter = {
|
|
|
1284
1568
|
capabilities: { decodeImage: decodeImageForImport }
|
|
1285
1569
|
};
|
|
1286
1570
|
|
|
1287
|
-
export { decodeImageForImport, imageImporter };
|
|
1571
|
+
export { cubeLutBytes, decodeImageForImport, imageImporter, importCubeSource, parseCubeLut, produceCubeTexture };
|
|
1288
1572
|
//# sourceMappingURL=image-importer.mjs.map
|
|
1289
1573
|
//# sourceMappingURL=image-importer.mjs.map
|