@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.
- package/README.md +91 -3
- package/dist/__tests__/ies-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts +2 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.mjs +230 -3
- package/dist/browser.mjs.map +1 -1
- package/dist/build-production.d.ts +1 -8
- package/dist/build-production.d.ts.map +1 -1
- package/dist/ies/ies-importer.d.ts +4 -0
- package/dist/ies/ies-importer.d.ts.map +1 -0
- package/dist/ies/parse-lm63.d.ts +13 -0
- package/dist/ies/parse-lm63.d.ts.map +1 -0
- package/dist/ies/resample-type-c.d.ts +4 -0
- package/dist/ies/resample-type-c.d.ts.map +1 -0
- package/dist/import-runner.d.ts.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2550 -1029
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-bin.d.ts.map +1 -1
- package/dist/mesh-bin.mjs +29 -1
- package/dist/mesh-bin.mjs.map +1 -1
- package/dist/mesh-lod.d.ts +60 -0
- package/dist/mesh-lod.d.ts.map +1 -0
- package/dist/parameterized-scriptable-pack.d.ts +68 -0
- package/dist/parameterized-scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack-file-snapshot.d.ts +71 -0
- package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -0
- package/dist/scriptable-pack-host.d.ts +70 -39
- package/dist/scriptable-pack-host.d.ts.map +1 -1
- package/dist/scriptable-pack-output-producers.d.ts +1 -0
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
- package/dist/scriptable-pack-staged-snapshot.d.ts +2 -2
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack.d.ts +13 -26
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/source-package-errors.d.ts.map +1 -1
- package/dist/source-package-publication.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/ies-contract.unit.test.ts +31 -0
- package/src/__tests__/ies-producer.unit.test.ts +104 -0
- package/src/__tests__/mesh-bin.test.ts +69 -0
- package/src/__tests__/mesh-lod-contract.unit.test.ts +92 -0
- package/src/__tests__/mesh-lod-reimport.integration.test.ts +15 -0
- package/src/__tests__/parameterized-scriptable-pack.unit.test.ts +270 -0
- package/src/__tests__/scriptable-pack-file-snapshot.unit.test.ts +177 -0
- package/src/__tests__/scriptable-pack-host.unit.test.ts +196 -4
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +7 -1
- package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +43 -12
- package/src/__tests__/source-package-publication-lod.integration.test.ts +29 -0
- package/src/__tests__/source-package-publication.integration.test.ts +26 -1
- package/src/browser.ts +5 -0
- package/src/build-production.ts +498 -141
- package/src/ies/ies-importer.ts +126 -0
- package/src/ies/parse-lm63.ts +98 -0
- package/src/ies/resample-type-c.ts +108 -0
- package/src/import-runner.ts +58 -0
- package/src/index.ts +49 -19
- package/src/mesh-bin.ts +38 -0
- package/src/mesh-lod.ts +265 -0
- package/src/parameterized-scriptable-pack.ts +725 -0
- package/src/scriptable-pack-file-snapshot.ts +346 -0
- package/src/scriptable-pack-host.ts +790 -343
- package/src/scriptable-pack-output-producers.ts +58 -1
- package/src/scriptable-pack-staged-snapshot.ts +12 -11
- package/src/scriptable-pack.ts +16 -485
- package/src/source-package-errors.ts +6 -0
- package/src/source-package-publication.ts +32 -2
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.integration.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +0 -1
- package/dist/scriptable-source-package.d.ts +0 -14
- package/dist/scriptable-source-package.d.ts.map +0 -1
- package/src/__tests__/scriptable-pack-product.contract.test.ts +0 -46
- package/src/__tests__/scriptable-pack.integration.test.ts +0 -435
- 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
|
+
}
|
package/src/import-runner.ts
CHANGED
|
@@ -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
|
-
|
|
107
|
-
type
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|