@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,265 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+
3
+ export interface MeshLodMetaEntry {
4
+ readonly sourceKey: string;
5
+ readonly meshGuid: string;
6
+ readonly screenCoverage?: number;
7
+ }
8
+
9
+ export interface MeshLodContractInput {
10
+ readonly lods: readonly Readonly<{ meshGuid: string; screenCoverage: number }>[];
11
+ readonly lodHysteresis?: number;
12
+ readonly rootMeshGuid?: string;
13
+ readonly refs?: readonly string[];
14
+ readonly generation?: number;
15
+ readonly rootBounds?: MeshLodBounds;
16
+ readonly lodBounds?: readonly MeshLodBounds[];
17
+ readonly rootMaterialSlots?: readonly MeshLodMaterialSlot[];
18
+ readonly lodMaterialSlots?: readonly (readonly MeshLodMaterialSlot[])[];
19
+ readonly relations?: readonly MeshLodRelation[];
20
+ }
21
+
22
+ export interface MeshLodBounds {
23
+ readonly min: readonly [number, number, number];
24
+ readonly max: readonly [number, number, number];
25
+ }
26
+
27
+ export interface MeshLodMaterialSlot {
28
+ readonly sourceKey: string;
29
+ }
30
+
31
+ export interface MeshLodRelation {
32
+ readonly from: string;
33
+ readonly to: string;
34
+ }
35
+
36
+ export type MeshLodContractError =
37
+ | { readonly code: 'mesh-lod-contract-invalid'; readonly reason: string }
38
+ | {
39
+ readonly code: 'mesh-lod-topology-change';
40
+ readonly reason: string;
41
+ readonly previousIndices: readonly number[];
42
+ readonly nextIndices: readonly number[];
43
+ }
44
+ | { readonly code: 'mesh-lod-authority-conflict'; readonly reason: string };
45
+
46
+ export interface ReconciledMeshLodMeta {
47
+ readonly lods: readonly (MeshLodMetaEntry & { readonly screenCoverage: number })[];
48
+ }
49
+
50
+ /** Generate the shared first-import screen coverage defaults, including LOD0. */
51
+ export function deriveDefaultLodScreenCoverages(levelCount: number): readonly number[] {
52
+ if (!Number.isInteger(levelCount) || levelCount < 1 || levelCount > 8) {
53
+ throw new RangeError('levelCount must be an integer in [1, 8]');
54
+ }
55
+ return Array.from(
56
+ { length: levelCount - 1 },
57
+ (_, index) => Math.round(0.5 * 0.4 ** index * 1_000_000) / 1_000_000,
58
+ );
59
+ }
60
+
61
+ export function validateMeshLodContract(
62
+ input: MeshLodContractInput,
63
+ ): Result<
64
+ Readonly<{ lods: readonly Readonly<{ meshGuid: string; screenCoverage: number }>[] }>,
65
+ MeshLodContractError
66
+ > {
67
+ if (input.lods.length > 7) {
68
+ return err({
69
+ code: 'mesh-lod-contract-invalid',
70
+ reason: 'at most seven lower-detail levels are supported',
71
+ });
72
+ }
73
+ if (
74
+ input.lodHysteresis !== undefined &&
75
+ (!Number.isFinite(input.lodHysteresis) || input.lodHysteresis < 0 || input.lodHysteresis >= 1)
76
+ ) {
77
+ return err({
78
+ code: 'mesh-lod-contract-invalid',
79
+ reason: 'lodHysteresis must be finite and in [0, 1)',
80
+ });
81
+ }
82
+ if (
83
+ input.generation !== undefined &&
84
+ (!Number.isSafeInteger(input.generation) || input.generation < 0)
85
+ ) {
86
+ return err({
87
+ code: 'mesh-lod-contract-invalid',
88
+ reason: 'generation must be a non-negative safe integer',
89
+ });
90
+ }
91
+ const guids = new Set<string>();
92
+ let previous = 1;
93
+ for (const level of input.lods) {
94
+ if (level.meshGuid.trim() === '' || guids.has(level.meshGuid)) {
95
+ return err({
96
+ code: 'mesh-lod-contract-invalid',
97
+ reason: 'LOD mesh GUIDs must be unique and non-empty',
98
+ });
99
+ }
100
+ if (
101
+ !Number.isFinite(level.screenCoverage) ||
102
+ level.screenCoverage <= 0 ||
103
+ level.screenCoverage > 1
104
+ ) {
105
+ return err({
106
+ code: 'mesh-lod-contract-invalid',
107
+ reason: 'screenCoverage must be finite and in (0, 1]',
108
+ });
109
+ }
110
+ if (level.screenCoverage >= previous) {
111
+ return err({
112
+ code: 'mesh-lod-contract-invalid',
113
+ reason: 'screenCoverage must strictly decrease by level',
114
+ });
115
+ }
116
+ guids.add(level.meshGuid);
117
+ previous = level.screenCoverage;
118
+ }
119
+ if (input.refs !== undefined) {
120
+ const refs = new Set(input.refs);
121
+ for (const level of input.lods) {
122
+ if (!refs.has(level.meshGuid)) {
123
+ return err({
124
+ code: 'mesh-lod-contract-invalid',
125
+ reason: 'every lower-detail mesh GUID must be enclosed by root refs',
126
+ });
127
+ }
128
+ }
129
+ }
130
+ if (
131
+ input.rootMeshGuid !== undefined &&
132
+ input.refs !== undefined &&
133
+ !input.refs.includes(input.rootMeshGuid)
134
+ ) {
135
+ return err({
136
+ code: 'mesh-lod-contract-invalid',
137
+ reason: 'root mesh GUID must be included in refs',
138
+ });
139
+ }
140
+ if (input.rootBounds !== undefined) {
141
+ if (!validBounds(input.rootBounds)) {
142
+ return err({
143
+ code: 'mesh-lod-contract-invalid',
144
+ reason: 'root bounds must be finite and ordered',
145
+ });
146
+ }
147
+ for (const bounds of input.lodBounds ?? []) {
148
+ if (!validBounds(bounds) || !encloses(input.rootBounds, bounds)) {
149
+ return err({
150
+ code: 'mesh-lod-contract-invalid',
151
+ reason: 'root bounds must enclose every lower-detail bounds',
152
+ });
153
+ }
154
+ }
155
+ }
156
+ if (input.lodBounds !== undefined && input.lodBounds.length !== input.lods.length) {
157
+ return err({
158
+ code: 'mesh-lod-contract-invalid',
159
+ reason: 'lod bounds must cover every lower level',
160
+ });
161
+ }
162
+ if (input.rootMaterialSlots !== undefined || input.lodMaterialSlots !== undefined) {
163
+ const rootSlots = input.rootMaterialSlots ?? [];
164
+ const lowerSlots = input.lodMaterialSlots ?? [];
165
+ if (
166
+ lowerSlots.length !== input.lods.length ||
167
+ lowerSlots.some((slots) => !sameSlots(rootSlots, slots))
168
+ ) {
169
+ return err({
170
+ code: 'mesh-lod-contract-invalid',
171
+ reason: 'lower-detail material slots must preserve root sourceKey order',
172
+ });
173
+ }
174
+ }
175
+ if (input.relations !== undefined && hasCycle(input.relations)) {
176
+ return err({ code: 'mesh-lod-contract-invalid', reason: 'LOD relations must be acyclic' });
177
+ }
178
+ return ok({ lods: input.lods });
179
+ }
180
+
181
+ function validBounds(bounds: MeshLodBounds): boolean {
182
+ const [minX, minY, minZ] = bounds.min;
183
+ const [maxX, maxY, maxZ] = bounds.max;
184
+ return (
185
+ [minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite) &&
186
+ minX <= maxX &&
187
+ minY <= maxY &&
188
+ minZ <= maxZ
189
+ );
190
+ }
191
+
192
+ function encloses(root: MeshLodBounds, child: MeshLodBounds): boolean {
193
+ return (
194
+ child.min[0] >= root.min[0] &&
195
+ child.min[1] >= root.min[1] &&
196
+ child.min[2] >= root.min[2] &&
197
+ child.max[0] <= root.max[0] &&
198
+ child.max[1] <= root.max[1] &&
199
+ child.max[2] <= root.max[2]
200
+ );
201
+ }
202
+
203
+ function sameSlots(
204
+ root: readonly MeshLodMaterialSlot[],
205
+ child: readonly MeshLodMaterialSlot[],
206
+ ): boolean {
207
+ return (
208
+ root.length === child.length &&
209
+ root.every((slot, index) => slot.sourceKey === child[index]?.sourceKey)
210
+ );
211
+ }
212
+
213
+ function hasCycle(relations: readonly MeshLodRelation[]): boolean {
214
+ const edges = new Map<string, string[]>();
215
+ for (const relation of relations)
216
+ edges.set(relation.from, [...(edges.get(relation.from) ?? []), relation.to]);
217
+ const visiting = new Set<string>();
218
+ const visited = new Set<string>();
219
+ const visit = (node: string): boolean => {
220
+ if (visiting.has(node)) return true;
221
+ if (visited.has(node)) return false;
222
+ visiting.add(node);
223
+ for (const next of edges.get(node) ?? []) if (visit(next)) return true;
224
+ visiting.delete(node);
225
+ visited.add(node);
226
+ return false;
227
+ };
228
+ return [...edges.keys()].some(visit);
229
+ }
230
+
231
+ /** Preserve sidecar facts by sourceKey while permitting only suffix changes. */
232
+ export function reconcileMeshLodMeta(
233
+ previous: readonly MeshLodMetaEntry[],
234
+ next: readonly MeshLodMetaEntry[],
235
+ ): Result<ReconciledMeshLodMeta, MeshLodContractError> {
236
+ const overlap = Math.min(previous.length, next.length);
237
+ for (let index = 0; index < overlap; index++) {
238
+ const oldEntry = previous[index];
239
+ const nextEntry = next[index];
240
+ if (oldEntry?.sourceKey !== nextEntry?.sourceKey) {
241
+ return err({
242
+ code: 'mesh-lod-topology-change',
243
+ reason: 'existing LOD source keys must remain prefix-stable',
244
+ previousIndices: [index],
245
+ nextIndices: [index],
246
+ });
247
+ }
248
+ if (oldEntry?.meshGuid !== nextEntry?.meshGuid) {
249
+ return err({
250
+ code: 'mesh-lod-authority-conflict',
251
+ reason: `sourceKey ${nextEntry?.sourceKey ?? '<missing>'} changed mesh GUID`,
252
+ });
253
+ }
254
+ }
255
+ const defaults = deriveDefaultLodScreenCoverages(next.length + 1);
256
+ const lods = next.map((entry, index) => ({
257
+ ...entry,
258
+ screenCoverage: previous[index]?.screenCoverage ?? entry.screenCoverage ?? defaults[index] ?? 0,
259
+ }));
260
+ const valid = validateMeshLodContract({
261
+ lods: lods.map(({ meshGuid, screenCoverage }) => ({ meshGuid, screenCoverage })),
262
+ });
263
+ if (!valid.ok) return valid;
264
+ return ok({ lods });
265
+ }