@forgeax/engine-import 0.1.20 → 0.1.23

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 (91) hide show
  1. package/README.md +91 -3
  2. package/dist/__tests__/ies-contract.unit.test.d.ts +2 -0
  3. package/dist/__tests__/ies-contract.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/ies-producer.unit.test.d.ts +2 -0
  5. package/dist/__tests__/ies-producer.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/mesh-lod-contract.unit.test.d.ts +2 -0
  7. package/dist/__tests__/mesh-lod-contract.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts +2 -0
  9. package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +2 -0
  11. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts +2 -0
  13. package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/source-package-publication-lod.integration.test.d.ts +2 -0
  15. package/dist/__tests__/source-package-publication-lod.integration.test.d.ts.map +1 -0
  16. package/dist/browser.d.ts +1 -0
  17. package/dist/browser.d.ts.map +1 -1
  18. package/dist/browser.mjs +230 -3
  19. package/dist/browser.mjs.map +1 -1
  20. package/dist/build-production.d.ts +1 -8
  21. package/dist/build-production.d.ts.map +1 -1
  22. package/dist/ies/ies-importer.d.ts +4 -0
  23. package/dist/ies/ies-importer.d.ts.map +1 -0
  24. package/dist/ies/parse-lm63.d.ts +13 -0
  25. package/dist/ies/parse-lm63.d.ts.map +1 -0
  26. package/dist/ies/resample-type-c.d.ts +4 -0
  27. package/dist/ies/resample-type-c.d.ts.map +1 -0
  28. package/dist/import-runner.d.ts.map +1 -1
  29. package/dist/index.d.ts +11 -6
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.mjs +2550 -1029
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/mesh-bin.d.ts.map +1 -1
  34. package/dist/mesh-bin.mjs +29 -1
  35. package/dist/mesh-bin.mjs.map +1 -1
  36. package/dist/mesh-lod.d.ts +60 -0
  37. package/dist/mesh-lod.d.ts.map +1 -0
  38. package/dist/parameterized-scriptable-pack.d.ts +68 -0
  39. package/dist/parameterized-scriptable-pack.d.ts.map +1 -0
  40. package/dist/scriptable-pack-file-snapshot.d.ts +71 -0
  41. package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -0
  42. package/dist/scriptable-pack-host.d.ts +70 -39
  43. package/dist/scriptable-pack-host.d.ts.map +1 -1
  44. package/dist/scriptable-pack-output-producers.d.ts +1 -0
  45. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  46. package/dist/scriptable-pack-staged-snapshot.d.ts +2 -2
  47. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
  48. package/dist/scriptable-pack.d.ts +13 -26
  49. package/dist/scriptable-pack.d.ts.map +1 -1
  50. package/dist/source-package-errors.d.ts.map +1 -1
  51. package/dist/source-package-publication.d.ts.map +1 -1
  52. package/package.json +8 -7
  53. package/src/__tests__/ies-contract.unit.test.ts +31 -0
  54. package/src/__tests__/ies-producer.unit.test.ts +104 -0
  55. package/src/__tests__/mesh-bin.test.ts +69 -0
  56. package/src/__tests__/mesh-lod-contract.unit.test.ts +92 -0
  57. package/src/__tests__/mesh-lod-reimport.integration.test.ts +15 -0
  58. package/src/__tests__/parameterized-scriptable-pack.unit.test.ts +270 -0
  59. package/src/__tests__/scriptable-pack-file-snapshot.unit.test.ts +177 -0
  60. package/src/__tests__/scriptable-pack-host.unit.test.ts +196 -4
  61. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +7 -1
  62. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +43 -12
  63. package/src/__tests__/source-package-publication-lod.integration.test.ts +29 -0
  64. package/src/__tests__/source-package-publication.integration.test.ts +26 -1
  65. package/src/browser.ts +5 -0
  66. package/src/build-production.ts +498 -141
  67. package/src/ies/ies-importer.ts +126 -0
  68. package/src/ies/parse-lm63.ts +98 -0
  69. package/src/ies/resample-type-c.ts +108 -0
  70. package/src/import-runner.ts +58 -0
  71. package/src/index.ts +49 -19
  72. package/src/mesh-bin.ts +38 -0
  73. package/src/mesh-lod.ts +265 -0
  74. package/src/parameterized-scriptable-pack.ts +725 -0
  75. package/src/scriptable-pack-file-snapshot.ts +346 -0
  76. package/src/scriptable-pack-host.ts +790 -343
  77. package/src/scriptable-pack-output-producers.ts +58 -1
  78. package/src/scriptable-pack-staged-snapshot.ts +12 -11
  79. package/src/scriptable-pack.ts +16 -485
  80. package/src/source-package-errors.ts +6 -0
  81. package/src/source-package-publication.ts +32 -2
  82. package/dist/.tsbuildinfo +0 -1
  83. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +0 -2
  84. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +0 -1
  85. package/dist/__tests__/scriptable-pack.integration.test.d.ts +0 -2
  86. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +0 -1
  87. package/dist/scriptable-source-package.d.ts +0 -14
  88. package/dist/scriptable-source-package.d.ts.map +0 -1
  89. package/src/__tests__/scriptable-pack-product.contract.test.ts +0 -46
  90. package/src/__tests__/scriptable-pack.integration.test.ts +0 -435
  91. package/src/scriptable-source-package.ts +0 -88
@@ -0,0 +1,126 @@
1
+ import type {
2
+ IesProfileAsset,
3
+ ImportContext,
4
+ Importer,
5
+ ImportResult,
6
+ Result,
7
+ } from '@forgeax/engine-types';
8
+ import {
9
+ err,
10
+ IES_PROFILE_BYTE_LENGTH,
11
+ IES_PROFILE_HEIGHT,
12
+ IES_PROFILE_WIDTH,
13
+ IMPORT_ERROR_HINTS,
14
+ ImportError,
15
+ ok,
16
+ } from '@forgeax/engine-types';
17
+ import { parseLm63TypeC } from './parse-lm63.js';
18
+ import { resampleTypeC } from './resample-type-c.js';
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+
24
+ function decodeFloat16(bytes: Uint8Array, offset: number): number {
25
+ const bits = (bytes[offset] ?? 0) | ((bytes[offset + 1] ?? 0) << 8);
26
+ const sign = (bits & 0x8000) === 0 ? 1 : -1;
27
+ const exponent = (bits >>> 10) & 0x1f;
28
+ const fraction = bits & 0x03ff;
29
+ if (exponent === 0) return sign * 2 ** -14 * (fraction / 2 ** 10);
30
+ if (exponent === 0x1f) return fraction === 0 ? sign * Infinity : Number.NaN;
31
+ return sign * 2 ** (exponent - 15) * (1 + fraction / 2 ** 10);
32
+ }
33
+
34
+ export function validateIesProfilePayload(value: unknown): Result<IesProfileAsset, Error> {
35
+ if (!isRecord(value) || value.kind !== 'ies-profile') {
36
+ return err(new Error('IES payload kind must be ies-profile'));
37
+ }
38
+ const data = value.data;
39
+ if (!(data instanceof Uint8Array) || data.byteLength !== IES_PROFILE_BYTE_LENGTH) {
40
+ return err(
41
+ new Error(
42
+ `IES payload must contain ${IES_PROFILE_WIDTH}x${IES_PROFILE_HEIGHT} little-endian f16 samples`,
43
+ ),
44
+ );
45
+ }
46
+ for (let offset = 0; offset < data.byteLength; offset += 2) {
47
+ if (!Number.isFinite(decodeFloat16(data, offset))) {
48
+ return err(new Error('IES payload contains a non-finite f16 sample'));
49
+ }
50
+ }
51
+ return ok(value as unknown as IesProfileAsset);
52
+ }
53
+
54
+ function sourceValidationError(ctx: ImportContext, reason: string): ImportError {
55
+ return new ImportError({
56
+ code: 'source-validation-failed',
57
+ expected: 'LM-63 Type C source with TILT=NONE',
58
+ hint: IMPORT_ERROR_HINTS['source-validation-failed'],
59
+ detail: {
60
+ diagnostics: [
61
+ {
62
+ code: 'ies-source-invalid',
63
+ severity: 'error',
64
+ sourcePath: ctx.source,
65
+ sourceRange: { start: 0, end: 0, line: 1, column: 1 },
66
+ rule: 'LM-63 Type C TILT=NONE',
67
+ expected: 'LM-63 Type C source with TILT=NONE',
68
+ actual: reason,
69
+ hint: 'repair the IES source and rerun the build-time importer',
70
+ },
71
+ ],
72
+ },
73
+ });
74
+ }
75
+
76
+ async function importIes(ctx: ImportContext): Promise<ImportResult> {
77
+ const source = await ctx.readSource();
78
+ if (!source.ok) {
79
+ return {
80
+ ok: false,
81
+ error: new ImportError({
82
+ code: 'source-read-failed',
83
+ expected: `readable IES source at ${ctx.source}`,
84
+ hint: IMPORT_ERROR_HINTS['source-read-failed'],
85
+ detail: { source: ctx.source, reason: String(source.error) },
86
+ }),
87
+ };
88
+ }
89
+ const parsed = parseLm63TypeC(new TextDecoder().decode(source.value));
90
+ if (!parsed.ok) return { ok: false, error: sourceValidationError(ctx, parsed.error.reason) };
91
+ const declaration = ctx.subAssets.find((asset) => asset.kind === 'ies-profile');
92
+ if (declaration === undefined) {
93
+ return {
94
+ ok: false,
95
+ error: sourceValidationError(ctx, 'missing ies-profile sub-asset declaration'),
96
+ };
97
+ }
98
+ const data = resampleTypeC(parsed.value);
99
+ const payload: IesProfileAsset = { kind: 'ies-profile', data };
100
+ return {
101
+ ok: true,
102
+ value: {
103
+ assets: [
104
+ {
105
+ guid: declaration.guid,
106
+ kind: 'ies-profile',
107
+ payload,
108
+ refs: [],
109
+ artifacts: {
110
+ body: {
111
+ mediaType: 'application/octet-stream',
112
+ assetCodec: { name: 'forgeax-ies-profile', version: '1' },
113
+ bytes: data,
114
+ },
115
+ },
116
+ },
117
+ ],
118
+ sourceDependencies: [ctx.source],
119
+ },
120
+ };
121
+ }
122
+
123
+ export const iesImporter: Importer = {
124
+ key: 'ies',
125
+ import: importIes,
126
+ };
@@ -0,0 +1,98 @@
1
+ import type { Result } from '@forgeax/engine-types';
2
+ import { err, ok } from '@forgeax/engine-types';
3
+
4
+ export interface Lm63TypeC {
5
+ readonly tilt: 'NONE';
6
+ readonly verticalAnglesDeg: readonly number[];
7
+ readonly horizontalAnglesDeg: readonly number[];
8
+ readonly candela: readonly number[];
9
+ }
10
+
11
+ export interface Lm63ParseError {
12
+ readonly code: 'invalid-ies' | 'unsupported-tilt' | 'unsupported-photometric-type';
13
+ readonly reason: string;
14
+ }
15
+
16
+ function numericTokens(source: string, startLine: number): number[] {
17
+ return source
18
+ .split(/\r?\n/)
19
+ .slice(startLine)
20
+ .join(' ')
21
+ .replaceAll(',', ' ')
22
+ .split(/\s+/)
23
+ .filter((token) => token.length > 0)
24
+ .map(Number);
25
+ }
26
+
27
+ function allFinite(values: readonly number[]): boolean {
28
+ return values.every(Number.isFinite);
29
+ }
30
+
31
+ export function parseLm63TypeC(source: string): Result<Lm63TypeC, Lm63ParseError> {
32
+ const lines = source.split(/\r?\n/);
33
+ const tiltLine = lines.findIndex((line) => /^\s*TILT\s*=/i.test(line));
34
+ if (tiltLine < 0) return err({ code: 'invalid-ies', reason: 'missing TILT declaration' });
35
+ const tiltSource = lines[tiltLine];
36
+ if (tiltSource === undefined)
37
+ return err({ code: 'invalid-ies', reason: 'missing TILT declaration' });
38
+ const tilt = tiltSource
39
+ .slice(tiltSource.indexOf('=') + 1)
40
+ .trim()
41
+ .toUpperCase();
42
+ if (tilt !== 'NONE') return err({ code: 'unsupported-tilt', reason: `TILT=${tilt}` });
43
+
44
+ const values = numericTokens(source, tiltLine + 1);
45
+ if (values.length < 12 || !allFinite(values.slice(0, 12))) {
46
+ return err({ code: 'invalid-ies', reason: 'numeric header is incomplete or non-finite' });
47
+ }
48
+ const verticalCount = values[3];
49
+ const horizontalCount = values[4];
50
+ const photometricType = values[5];
51
+ if (
52
+ verticalCount === undefined ||
53
+ horizontalCount === undefined ||
54
+ photometricType === undefined
55
+ ) {
56
+ return err({ code: 'invalid-ies', reason: 'numeric header is incomplete or non-finite' });
57
+ }
58
+ if (photometricType !== 1) {
59
+ return err({
60
+ code: 'unsupported-photometric-type',
61
+ reason: `photometric type ${photometricType} is not Type C`,
62
+ });
63
+ }
64
+ if (
65
+ !Number.isInteger(verticalCount) ||
66
+ !Number.isInteger(horizontalCount) ||
67
+ verticalCount < 2 ||
68
+ horizontalCount < 1
69
+ ) {
70
+ return err({ code: 'invalid-ies', reason: 'angle counts are invalid' });
71
+ }
72
+
73
+ const angleOffset = 12;
74
+ const verticalEnd = angleOffset + verticalCount;
75
+ const horizontalEnd = verticalEnd + horizontalCount;
76
+ const candelaEnd = horizontalEnd + verticalCount * horizontalCount;
77
+ if (values.length < candelaEnd) {
78
+ return err({ code: 'invalid-ies', reason: 'angle or candela table is incomplete' });
79
+ }
80
+ const verticalAnglesDeg = values.slice(angleOffset, verticalEnd);
81
+ const horizontalAnglesDeg = values.slice(verticalEnd, horizontalEnd);
82
+ const candelaMultiplier = values[2];
83
+ if (candelaMultiplier === undefined) {
84
+ return err({ code: 'invalid-ies', reason: 'numeric header is incomplete or non-finite' });
85
+ }
86
+ const candela = values.slice(horizontalEnd, candelaEnd).map((value) => value * candelaMultiplier);
87
+ if (
88
+ !allFinite(verticalAnglesDeg) ||
89
+ !allFinite(horizontalAnglesDeg) ||
90
+ !allFinite(candela) ||
91
+ verticalAnglesDeg[0] !== 0 ||
92
+ verticalAnglesDeg[verticalAnglesDeg.length - 1] !== 180 ||
93
+ horizontalAnglesDeg[0] !== 0
94
+ ) {
95
+ return err({ code: 'invalid-ies', reason: 'Type C angles or candela values are invalid' });
96
+ }
97
+ return ok({ tilt: 'NONE', verticalAnglesDeg, horizontalAnglesDeg, candela });
98
+ }
@@ -0,0 +1,108 @@
1
+ import { IES_PROFILE_HEIGHT, IES_PROFILE_WIDTH, type IesProfileAsset } from '@forgeax/engine-types';
2
+ import type { Lm63TypeC } from './parse-lm63.js';
3
+
4
+ function horizontalTable(source: Lm63TypeC): { angles: number[]; values: number[] } {
5
+ const angles = [...source.horizontalAnglesDeg];
6
+ const values = [...source.candela];
7
+ const verticalCount = source.verticalAnglesDeg.length;
8
+ const lastAngle = angles.at(-1);
9
+ if (lastAngle === undefined) return { angles, values };
10
+ if (lastAngle < 360) {
11
+ for (let index = angles.length - 2; index > 0; index--) {
12
+ const angle = angles[index];
13
+ if (angle === undefined) continue;
14
+ angles.push(360 - angle);
15
+ const row = source.horizontalAnglesDeg.length - 1 - index;
16
+ values.push(
17
+ ...source.candela.slice(row * verticalCount, row * verticalCount + verticalCount),
18
+ );
19
+ }
20
+ }
21
+ return { angles, values };
22
+ }
23
+
24
+ function interpolate(
25
+ values: readonly number[],
26
+ angles: readonly number[],
27
+ angle: number,
28
+ verticalIndex: number,
29
+ verticalCount: number,
30
+ ): number {
31
+ const wrapped = ((angle % 360) + 360) % 360;
32
+ const last = angles.length - 1;
33
+ const finalAngle = angles[last];
34
+ const firstAngle = angles[0];
35
+ if (finalAngle === undefined || firstAngle === undefined) return Number.NaN;
36
+ for (let index = 0; index < last; index++) {
37
+ const left = angles[index];
38
+ const right = angles[index + 1];
39
+ if (left === undefined || right === undefined) continue;
40
+ if (wrapped < left || wrapped > right) continue;
41
+ const span = right - left;
42
+ const factor = span === 0 ? 0 : (wrapped - left) / span;
43
+ const a = values[index * verticalCount + verticalIndex] ?? 0;
44
+ const b = values[(index + 1) * verticalCount + verticalIndex] ?? 0;
45
+ return a + (b - a) * factor;
46
+ }
47
+ const first = values[verticalIndex] ?? 0;
48
+ const finalRow = (angles.length - 1) * verticalCount + verticalIndex;
49
+ const final = values[finalRow] ?? 0;
50
+ const span = 360 - finalAngle + firstAngle;
51
+ const factor = span === 0 ? 0 : (wrapped - finalAngle + 360) / span;
52
+ return final + (first - final) * factor;
53
+ }
54
+
55
+ function toFloat16(value: number): number {
56
+ if (value === 0) return 0;
57
+ const sign = value < 0 ? 0x8000 : 0;
58
+ const absolute = Math.abs(value);
59
+ if (!Number.isFinite(absolute)) return sign | 0x7c00;
60
+ const exponent = Math.floor(Math.log2(absolute));
61
+ if (exponent < -14) return sign | Math.round(absolute / 2 ** -24);
62
+ if (exponent > 15) return sign | 0x7c00;
63
+ const mantissa = Math.round((absolute / 2 ** exponent - 1) * 1024);
64
+ return sign | ((exponent + 15) << 10) | Math.min(mantissa, 1023);
65
+ }
66
+
67
+ export function readFloat16LE(bytes: Uint8Array, offset: number): number {
68
+ const bits = (bytes[offset] ?? 0) | ((bytes[offset + 1] ?? 0) << 8);
69
+ const sign = (bits & 0x8000) === 0 ? 1 : -1;
70
+ const exponent = (bits >>> 10) & 0x1f;
71
+ const fraction = bits & 0x03ff;
72
+ if (exponent === 0) return sign * 2 ** -14 * (fraction / 2 ** 10);
73
+ if (exponent === 0x1f) return fraction === 0 ? sign * Infinity : Number.NaN;
74
+ return sign * 2 ** (exponent - 15) * (1 + fraction / 2 ** 10);
75
+ }
76
+
77
+ export function resampleTypeC(source: Lm63TypeC): Uint8Array {
78
+ const table = horizontalTable(source);
79
+ const verticalCount = source.verticalAnglesDeg.length;
80
+ const output = new Uint8Array(IES_PROFILE_WIDTH * IES_PROFILE_HEIGHT * 2);
81
+ let peak = 0;
82
+ const samples: number[] = [];
83
+ for (let y = 0; y < IES_PROFILE_HEIGHT; y++) {
84
+ const vertical = (y / (IES_PROFILE_HEIGHT - 1)) * 180;
85
+ let lower = 0;
86
+ while (lower + 1 < verticalCount && (source.verticalAnglesDeg[lower + 1] ?? 0) < vertical)
87
+ lower++;
88
+ const upper = Math.min(lower + 1, verticalCount - 1);
89
+ const left = source.verticalAnglesDeg[lower] ?? 0;
90
+ const right = source.verticalAnglesDeg[upper] ?? 0;
91
+ const factor = right === left ? 0 : (vertical - left) / (right - left);
92
+ for (let x = 0; x < IES_PROFILE_WIDTH; x++) {
93
+ const horizontal = (x / IES_PROFILE_WIDTH) * 360;
94
+ const low = interpolate(table.values, table.angles, horizontal, lower, verticalCount);
95
+ const high = interpolate(table.values, table.angles, horizontal, upper, verticalCount);
96
+ const value = low + (high - low) * factor;
97
+ samples.push(Math.max(0, value));
98
+ peak = Math.max(peak, value);
99
+ }
100
+ }
101
+ const divisor = peak > 0 ? peak : 1;
102
+ for (let index = 0; index < samples.length; index++) {
103
+ const bits = toFloat16((samples[index] ?? 0) / divisor);
104
+ output[index * 2] = bits & 0xff;
105
+ output[index * 2 + 1] = bits >>> 8;
106
+ }
107
+ return output satisfies IesProfileAsset['data'];
108
+ }
@@ -46,6 +46,7 @@ import {
46
46
  type TerminalImportProduct,
47
47
  } from './import-product.js';
48
48
  import type { ImporterRegistry } from './importer-registry.js';
49
+ import { validateMeshLodContract } from './mesh-lod.js';
49
50
 
50
51
  /** Reserved `meta.importer` key consumed by vite-plugin-shader, not the import runner. */
51
52
  export const SHADER_RESERVED_IMPORTER_KEY = 'shader';
@@ -79,6 +80,13 @@ function isModuleLoadFailure(e: unknown): boolean {
79
80
  );
80
81
  }
81
82
 
83
+ function declarationsSourceKey(
84
+ declarations: readonly { readonly guid: string; readonly sourceKey?: string }[],
85
+ guid: string,
86
+ ): string | undefined {
87
+ return declarations.find((declaration) => declaration.guid === guid)?.sourceKey;
88
+ }
89
+
82
90
  /**
83
91
  * bug-20260610-pack-typed-array-roundtrip: normalise a value tree so every
84
92
  * typed-array becomes a plain `number[]`. The DDC pack is serialised via
@@ -634,6 +642,56 @@ export async function runImport(
634
642
 
635
643
  const produced = product.assets;
636
644
 
645
+ // LOD is a producer-authored MeshAsset fact. Validate it once at the
646
+ // import boundary so malformed coverage never reaches DDC or runtime.
647
+ for (const asset of produced) {
648
+ if (asset.kind !== 'mesh' || asset.payload === null || typeof asset.payload !== 'object')
649
+ continue;
650
+ const payload = asset.payload as {
651
+ readonly lods?: readonly {
652
+ readonly mesh?: unknown;
653
+ readonly meshGuid?: unknown;
654
+ readonly screenCoverage?: unknown;
655
+ }[];
656
+ readonly lodHysteresis?: unknown;
657
+ };
658
+ if (payload.lods === undefined) continue;
659
+ const lods = payload.lods.map((level) => ({
660
+ meshGuid:
661
+ typeof level.meshGuid === 'string'
662
+ ? level.meshGuid
663
+ : typeof level.mesh === 'string'
664
+ ? level.mesh
665
+ : JSON.stringify(level.mesh),
666
+ screenCoverage: level.screenCoverage,
667
+ }));
668
+ const validated = validateMeshLodContract({
669
+ lods: lods as readonly { meshGuid: string; screenCoverage: number }[],
670
+ ...(payload.lodHysteresis === undefined
671
+ ? {}
672
+ : { lodHysteresis: payload.lodHysteresis as number }),
673
+ });
674
+ if (!validated.ok) {
675
+ return errResult(
676
+ new ImportError({
677
+ code: validated.error.code,
678
+ expected: 'MeshAsset LOD facts to satisfy the shared contract',
679
+ hint: IMPORT_ERROR_HINTS[validated.error.code],
680
+ detail: {
681
+ meshLodSourceKey: declarationsSourceKey(meta.subAssets, asset.guid),
682
+ reason: validated.error.reason,
683
+ ...(validated.error.code === 'mesh-lod-topology-change'
684
+ ? {
685
+ previousIndices: validated.error.previousIndices,
686
+ nextIndices: validated.error.nextIndices,
687
+ }
688
+ : {}),
689
+ },
690
+ }),
691
+ );
692
+ }
693
+ }
694
+
637
695
  // GUID import-stable iron law: the produced GUID set must be a superset of
638
696
  // the declared set, and must not contain any GUID the meta never declared.
639
697
  const declared = new Set(meta.subAssets.map((s) => s.guid));
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  AudioClipAsset,
22
22
  EquirectAsset,
23
23
  FontAsset,
24
+ IesProfileAsset,
24
25
  MaterialAsset,
25
26
  MeshAsset,
26
27
  ParticleEffectAsset,
@@ -51,7 +52,6 @@ export {
51
52
  type BuildProductionOptions,
52
53
  type BuildProductionSink,
53
54
  produceBuildAssets,
54
- type ScriptableBuildPackage,
55
55
  } from './build-production.js';
56
56
  export {
57
57
  type CatalogImporterDisposition,
@@ -60,6 +60,16 @@ export {
60
60
  DEFAULT_CATALOG_IMPORTER_KEYS,
61
61
  } from './catalog-importer-policy.js';
62
62
  export { buildCatalogResult } from './catalog-inventory.js';
63
+ export {
64
+ iesImporter,
65
+ validateIesProfilePayload,
66
+ } from './ies/ies-importer.js';
67
+ export {
68
+ type Lm63ParseError,
69
+ type Lm63TypeC,
70
+ parseLm63TypeC,
71
+ } from './ies/parse-lm63.js';
72
+ export { readFloat16LE, resampleTypeC } from './ies/resample-type-c.js';
63
73
  export {
64
74
  createImportProduct,
65
75
  finalizeImportProducts,
@@ -83,18 +93,37 @@ export {
83
93
  type MeshBinEncodeError,
84
94
  packMeshBinV4,
85
95
  } from './mesh-bin.js';
96
+ export {
97
+ deriveDefaultLodScreenCoverages,
98
+ type MeshLodBounds,
99
+ type MeshLodContractError,
100
+ type MeshLodContractInput,
101
+ type MeshLodMaterialSlot,
102
+ type MeshLodMetaEntry,
103
+ type MeshLodRelation,
104
+ type ReconciledMeshLodMeta,
105
+ reconcileMeshLodMeta,
106
+ validateMeshLodContract,
107
+ } from './mesh-lod.js';
86
108
  export { projectImportProductForBuild } from './pack-projection.js';
109
+ export {
110
+ buildParameterizedScriptablePack,
111
+ buildParameterizedScriptablePackWorklist,
112
+ type ParameterizedScriptablePackBuildOptions,
113
+ type ParameterizedScriptablePackBuildProduct,
114
+ type ParameterizedScriptablePackBuildResult,
115
+ type ParameterizedScriptablePackWorkItem,
116
+ type ParameterizedScriptablePackWorklistOptions,
117
+ type ParameterizedScriptablePackWorklistProduct,
118
+ } from './parameterized-scriptable-pack.js';
87
119
  export {
88
120
  type AssetOutputInput,
89
121
  type AssetOutputPayload,
90
122
  type AssetOutputProducer,
91
123
  AssetOutputProducerRegistry,
92
124
  type AssetOutputProduct,
93
- type BuildScriptablePackOptions,
94
- buildScriptablePack,
95
125
  type ScriptablePackAssetSnapshot,
96
126
  type ScriptablePackAssetSnapshotSource,
97
- type ScriptablePackBuildBridgeResult,
98
127
  type ScriptablePackBuildProduct,
99
128
  type ScriptablePackDomainError,
100
129
  type ScriptablePackExternalEvidence,
@@ -103,25 +132,31 @@ export {
103
132
  type ScriptablePackStagedOutput,
104
133
  } from './scriptable-pack.js';
105
134
  export {
106
- type AuthoredPackTransport,
107
- type CookedAuthoredPack,
135
+ createScriptablePackFileAssetSnapshotSource,
136
+ type ScriptablePackFileAssetSnapshotError,
137
+ type ScriptablePackFileAssetSnapshotSourceOptions,
138
+ } from './scriptable-pack-file-snapshot.js';
139
+ export {
108
140
  canonicalScriptableSourcePath,
141
+ type DirectPackTransport,
142
+ type DirectPackTransportInput,
109
143
  declaredPackExternalOutputs,
110
- materializePreparedScriptablePack,
111
- type PreparedScriptablePack,
112
- prepareAuthoredPackTransport,
113
- produceScriptablePackProducts,
114
- projectScriptablePackPublication,
144
+ type LegacyPackTransport,
145
+ materializePreparedParameterizedScriptablePack,
146
+ type ParameterizedScriptablePackInput,
147
+ type ParameterizedScriptablePackProductionOptions,
148
+ type PreparedParameterizedScriptablePack,
149
+ prepareDirectPackTransport,
150
+ prepareLegacyPackTransport,
151
+ produceParameterizedScriptablePackProducts,
115
152
  readCookedAuthoredPack,
116
153
  type ScriptablePackExternalImportOptions,
117
- type ScriptablePackInput,
118
154
  type ScriptablePackPublicationFacts,
119
155
  type ScriptablePackTransportPaths,
120
- type ScriptablePackTransportPolicy,
121
156
  type ScriptablePackTransportSink,
122
- scriptablePackInputs,
123
157
  } from './scriptable-pack-host.js';
124
158
  export {
159
+ createPreExternalizedSceneAssetOutputProducer,
125
160
  createSceneAssetOutputProducer,
126
161
  createStandardAssetOutputProducerRegistry,
127
162
  materialAssetOutputProducer,
@@ -134,11 +169,6 @@ export {
134
169
  type ScriptablePackStagedOwner,
135
170
  type ScriptablePackStagedSnapshotOptions,
136
171
  } from './scriptable-pack-staged-snapshot.js';
137
- export {
138
- produceScriptableSourcePackage,
139
- type ScriptableSourcePackageProduct,
140
- type ScriptableSourcePackageResult,
141
- } from './scriptable-source-package.js';
142
172
  export {
143
173
  finalizeSourcePackage,
144
174
  type ProducerReadiness,
package/src/mesh-bin.ts CHANGED
@@ -76,6 +76,42 @@ function refsMeta(payload: MeshPayloadIn, refs: readonly string[]): Record<strin
76
76
  };
77
77
  },
78
78
  );
79
+ if (payload.lods !== undefined && payload.lods.length > 7) {
80
+ throw new Error('MeshAsset LOD chain supports at most seven lower-detail levels');
81
+ }
82
+ let previousCoverage = 1;
83
+ const seenLodGuids = new Set<string>();
84
+ const lods = payload.lods?.map((lod, lodIndex) => {
85
+ const guid = AssetGuid.format(lod.mesh).toLowerCase();
86
+ const meshRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
87
+ if (meshRef < 0) {
88
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is absent from refs`);
89
+ }
90
+ if (seenLodGuids.has(guid)) {
91
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is duplicated`);
92
+ }
93
+ if (
94
+ !Number.isFinite(lod.screenCoverage) ||
95
+ lod.screenCoverage <= 0 ||
96
+ lod.screenCoverage > 1 ||
97
+ lod.screenCoverage >= previousCoverage
98
+ ) {
99
+ throw new Error(
100
+ `LOD ${lodIndex} screenCoverage must be finite, in (0, 1], and strictly decreasing`,
101
+ );
102
+ }
103
+ seenLodGuids.add(guid);
104
+ previousCoverage = lod.screenCoverage;
105
+ return { meshRef, screenCoverage: lod.screenCoverage };
106
+ });
107
+ if (
108
+ payload.lodHysteresis !== undefined &&
109
+ (!Number.isFinite(payload.lodHysteresis) ||
110
+ payload.lodHysteresis < 0 ||
111
+ payload.lodHysteresis >= 1)
112
+ ) {
113
+ throw new Error('lodHysteresis must be finite and in [0, 1)');
114
+ }
79
115
  return {
80
116
  submeshes:
81
117
  payload.submeshes === undefined || payload.submeshes.length === 0
@@ -89,6 +125,8 @@ function refsMeta(payload: MeshPayloadIn, refs: readonly string[]): Record<strin
89
125
  ...(payload.morphWeights === undefined
90
126
  ? {}
91
127
  : { morphWeights: jsonValue(payload.morphWeights) }),
128
+ ...(lods === undefined ? {} : { lods }),
129
+ ...(payload.lodHysteresis === undefined ? {} : { lodHysteresis: payload.lodHysteresis }),
92
130
  };
93
131
  }
94
132