@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
package/README.md CHANGED
@@ -2,12 +2,28 @@
2
2
 
3
3
  ## Authoring and recovery index
4
4
 
5
+ ## 灯光资产 producer 入口
6
+
7
+ 三条最短入口:
8
+
9
+ 1. `RectAreaLight`、`SpotLight` 与 `LightProbe` 由 render 的 ECS schema 创作,scene producer 只外化 `shared<T>` GUID。
10
+ 2. `iesImporter` 只在 build-time 接受 LM-63 Type C `TILT=NONE`,输出固定 cooked `IesProfileAsset`;runtime 不解析 `.ies` 文本。
11
+ 3. `SpotLight.cookie` 走现有 `TextureAsset`/image producer;不要建立 Cookie registry 或第二条采样路径。
12
+
13
+ producer 持有 authored/source authority,Pack 产出 published 记录,Catalog 是 admitted/current 的投影;失败只返回结构化 `import-failed` 并保留原 GUID,修复 producer 后按同 GUID 重试。`lastKnownGood` 只可预览,不能冒充 verified/current。
14
+
15
+ Extended-lighting imports keep the same source key across Rect, Spot modifier,
16
+ Probe, and recovery carriers. IES is a build-time Type C `TILT=NONE` producer;
17
+ Cookie uses the existing texture producer; Probe and Sky are not new asset
18
+ registries. A failed publish is a structured producer result, and runtime must
19
+ not parse source text, invent a cooked payload, or promote LKG to current.
20
+
5
21
  ScriptablePack sources use the public `@forgeax/engine-import` producer bridge.
6
22
  The output is the same Pack v2 durable payload used by ordinary importers. The
7
- 16 `SCRIPTABLE_PACK_ASSET_KINDS` are `mesh`, `material`, `scene`, `texture`,
23
+ 17 `SCRIPTABLE_PACK_ASSET_KINDS` are `mesh`, `material`, `scene`, `texture`,
8
24
  `equirect`, `sampler`, `font`, `render-pipeline`, `tileset`, `video`,
9
25
  `skeleton`, `skin`, `animation-clip`, `animation-graph`, `audio`, and
10
- `particle-effect`; all are loadable by GUID, while `refs`,
26
+ `particle-effect`, and `ies-profile`; all are loadable by GUID, while `refs`,
11
27
  `artifacts`, `mediaType`, and `programFingerprint` remain producer facts.
12
28
  Producer or dependency failures return `code`, `expected`, `hint`, and
13
29
  `detail`; inspect the owning evidence, rebuild or cold-cook, and retry.
@@ -23,6 +39,28 @@ The import contract is [`asset-authority.schema.json`](../../asset-authority.sch
23
39
 
24
40
  The Importer owns neither DDC lifecycle nor Editor authoring writes. Editor writes go through its asset-authoring gateway, and runtime consumes the validated projection.
25
41
 
42
+ ### Parameterized Pack generation
43
+
44
+ `buildParameterizedScriptablePack()` accepts a Pack v2 definition, resolves the
45
+ default/inherited values, and executes `build()` once for the current subject
46
+ identity. The result is a dynamic `sourceKey -> Asset` map; the bridge derives
47
+ `AssetGuid` values from `(subjectPackageId, sourceKey)` and sends the resulting
48
+ assets through the same registered output producers and Pack v2 finalizer used
49
+ by ordinary imports.
50
+
51
+ `buildParameterizedScriptablePackWorklist()` is the internal clean-generation
52
+ fixed point. Subjects are evaluated in stable order, a forward `readByGuid()`
53
+ content miss waits for the subject that can materialize that GUID, and a pass
54
+ with no progress returns `pack-content-dependency-stalled`. Once all subjects
55
+ settle, the bridge validates references and incoming-reference deletions before
56
+ publication. A GUID written into a payload is only a reference dependency;
57
+ only `readByGuid()` creates a content dependency.
58
+
59
+ `produceParameterizedScriptablePackProducts()` and
60
+ `materializePreparedParameterizedScriptablePack()` adapt that result to the
61
+ existing DDC/Catalog/Pack v2 publication tuple. No parameter, parent, sourceKey,
62
+ or TypeScript execution contract crosses into the runtime package.
63
+
26
64
  Build-time asset **import** runner + `ImporterRegistry` — the build-time half of
27
65
  the engine's import/load split.
28
66
 
@@ -103,7 +141,7 @@ flowchart LR
103
141
 
104
142
  Domain producers return structured `ImportError`; the generic bridge contains no per-kind switch. `createStandardAssetOutputProducerRegistry(sceneComponents)` composes the production Scene, Material, and Mesh owners with the definition-bound scene schema. Scene refs use the ECS externalization kernel shared with runtime save, Material refs cover parent/texture/sampler GUIDs, and Mesh emits the versioned binary artifact. The fingerprint covers canonical Meta, the definition-bound scene schema, the recursive source closure, observed external evidence, authoring contract version, and registered producer versions.
105
143
 
106
- `createScriptablePackStagedAssetSnapshotSource()` owns clean-build content reads. It resolves a GUID to its local source owner, lazily builds that owner inside one fixed generation, memoizes private clones and digests, and reports `pack-source-build-cycle` without consulting an older fallback generation. A fallback is only an explicit prebuilt authority for GUIDs with no local owner.
144
+ `createScriptablePackStagedAssetSnapshotSource()` owns clean-build content reads. It resolves a GUID to its local source owner, lazily builds that owner inside one fixed generation, memoizes private clones and digests, and reports `pack-content-dependency-stalled` for a cycle without consulting an older fallback generation. A fallback is only an explicit prebuilt authority for GUIDs with no local owner.
107
145
 
108
146
  `createStandardAssetOutputProducerRegistry(sceneComponents)` registers the production
109
147
  Material, Mesh, and Scene producers. Material output derives
@@ -132,6 +170,13 @@ binary metadata.
132
170
  > so the importer cannot publish a partial mesh product. v2/v3 mesh binaries
133
171
  > are not compatibility inputs.
134
172
 
173
+ For prebuilt ordinary Pack dependencies, use
174
+ `createScriptablePackFileAssetSnapshotSource({ assetRoots })` from the same
175
+ package. It recursively indexes validated `.pack.json` assets by GUID and
176
+ returns private `ScriptablePackAssetSnapshot` clones with deterministic
177
+ generation/digest evidence; missing, malformed, unreadable, and colliding
178
+ inputs remain structured `Result` errors.
179
+
135
180
  At the low-level `buildScriptablePack` boundary, `assetSource` is optional only
136
181
  when `build` never calls `reader.readByGuid`. The first attempted content read
137
182
  without a source returns structured `asset-not-found`; no empty snapshot or
@@ -220,8 +265,51 @@ Result boundary. A digest-capability refusal during CookProduct finalization
220
265
  does not publish a partial product; repair that capability and retry the same
221
266
  Meta/GUID through the same `ImporterRegistry`.
222
267
 
268
+ ### Source-format LOD adapters
269
+
270
+ Format adapters project producer facts into the same `MeshAsset.lods` and Pack
271
+ closure. The adapter owns interpretation; the sidecar owns retained author
272
+ facts and the importer only produces validated derived output.
273
+
274
+ | Source fact | Normalized result | Required closure |
275
+ |:--|:--|:--|
276
+ | glTF `MSFT_lod` node relation | ordered root and lower mesh GUIDs | root `refs[]` contains every lower mesh |
277
+ | FBX `FbxLODGroup` level relation | ordered root and lower mesh GUIDs | root `refs[]` contains every lower mesh |
278
+ | sidecar `sourceOverrides.mesh.lods` | retained `screenCoverage` and source keys | every GUID resolves in the same package |
279
+
280
+ On first import the helper derives lower-level coverage with
281
+
282
+ $$c_i = round6(0.5 \times 0.4^{i-1})$$
283
+
284
+ for `i = 1 .. n`, where `n` is the number of lower levels. This yields
285
+ `0.5` for two total LODs, `0.5, 0.2` for three, and `0.5, 0.2, 0.08`
286
+ for four. Existing legal sidecar values always win; only a newly appended
287
+ suffix receives defaults. A changed middle source key or mesh GUID is rejected
288
+ and the previous sidecar plus last-known-good product remains authoritative.
289
+
223
290
  The import package stops at build-time source conversion and product
224
291
  publication. It does not write authoring state, own Editor operations, or add
225
292
  runtime transport branches. A dev `ImportTransport` is an explicit host
226
293
  adapter; a shipped bundle must carry its build-time product and keeps
227
294
  `asset-not-imported` fail-fast semantics.
295
+
296
+ ## Mesh LOD normalization and recovery
297
+
298
+ The shared helper in `src/mesh-lod.ts` owns validation, default coverage, and
299
+ prefix-stable reimport. For `n` lower levels, first-import defaults are derived
300
+ once with:
301
+
302
+ $$c_i = round6(0.5 * 0.4^{i-1}), 1 <= i <= n$$
303
+
304
+ Thus two levels produce `0.5`, three produce `0.5, 0.2`, and four produce
305
+ `0.5, 0.2, 0.08`. Existing legal sidecar values win; newly appended suffix
306
+ levels receive defaults. A middle sourceKey or GUID change is a structured
307
+ topology/authority error, so the previous sidecar and LKG remain intact.
308
+
309
+ ```mermaid
310
+ flowchart LR
311
+ A["source plus Meta"] --> B["validate LOD facts"]
312
+ B -->|"valid"| C["reconcile prefix and defaults"]
313
+ C --> D["publish Pack and Catalog"]
314
+ B -->|"invalid"| E["restore old sidecar and LKG"]
315
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ies-contract.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ies-contract.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ies-contract.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ies-producer.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ies-producer.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ies-producer.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mesh-lod-contract.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-lod-contract.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mesh-lod-contract.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mesh-lod-reimport.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-lod-reimport.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mesh-lod-reimport.integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parameterized-scriptable-pack.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameterized-scriptable-pack.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parameterized-scriptable-pack.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scriptable-pack-file-snapshot.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scriptable-pack-file-snapshot.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scriptable-pack-file-snapshot.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=source-package-publication-lod.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-package-publication-lod.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/source-package-publication-lod.integration.test.ts"],"names":[],"mappings":""}
package/dist/browser.d.ts CHANGED
@@ -2,4 +2,5 @@ export { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
2
2
  export { type DdcPack, type ImportRunnerFs, normaliseForPack, type RunImportMeta, type RunImportOk, type RunImportProductResult, type RunImportResult, runImport, SHADER_RESERVED_IMPORTER_KEY, } from './import-runner.js';
3
3
  export { ImporterRegistry } from './importer-registry.js';
4
4
  export { packMeshBinV4 } from './mesh-bin.js';
5
+ export { deriveDefaultLodScreenCoverages, reconcileMeshLodMeta, validateMeshLodContract, } from './mesh-lod.js';
5
6
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,SAAS,EACT,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,SAAS,EACT,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,+BAA+B,EAC/B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,eAAe,CAAC"}
package/dist/browser.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { validateSourceOverrideMap, ImportError, IMPORT_ERROR_HINTS, canonicalizeSourceOverrides, err, ok } from '@forgeax/engine-types';
1
+ import { err, ok, validateSourceOverrideMap, ImportError, IMPORT_ERROR_HINTS, canonicalizeSourceOverrides } from '@forgeax/engine-types';
2
2
  export { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
3
3
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
4
4
  import { AssetGuid } from '@forgeax/engine-pack/guid';
@@ -113,6 +113,171 @@ function finalizeImportProducts(product, inputFingerprint) {
113
113
  return products;
114
114
  })();
115
115
  }
116
+ function deriveDefaultLodScreenCoverages(levelCount) {
117
+ if (!Number.isInteger(levelCount) || levelCount < 1 || levelCount > 8) {
118
+ throw new RangeError("levelCount must be an integer in [1, 8]");
119
+ }
120
+ return Array.from(
121
+ { length: levelCount - 1 },
122
+ (_, index) => Math.round(0.5 * 0.4 ** index * 1e6) / 1e6
123
+ );
124
+ }
125
+ function validateMeshLodContract(input) {
126
+ if (input.lods.length > 7) {
127
+ return err({
128
+ code: "mesh-lod-contract-invalid",
129
+ reason: "at most seven lower-detail levels are supported"
130
+ });
131
+ }
132
+ if (input.lodHysteresis !== void 0 && (!Number.isFinite(input.lodHysteresis) || input.lodHysteresis < 0 || input.lodHysteresis >= 1)) {
133
+ return err({
134
+ code: "mesh-lod-contract-invalid",
135
+ reason: "lodHysteresis must be finite and in [0, 1)"
136
+ });
137
+ }
138
+ if (input.generation !== void 0 && (!Number.isSafeInteger(input.generation) || input.generation < 0)) {
139
+ return err({
140
+ code: "mesh-lod-contract-invalid",
141
+ reason: "generation must be a non-negative safe integer"
142
+ });
143
+ }
144
+ const guids = /* @__PURE__ */ new Set();
145
+ let previous = 1;
146
+ for (const level of input.lods) {
147
+ if (level.meshGuid.trim() === "" || guids.has(level.meshGuid)) {
148
+ return err({
149
+ code: "mesh-lod-contract-invalid",
150
+ reason: "LOD mesh GUIDs must be unique and non-empty"
151
+ });
152
+ }
153
+ if (!Number.isFinite(level.screenCoverage) || level.screenCoverage <= 0 || level.screenCoverage > 1) {
154
+ return err({
155
+ code: "mesh-lod-contract-invalid",
156
+ reason: "screenCoverage must be finite and in (0, 1]"
157
+ });
158
+ }
159
+ if (level.screenCoverage >= previous) {
160
+ return err({
161
+ code: "mesh-lod-contract-invalid",
162
+ reason: "screenCoverage must strictly decrease by level"
163
+ });
164
+ }
165
+ guids.add(level.meshGuid);
166
+ previous = level.screenCoverage;
167
+ }
168
+ if (input.refs !== void 0) {
169
+ const refs = new Set(input.refs);
170
+ for (const level of input.lods) {
171
+ if (!refs.has(level.meshGuid)) {
172
+ return err({
173
+ code: "mesh-lod-contract-invalid",
174
+ reason: "every lower-detail mesh GUID must be enclosed by root refs"
175
+ });
176
+ }
177
+ }
178
+ }
179
+ if (input.rootMeshGuid !== void 0 && input.refs !== void 0 && !input.refs.includes(input.rootMeshGuid)) {
180
+ return err({
181
+ code: "mesh-lod-contract-invalid",
182
+ reason: "root mesh GUID must be included in refs"
183
+ });
184
+ }
185
+ if (input.rootBounds !== void 0) {
186
+ if (!validBounds(input.rootBounds)) {
187
+ return err({
188
+ code: "mesh-lod-contract-invalid",
189
+ reason: "root bounds must be finite and ordered"
190
+ });
191
+ }
192
+ for (const bounds of input.lodBounds ?? []) {
193
+ if (!validBounds(bounds) || !encloses(input.rootBounds, bounds)) {
194
+ return err({
195
+ code: "mesh-lod-contract-invalid",
196
+ reason: "root bounds must enclose every lower-detail bounds"
197
+ });
198
+ }
199
+ }
200
+ }
201
+ if (input.lodBounds !== void 0 && input.lodBounds.length !== input.lods.length) {
202
+ return err({
203
+ code: "mesh-lod-contract-invalid",
204
+ reason: "lod bounds must cover every lower level"
205
+ });
206
+ }
207
+ if (input.rootMaterialSlots !== void 0 || input.lodMaterialSlots !== void 0) {
208
+ const rootSlots = input.rootMaterialSlots ?? [];
209
+ const lowerSlots = input.lodMaterialSlots ?? [];
210
+ if (lowerSlots.length !== input.lods.length || lowerSlots.some((slots) => !sameSlots(rootSlots, slots))) {
211
+ return err({
212
+ code: "mesh-lod-contract-invalid",
213
+ reason: "lower-detail material slots must preserve root sourceKey order"
214
+ });
215
+ }
216
+ }
217
+ if (input.relations !== void 0 && hasCycle(input.relations)) {
218
+ return err({ code: "mesh-lod-contract-invalid", reason: "LOD relations must be acyclic" });
219
+ }
220
+ return ok({ lods: input.lods });
221
+ }
222
+ function validBounds(bounds) {
223
+ const [minX, minY, minZ] = bounds.min;
224
+ const [maxX, maxY, maxZ] = bounds.max;
225
+ return [minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite) && minX <= maxX && minY <= maxY && minZ <= maxZ;
226
+ }
227
+ function encloses(root, child) {
228
+ return child.min[0] >= root.min[0] && child.min[1] >= root.min[1] && child.min[2] >= root.min[2] && child.max[0] <= root.max[0] && child.max[1] <= root.max[1] && child.max[2] <= root.max[2];
229
+ }
230
+ function sameSlots(root, child) {
231
+ return root.length === child.length && root.every((slot, index) => slot.sourceKey === child[index]?.sourceKey);
232
+ }
233
+ function hasCycle(relations) {
234
+ const edges = /* @__PURE__ */ new Map();
235
+ for (const relation of relations)
236
+ edges.set(relation.from, [...edges.get(relation.from) ?? [], relation.to]);
237
+ const visiting = /* @__PURE__ */ new Set();
238
+ const visited = /* @__PURE__ */ new Set();
239
+ const visit = (node) => {
240
+ if (visiting.has(node)) return true;
241
+ if (visited.has(node)) return false;
242
+ visiting.add(node);
243
+ for (const next of edges.get(node) ?? []) if (visit(next)) return true;
244
+ visiting.delete(node);
245
+ visited.add(node);
246
+ return false;
247
+ };
248
+ return [...edges.keys()].some(visit);
249
+ }
250
+ function reconcileMeshLodMeta(previous, next) {
251
+ const overlap = Math.min(previous.length, next.length);
252
+ for (let index = 0; index < overlap; index++) {
253
+ const oldEntry = previous[index];
254
+ const nextEntry = next[index];
255
+ if (oldEntry?.sourceKey !== nextEntry?.sourceKey) {
256
+ return err({
257
+ code: "mesh-lod-topology-change",
258
+ reason: "existing LOD source keys must remain prefix-stable",
259
+ previousIndices: [index],
260
+ nextIndices: [index]
261
+ });
262
+ }
263
+ if (oldEntry?.meshGuid !== nextEntry?.meshGuid) {
264
+ return err({
265
+ code: "mesh-lod-authority-conflict",
266
+ reason: `sourceKey ${nextEntry?.sourceKey ?? "<missing>"} changed mesh GUID`
267
+ });
268
+ }
269
+ }
270
+ const defaults = deriveDefaultLodScreenCoverages(next.length + 1);
271
+ const lods = next.map((entry, index) => ({
272
+ ...entry,
273
+ screenCoverage: previous[index]?.screenCoverage ?? entry.screenCoverage ?? defaults[index] ?? 0
274
+ }));
275
+ const valid = validateMeshLodContract({
276
+ lods: lods.map(({ meshGuid, screenCoverage }) => ({ meshGuid, screenCoverage }))
277
+ });
278
+ if (!valid.ok) return valid;
279
+ return ok({ lods });
280
+ }
116
281
 
117
282
  // src/import-runner.ts
118
283
  var SHADER_RESERVED_IMPORTER_KEY = "shader";
@@ -125,6 +290,9 @@ function isModuleLoadFailure(e) {
125
290
  const msg = e.message;
126
291
  return msg.includes("Cannot find module") || msg.includes("native addon") || msg.includes(".node");
127
292
  }
293
+ function declarationsSourceKey(declarations, guid) {
294
+ return declarations.find((declaration) => declaration.guid === guid)?.sourceKey;
295
+ }
128
296
  function normaliseForPack(value) {
129
297
  if (value === null || value === void 0) return value;
130
298
  if (value instanceof Float32Array || value instanceof Float64Array || value instanceof Uint8Array || value instanceof Uint16Array || value instanceof Uint32Array || value instanceof Int8Array || value instanceof Int16Array || value instanceof Int32Array) {
@@ -434,6 +602,37 @@ async function runImport(meta, registry, fs) {
434
602
  );
435
603
  }
436
604
  const produced = product.assets;
605
+ for (const asset of produced) {
606
+ if (asset.kind !== "mesh" || asset.payload === null || typeof asset.payload !== "object")
607
+ continue;
608
+ const payload = asset.payload;
609
+ if (payload.lods === void 0) continue;
610
+ const lods = payload.lods.map((level) => ({
611
+ meshGuid: typeof level.meshGuid === "string" ? level.meshGuid : typeof level.mesh === "string" ? level.mesh : JSON.stringify(level.mesh),
612
+ screenCoverage: level.screenCoverage
613
+ }));
614
+ const validated = validateMeshLodContract({
615
+ lods,
616
+ ...payload.lodHysteresis === void 0 ? {} : { lodHysteresis: payload.lodHysteresis }
617
+ });
618
+ if (!validated.ok) {
619
+ return errResult(
620
+ new ImportError({
621
+ code: validated.error.code,
622
+ expected: "MeshAsset LOD facts to satisfy the shared contract",
623
+ hint: IMPORT_ERROR_HINTS[validated.error.code],
624
+ detail: {
625
+ meshLodSourceKey: declarationsSourceKey(meta.subAssets, asset.guid),
626
+ reason: validated.error.reason,
627
+ ...validated.error.code === "mesh-lod-topology-change" ? {
628
+ previousIndices: validated.error.previousIndices,
629
+ nextIndices: validated.error.nextIndices
630
+ } : {}
631
+ }
632
+ })
633
+ );
634
+ }
635
+ }
437
636
  const declared = new Set(meta.subAssets.map((s) => s.guid));
438
637
  const producedGuids = new Set(produced.map((a) => a.guid));
439
638
  const unexpectedGuids = [...producedGuids].filter((g) => !declared.has(g));
@@ -651,12 +850,40 @@ function refsMeta(payload, refs) {
651
850
  };
652
851
  }
653
852
  );
853
+ if (payload.lods !== void 0 && payload.lods.length > 7) {
854
+ throw new Error("MeshAsset LOD chain supports at most seven lower-detail levels");
855
+ }
856
+ let previousCoverage = 1;
857
+ const seenLodGuids = /* @__PURE__ */ new Set();
858
+ const lods = payload.lods?.map((lod, lodIndex) => {
859
+ const guid = AssetGuid.format(lod.mesh).toLowerCase();
860
+ const meshRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
861
+ if (meshRef < 0) {
862
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is absent from refs`);
863
+ }
864
+ if (seenLodGuids.has(guid)) {
865
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is duplicated`);
866
+ }
867
+ if (!Number.isFinite(lod.screenCoverage) || lod.screenCoverage <= 0 || lod.screenCoverage > 1 || lod.screenCoverage >= previousCoverage) {
868
+ throw new Error(
869
+ `LOD ${lodIndex} screenCoverage must be finite, in (0, 1], and strictly decreasing`
870
+ );
871
+ }
872
+ seenLodGuids.add(guid);
873
+ previousCoverage = lod.screenCoverage;
874
+ return { meshRef, screenCoverage: lod.screenCoverage };
875
+ });
876
+ if (payload.lodHysteresis !== void 0 && (!Number.isFinite(payload.lodHysteresis) || payload.lodHysteresis < 0 || payload.lodHysteresis >= 1)) {
877
+ throw new Error("lodHysteresis must be finite and in [0, 1)");
878
+ }
654
879
  return {
655
880
  submeshes: payload.submeshes === void 0 || payload.submeshes.length === 0 ? [{ indexOffset: 0, indexCount: payload.indices?.length ?? 0, materialSlot: 0 }] : payload.submeshes,
656
881
  materialSlots,
657
882
  ...payload.aabb === void 0 ? {} : { aabb: jsonValue(payload.aabb) },
658
883
  ...payload.morphTargets === void 0 ? {} : { morphTargets: jsonValue(payload.morphTargets) },
659
- ...payload.morphWeights === void 0 ? {} : { morphWeights: jsonValue(payload.morphWeights) }
884
+ ...payload.morphWeights === void 0 ? {} : { morphWeights: jsonValue(payload.morphWeights) },
885
+ ...lods === void 0 ? {} : { lods },
886
+ ...payload.lodHysteresis === void 0 ? {} : { lodHysteresis: payload.lodHysteresis }
660
887
  };
661
888
  }
662
889
  function packMeshBinV4(payload, sourceKey, refs = []) {
@@ -780,6 +1007,6 @@ function packMeshBinV4(payload, sourceKey, refs = []) {
780
1007
  }
781
1008
  }
782
1009
 
783
- export { ImporterRegistry, SHADER_RESERVED_IMPORTER_KEY, normaliseForPack, packMeshBinV4, runImport };
1010
+ export { ImporterRegistry, SHADER_RESERVED_IMPORTER_KEY, deriveDefaultLodScreenCoverages, normaliseForPack, packMeshBinV4, reconcileMeshLodMeta, runImport, validateMeshLodContract };
784
1011
  //# sourceMappingURL=browser.mjs.map
785
1012
  //# sourceMappingURL=browser.mjs.map