@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
@@ -9,20 +9,21 @@ import type {
9
9
  } from '@forgeax/engine-types';
10
10
  import { describe, expectTypeOf, it } from 'vitest';
11
11
 
12
- // Type-only tests. Verify the 8-member ImageErrorCode closed union compiles
12
+ // Type-only tests. Verify the 9-member ImageErrorCode closed union compiles
13
13
  // an exhaustive switch without a default arm (charter P4 explicit failure).
14
14
  // Verifies the discriminated ImageErrorDetail narrowing per .code (charter
15
15
  // P3 machine-readable union > prose; AGENTS.md Error model row pattern).
16
16
  //
17
17
  // feat-20260521-sprite-atlas-animation M1 T-03 update: union grew from 5 to
18
18
  // 8 with the atlas-* triplet (atlas-empty-input / atlas-size-exceeded /
19
- // atlas-region-mismatch). The exact-member assertion + exhaustive switch
19
+ // atlas-region-mismatch); the 9th arm is the image-owned PixelSurface
20
+ // authoring error. The exact-member assertion + exhaustive switch
20
21
  // arms below mirror the SSOT in @forgeax/engine-types ImageErrorCode +
21
22
  // ImageErrorDetail so a future minor add still fails this file at compile
22
23
  // time until the new arm + .expected + .hint rows are supplied.
23
24
 
24
25
  describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () => {
25
- it('ImageErrorCode is the exact 8-member union literal set', () => {
26
+ it('ImageErrorCode is the exact 9-member union literal set', () => {
26
27
  expectTypeOf<ImageErrorCode>().toEqualTypeOf<
27
28
  | 'image-decode-failed'
28
29
  | 'image-format-unsupported'
@@ -32,6 +33,7 @@ describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () =
32
33
  | 'atlas-empty-input'
33
34
  | 'atlas-size-exceeded'
34
35
  | 'atlas-region-mismatch'
36
+ | 'image-surface-invalid'
35
37
  >();
36
38
  });
37
39
 
@@ -54,6 +56,8 @@ describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () =
54
56
  return 'downscale source or split atlas under maxAtlasSize cap';
55
57
  case 'atlas-region-mismatch':
56
58
  return 'shelfPack regions exceed atlas footprint -- packer safety net';
59
+ case 'image-surface-invalid':
60
+ return 'repair PixelSurface authoring input';
57
61
  }
58
62
  }
59
63
  expectTypeOf(recover).toBeFunction();
@@ -78,6 +82,8 @@ describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () =
78
82
  return `atlas-size:${detail.name}:${detail.width}x${detail.height}>${detail.maxAtlasSize}`;
79
83
  case 'atlas-region-mismatch':
80
84
  return `atlas-region:${detail.name}:${detail.regionsTotalPixels}>${detail.atlasPixels}`;
85
+ case 'image-surface-invalid':
86
+ return `surface:${detail.operation}:${detail.field}`;
81
87
  }
82
88
  }
83
89
  expectTypeOf(describe_).toBeFunction();
@@ -102,6 +108,8 @@ describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () =
102
108
  return `${error.detail.name}:${error.detail.maxAtlasSize}`;
103
109
  case 'atlas-region-mismatch':
104
110
  return `${error.detail.name}:${error.detail.atlasPixels}`;
111
+ case 'image-surface-invalid':
112
+ return `${error.detail.operation}:${error.detail.field}`;
105
113
  }
106
114
  }
107
115
 
@@ -61,6 +61,8 @@ function expectConversionFailure(
61
61
  code: diagnosticCode,
62
62
  rule: expect.stringMatching(/^image-conversion-/),
63
63
  severity: 'error',
64
+ expected: expect.any(String),
65
+ hint: expect.any(String),
64
66
  }),
65
67
  ],
66
68
  });
@@ -206,6 +208,25 @@ describe('image importer conversion failure through the public runner', () => {
206
208
  expect(state.reads).toBe(6);
207
209
  });
208
210
 
211
+ it.each([
212
+ { colorSpace: 'srgb' as const, format: 'rgba8unorm-srgb' },
213
+ { colorSpace: 'linear' as const, format: 'rgba8unorm' },
214
+ ])('publishes a format matching the authored $colorSpace color space', async (input) => {
215
+ const result = await runImport(
216
+ meta('valid.png', { colorSpace: input.colorSpace, mipmap: 'none' }),
217
+ registry(),
218
+ { readSource: source({ bytes: makePng(1, 1, [1, 2, 3, 255]), reads: 0 }) },
219
+ );
220
+
221
+ expect(result.ok).toBe(true);
222
+ if (!result.ok || 'skipped' in result.value) return;
223
+ expect(result.value.pack.assets[0]?.payload).toMatchObject({
224
+ colorSpace: input.colorSpace,
225
+ format: input.format,
226
+ mips: { kind: 'none' },
227
+ });
228
+ });
229
+
209
230
  it.skipIf(!pkgBuilt)('repairs raw Basis color-space settings in the same registry', async () => {
210
231
  const state = { bytes: await makeRawBasis(), reads: 0 };
211
232
  const importerRegistry = registry();
@@ -226,7 +247,11 @@ describe('image importer conversion failure through the public runner', () => {
226
247
  if (!repaired.ok || 'skipped' in repaired.value) return;
227
248
  expect(repaired.value.pack.assets[0]).toMatchObject({
228
249
  guid: GUID,
229
- payload: { colorSpace: 'linear', width: 4, height: 4 },
250
+ payload: {
251
+ colorSpace: 'linear',
252
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
253
+ mips: { kind: 'none' },
254
+ },
230
255
  artifacts: {
231
256
  body: {
232
257
  mediaType: 'application/x-forgeax-basis',
@@ -270,7 +295,11 @@ describe('image importer conversion failure through the public runner', () => {
270
295
  if (!repaired.ok || 'skipped' in repaired.value) return;
271
296
  expect(repaired.value.pack.assets[0]).toMatchObject({
272
297
  guid: GUID,
273
- payload: { colorSpace: 'linear', width: 4, height: 4, mipmap: false },
298
+ payload: {
299
+ colorSpace: 'linear',
300
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
301
+ mips: { kind: 'none' },
302
+ },
274
303
  artifacts: {
275
304
  body: {
276
305
  mediaType: 'image/ktx2',
@@ -172,10 +172,10 @@ describe('image importer required output topology', () => {
172
172
  sourceIndex: 0,
173
173
  payload: {
174
174
  kind: 'texture',
175
- width: 1,
176
- height: 1,
175
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
177
176
  format: 'rgba8unorm-srgb',
178
177
  colorSpace: 'srgb',
178
+ mips: { kind: 'none' },
179
179
  },
180
180
  artifacts: {
181
181
  body: {
@@ -601,8 +601,7 @@ import { makeCorruptPng, makeJpg, makePng } from './make-fixture.js';
601
601
 
602
602
  const payload = asset?.payload as unknown as Record<string, unknown>;
603
603
  expect(payload.kind).toBe('texture');
604
- expect(payload.width).toBe(8);
605
- expect(payload.height).toBe(4);
604
+ expect(payload.shape).toEqual({ viewDimension: '2d', extent: { width: 8, height: 4 } });
606
605
  expect(payload.format).toBe('rgba8unorm-srgb');
607
606
  expect(payload.colorSpace).toBe('srgb');
608
607
  expect(payload.data).toBeInstanceOf(Uint8Array);
@@ -859,8 +858,7 @@ import { makeCorruptPng, makeJpg, makePng } from './make-fixture.js';
859
858
 
860
859
  const payload = asset?.payload as TextureAsset;
861
860
  expect(payload.kind).toBe('texture');
862
- expect(payload.width).toBe(8);
863
- expect(payload.height).toBe(4);
861
+ expect(payload.shape).toEqual({ viewDimension: '2d', extent: { width: 8, height: 4 } });
864
862
  expect(payload.format).toBe('rgba8unorm-srgb');
865
863
  expect(payload.colorSpace).toBe('srgb');
866
864
  expect(payload.data.length).toBe(8 * 4 * 4);
@@ -97,11 +97,10 @@ describe.skipIf(!pkgBuilt)('KTX2/Basis source importer', () => {
97
97
  if (!result.ok) return;
98
98
  expect(result.value.assets[0]?.payload).toMatchObject({
99
99
  kind: 'texture',
100
- width: 4,
101
- height: 4,
100
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
102
101
  format: 'rgba8unorm',
103
102
  colorSpace: 'linear',
104
- mipmap: false,
103
+ mips: { kind: 'none' },
105
104
  });
106
105
  expect(result.value.assets[0]?.artifacts.body?.assetCodec).toMatchObject({
107
106
  name: 'basis',
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createPixelSurface, toAssetPack } from '../index.js';
3
+
4
+ const META = {
5
+ guid: '01928000-7c00-7000-8000-000000000099',
6
+ colorSpace: 'srgb' as const,
7
+ mipmap: 'none' as const,
8
+ addressMode: 'repeat' as const,
9
+ filterMode: 'nearest' as const,
10
+ };
11
+
12
+ function surface(width = 4, height = 3) {
13
+ const result = createPixelSurface({ width, height, colorSpace: 'srgb', mipmap: false });
14
+ expect(result.ok).toBe(true);
15
+ if (!result.ok) throw result.error;
16
+ return result.value;
17
+ }
18
+
19
+ describe('PixelSurface', () => {
20
+ it('rejects malformed dimensions and channel/seed values through ImageError', () => {
21
+ const badDimension = createPixelSurface({ width: 0, height: 2 });
22
+ expect(badDimension.ok).toBe(false);
23
+ if (!badDimension.ok) {
24
+ expect(badDimension.error.code).toBe('image-surface-invalid');
25
+ if (badDimension.error.code === 'image-surface-invalid') {
26
+ expect(badDimension.error.detail.expected).toContain('positive integer');
27
+ expect(badDimension.error.detail.code).toBe('image-surface-invalid');
28
+ }
29
+ }
30
+
31
+ const candidate = surface();
32
+ const badColor = candidate.setPixel(0, 0, [0, 0, 256, 255]);
33
+ expect(badColor.ok).toBe(false);
34
+ if (!badColor.ok && badColor.error.code === 'image-surface-invalid') {
35
+ expect(badColor.error.detail.operation).toBe('set-pixel');
36
+ }
37
+
38
+ const badSeed = candidate.fillNoise(Number.NaN);
39
+ expect(badSeed.ok).toBe(false);
40
+ if (!badSeed.ok && badSeed.error.code === 'image-surface-invalid') {
41
+ expect(badSeed.error.detail.operation).toBe('noise');
42
+ }
43
+ });
44
+
45
+ it('rounds coordinates, clips writes, and keeps exact RGBA8 channel order', () => {
46
+ const candidate = surface();
47
+ expect(candidate.setPixel(1.4, 1.6, [10, 20, 30, 40]).ok).toBe(true);
48
+ expect(candidate.data.slice((2 * 4 + 1) * 4, (2 * 4 + 2) * 4)).toEqual(
49
+ Uint8Array.of(10, 20, 30, 40),
50
+ );
51
+ expect(candidate.fillRect(-1.2, -1.2, 3.1, 3.1, [1, 2, 3, 4]).ok).toBe(true);
52
+ expect(candidate.data.slice(0, 4)).toEqual(Uint8Array.of(1, 2, 3, 4));
53
+ expect(candidate.fillCircle(3, 2, 1, { r: 9, g: 8, b: 7, a: 6 }).ok).toBe(true);
54
+ expect(candidate.data.slice((2 * 4 + 3) * 4, (2 * 4 + 4) * 4)).toEqual(
55
+ Uint8Array.of(9, 8, 7, 6),
56
+ );
57
+ });
58
+
59
+ it('snapshots overlapping blits before writing', () => {
60
+ const candidate = surface(4, 1);
61
+ for (let x = 0; x < 4; x += 1) {
62
+ expect(candidate.setPixel(x, 0, [x + 1, 0, 0, 255]).ok).toBe(true);
63
+ }
64
+ expect(candidate.blit(candidate, 1, 0, { x: 0, y: 0, width: 3, height: 1 }).ok).toBe(true);
65
+ expect(Array.from(candidate.data.filter((_value, index) => index % 4 === 0))).toEqual([
66
+ 1, 1, 2, 3,
67
+ ]);
68
+ });
69
+
70
+ it('produces byte-identical seeded noise and a regular TextureAsset/Pack carrier', () => {
71
+ const first = surface();
72
+ const second = surface();
73
+ expect(first.fillNoise(42, { min: 10, max: 200, alpha: 123 }).ok).toBe(true);
74
+ expect(second.noise(42, { min: 10, max: 200, alpha: 123 }).ok).toBe(true);
75
+ expect(first.data).toEqual(second.data);
76
+ expect(first.toTextureAsset()).toMatchObject({
77
+ kind: 'texture',
78
+ shape: {
79
+ viewDimension: '2d',
80
+ extent: { width: 4, height: 3 },
81
+ },
82
+ format: 'rgba8unorm-srgb',
83
+ colorSpace: 'srgb',
84
+ mips: { kind: 'none' },
85
+ });
86
+ const decoded = first.toDecodedImage();
87
+ const pack = toAssetPack(decoded, META);
88
+ expect(pack.subAssets).toEqual([{ guid: META.guid, sourceIndex: 0, kind: 'texture' }]);
89
+ expect(pack.importSettings.mipmap).toBe('none');
90
+ const changed = decoded.bytes.slice();
91
+ changed[0] = (changed[0] ?? 0) ^ 1;
92
+ expect(changed).not.toEqual(decoded.bytes);
93
+ });
94
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { produceTextureSource } from '../texture/importer.js';
3
+ import type { TextureSourceDescriptor } from '../texture/source-descriptor.js';
4
+ import { parseTextureSourceDescriptor } from '../texture/source-descriptor.js';
5
+
6
+ const GUID = '019ffa97-3000-7000-8000-000000000101';
7
+ const SOURCE_KEY = 'density/volume';
8
+ const BODY_BYTES = 64 * 64 * 64;
9
+
10
+ function densityDescriptor(): TextureSourceDescriptor {
11
+ return {
12
+ schemaVersion: '1',
13
+ shape: {
14
+ viewDimension: '3d',
15
+ extent: { width: 64, height: 64, depth: 64 },
16
+ },
17
+ format: 'r8unorm',
18
+ colorSpace: 'linear',
19
+ mips: { kind: 'none' },
20
+ rawSibling: 'density.raw',
21
+ };
22
+ }
23
+
24
+ function arrayDescriptor(): TextureSourceDescriptor {
25
+ return {
26
+ schemaVersion: '1',
27
+ shape: {
28
+ viewDimension: '2d-array',
29
+ extent: { width: 8, height: 4, layers: 3 },
30
+ },
31
+ format: 'r8unorm',
32
+ colorSpace: 'linear',
33
+ mips: { kind: 'none' },
34
+ rawSibling: 'array.raw',
35
+ };
36
+ }
37
+
38
+ describe('texture source descriptor producer contract', () => {
39
+ it('produces one 64^3 density asset from its raw sibling', async () => {
40
+ const descriptor = densityDescriptor();
41
+ const raw = new Uint8Array(BODY_BYTES);
42
+ raw[0] = 17;
43
+ raw[raw.length - 1] = 231;
44
+ const readSibling = async (uri: string) => {
45
+ expect(uri).toBe(descriptor.rawSibling);
46
+ return { ok: true as const, value: raw };
47
+ };
48
+
49
+ const result = await produceTextureSource({
50
+ descriptor,
51
+ guid: GUID,
52
+ sourceKey: SOURCE_KEY,
53
+ readSibling,
54
+ });
55
+
56
+ expect(result.ok).toBe(true);
57
+ if (!result.ok) return;
58
+ expect(result.value.guid).toBe(GUID);
59
+ expect(result.value.kind).toBe('texture');
60
+ expect(result.value.payload).toEqual({
61
+ kind: 'texture',
62
+ shape: descriptor.shape,
63
+ format: descriptor.format,
64
+ colorSpace: 'linear',
65
+ mips: { kind: 'none' },
66
+ data: raw,
67
+ });
68
+ expect(result.value.artifacts.body?.bytes).toEqual(raw);
69
+ expect(result.value.artifacts.body?.mediaType).toBe('application/x-forgeax-r8');
70
+ });
71
+
72
+ it('keeps array and 3d payloads equivalent without minting slice identities', async () => {
73
+ const descriptors = [arrayDescriptor(), densityDescriptor()];
74
+ for (const descriptor of descriptors) {
75
+ const layoutBytes = descriptor.shape.viewDimension === '3d' ? BODY_BYTES : 8 * 4 * 3;
76
+ const raw = new Uint8Array(layoutBytes);
77
+ const result = await produceTextureSource({
78
+ descriptor,
79
+ guid: GUID,
80
+ sourceKey: SOURCE_KEY,
81
+ readSibling: async () => ({ ok: true as const, value: raw }),
82
+ });
83
+
84
+ expect(result.ok).toBe(true);
85
+ if (!result.ok) continue;
86
+ expect(result.value.guid).toBe(GUID);
87
+ expect(result.value.payload.shape).toEqual(descriptor.shape);
88
+ expect(result.value.artifacts.body?.bytes).toEqual(raw);
89
+ }
90
+ });
91
+
92
+ it('rejects missing sibling and invalid descriptor facts structurally', async () => {
93
+ const missing = await produceTextureSource({
94
+ descriptor: densityDescriptor(),
95
+ guid: GUID,
96
+ sourceKey: SOURCE_KEY,
97
+ readSibling: async () => ({ ok: false as const, error: new Error('missing raw sibling') }),
98
+ });
99
+ expect(missing.ok).toBe(false);
100
+ if (!missing.ok) {
101
+ expect(missing.error.code).toBe('source-read-failed');
102
+ expect(missing.error.detail).toMatchObject({ sourceKey: SOURCE_KEY, sibling: 'density.raw' });
103
+ }
104
+
105
+ const invalid = parseTextureSourceDescriptor({
106
+ ...densityDescriptor(),
107
+ shape: { viewDimension: '3d', extent: { width: 64, height: 64, depth: 64 } },
108
+ format: 'bc7-rgba-unorm',
109
+ mips: { kind: 'none' },
110
+ });
111
+ expect(invalid.ok).toBe(false);
112
+ if (!invalid.ok) {
113
+ expect(invalid.error.code).toBe('texture-format-dimension-unsupported');
114
+ expect(invalid.error.detail).toMatchObject({ format: 'bc7-rgba-unorm', viewDimension: '3d' });
115
+ }
116
+ });
117
+ });
package/src/errors.ts CHANGED
@@ -25,6 +25,8 @@ const IMAGE_ERROR_EXPECTED: Readonly<Record<ImageErrorCode, string>> = {
25
25
  'atlas-size-exceeded':
26
26
  'image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint',
27
27
  'atlas-region-mismatch': 'sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight',
28
+ 'image-surface-invalid':
29
+ 'PixelSurface dimensions and authoring inputs satisfy the RGBA8 contract',
28
30
  };
29
31
 
30
32
  /** Runtime implementation of the correlated `ImageErrorFor<C>` envelope. */
@@ -46,6 +46,7 @@ import { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
46
46
  import type { CompressionMode } from './ktx2-encode.js';
47
47
  import { encodeTextureToKtx2, resolveEncodeMode } from './ktx2-encode.js';
48
48
  import { parseImage } from './parse-image.js';
49
+ import { importTextureSource } from './texture/importer.js';
49
50
 
50
51
  /** Map a source path / mime hint to the parseImage mime literal. */
51
52
  function mimeFromSource(source: string): 'image/png' | 'image/jpeg' | undefined {
@@ -319,13 +320,17 @@ async function importKtx2Source(ctx: ImportContext, bytes: Uint8Array): Promise<
319
320
  kind: 'texture',
320
321
  payload: {
321
322
  kind: 'texture',
322
- width: inspection.width,
323
- height: inspection.height,
323
+ shape: {
324
+ viewDimension: '2d',
325
+ extent: { width: inspection.width, height: inspection.height },
326
+ },
324
327
  format: colorSpaceToFormat(inspection.colorSpace),
325
328
  data: bytes,
326
329
  colorSpace: inspection.colorSpace,
327
- mipmap: inspection.levelCount > 1,
328
- mipLevelCount: inspection.levelCount,
330
+ mips:
331
+ inspection.levelCount > 1
332
+ ? { kind: 'packed', levelCount: inspection.levelCount }
333
+ : { kind: 'none' },
329
334
  },
330
335
  refs: [],
331
336
  artifacts: {
@@ -481,13 +486,17 @@ async function importBasisSource(ctx: ImportContext, bytes: Uint8Array): Promise
481
486
  kind: 'texture',
482
487
  payload: {
483
488
  kind: 'texture',
484
- width: inspection.width,
485
- height: inspection.height,
489
+ shape: {
490
+ viewDimension: '2d',
491
+ extent: { width: inspection.width, height: inspection.height },
492
+ },
486
493
  format: colorSpaceToFormat(colorSpace),
487
494
  data: bytes,
488
495
  colorSpace,
489
- mipmap: inspection.levelCount > 1,
490
- mipLevelCount: inspection.levelCount,
496
+ mips:
497
+ inspection.levelCount > 1
498
+ ? { kind: 'packed', levelCount: inspection.levelCount }
499
+ : { kind: 'none' },
491
500
  },
492
501
  refs: [],
493
502
  artifacts: {
@@ -574,6 +583,9 @@ async function maybeEncodeTextureBytes(
574
583
  }
575
584
 
576
585
  async function importImage(ctx: ImportContext): Promise<ImportResult> {
586
+ if (ctx.source.toLowerCase().endsWith('.texture.json')) {
587
+ return importTextureSource(ctx);
588
+ }
577
589
  const requiredKind = requiredImageOutputKind(ctx.source);
578
590
  if (requiredKind !== undefined) {
579
591
  const topologyError = validateImageOutputTopology(ctx, requiredKind);
@@ -730,12 +742,11 @@ async function importImage(ctx: ImportContext): Promise<ImportResult> {
730
742
  if (sub.kind !== 'texture') continue;
731
743
  const payload: TextureAsset = {
732
744
  kind: 'texture',
733
- width: dec.width,
734
- height: dec.height,
745
+ shape: { viewDimension: '2d', extent: { width: dec.width, height: dec.height } },
735
746
  format: colorSpaceToFormat(colorSpace),
736
747
  data: encodedBytes ?? dec.bytes,
737
748
  colorSpace,
738
- mipmap,
749
+ mips: mipmap ? { kind: 'generate' } : { kind: 'none' },
739
750
  };
740
751
  out.push({
741
752
  guid: sub.guid,
@@ -825,11 +836,10 @@ export const decodeImageForImport: ImportContext['decodeImage'] = async (
825
836
  texture: {
826
837
  kind: 'texture' as const,
827
838
  data: cookedBytes,
828
- width: tex.width,
829
- height: tex.height,
839
+ shape: { viewDimension: '2d', extent: { width: tex.width, height: tex.height } },
830
840
  format: colorSpaceToFormat(colorSpace),
831
841
  colorSpace,
832
- mipmap,
842
+ mips: mipmap ? { kind: 'generate' as const } : { kind: 'none' as const },
833
843
  },
834
844
  bytes: cookedBytes,
835
845
  mediaType,
package/src/index.ts CHANGED
@@ -23,6 +23,14 @@ export { decodeHdr } from './hdr-decoder.js';
23
23
  export { decodeImageInBrowser } from './image-decoder-browser.js';
24
24
  export type { JpegModule, UpngModule } from './image-decoder-node.js';
25
25
  export { loadJpeg, loadUpng } from './image-decoder-node.js';
26
+ export {
27
+ createPixelSurface,
28
+ type PixelColor,
29
+ type PixelSurface,
30
+ type PixelSurfaceNoiseOptions,
31
+ type PixelSurfaceOptions,
32
+ type PixelSurfaceRect,
33
+ } from './pixel-surface.js';
26
34
  export type {
27
35
  EmittedSubAsset,
28
36
  ExistingExternalAssetPackage,