@forgeax/engine-import 0.1.2

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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +226 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/decode-image-owner.test-d.d.ts +2 -0
  5. package/dist/__tests__/decode-image-owner.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/import-contract-migration.test.d.ts +2 -0
  7. package/dist/__tests__/import-contract-migration.test.d.ts.map +1 -0
  8. package/dist/__tests__/import-dependencies.test.d.ts +2 -0
  9. package/dist/__tests__/import-dependencies.test.d.ts.map +1 -0
  10. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts +2 -0
  11. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts.map +1 -0
  12. package/dist/__tests__/import-local-artifacts.test.d.ts +2 -0
  13. package/dist/__tests__/import-local-artifacts.test.d.ts.map +1 -0
  14. package/dist/__tests__/import.unit.test.d.ts +2 -0
  15. package/dist/__tests__/import.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/material-import-product.unit.test.d.ts +2 -0
  17. package/dist/__tests__/material-import-product.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
  19. package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
  20. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  21. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  22. package/dist/__tests__/mesh-bin.test.d.ts +2 -0
  23. package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
  24. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts +2 -0
  25. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts +2 -0
  27. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts +2 -0
  29. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/scriptable-pack.integration.test.d.ts +2 -0
  31. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts +2 -0
  33. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts.map +1 -0
  34. package/dist/import-product.d.ts +36 -0
  35. package/dist/import-product.d.ts.map +1 -0
  36. package/dist/import-runner.d.ts +133 -0
  37. package/dist/import-runner.d.ts.map +1 -0
  38. package/dist/importer-registry.d.ts +40 -0
  39. package/dist/importer-registry.d.ts.map +1 -0
  40. package/dist/index.d.ts +10 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.mjs +1607 -0
  43. package/dist/index.mjs.map +1 -0
  44. package/dist/mesh-bin.d.ts +19 -0
  45. package/dist/mesh-bin.d.ts.map +1 -0
  46. package/dist/scriptable-pack-output-producers.d.ts +6 -0
  47. package/dist/scriptable-pack-output-producers.d.ts.map +1 -0
  48. package/dist/scriptable-pack-staged-snapshot.d.ts +22 -0
  49. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -0
  50. package/dist/scriptable-pack.d.ts +153 -0
  51. package/dist/scriptable-pack.d.ts.map +1 -0
  52. package/package.json +63 -0
  53. package/src/__tests__/decode-image-owner.test-d.ts +53 -0
  54. package/src/__tests__/import-contract-migration.test.ts +190 -0
  55. package/src/__tests__/import-dependencies.test.ts +65 -0
  56. package/src/__tests__/import-finalizer-digest-retry.test.ts +113 -0
  57. package/src/__tests__/import-local-artifacts.test.ts +100 -0
  58. package/src/__tests__/import.unit.test.ts +429 -0
  59. package/src/__tests__/material-import-product.unit.test.ts +56 -0
  60. package/src/__tests__/mesh-bin-contract.test.ts +95 -0
  61. package/src/__tests__/mesh-bin-morph-v3.test.ts +44 -0
  62. package/src/__tests__/mesh-bin.test.ts +102 -0
  63. package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +190 -0
  64. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +429 -0
  65. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +258 -0
  66. package/src/__tests__/scriptable-pack.integration.test.ts +435 -0
  67. package/src/__tests__/source-overrides-import-context.unit.test.ts +92 -0
  68. package/src/import-product.ts +195 -0
  69. package/src/import-runner.ts +720 -0
  70. package/src/importer-registry.ts +80 -0
  71. package/src/index.ts +103 -0
  72. package/src/mesh-bin.ts +229 -0
  73. package/src/scriptable-pack-output-producers.ts +466 -0
  74. package/src/scriptable-pack-staged-snapshot.ts +161 -0
  75. package/src/scriptable-pack.ts +615 -0
@@ -0,0 +1,80 @@
1
+ // @forgeax/engine-import - ImporterRegistry (feat-20260603-asset-import-loader-injection M2 / w14).
2
+ //
3
+ // The build-time twin of the runtime LoaderRegistry
4
+ // (packages/runtime/src/loader-registry.ts): an injectable table that maps a
5
+ // `meta.importer` string key -> an `Importer` (the contract SSOT lives in
6
+ // `@forgeax/engine-types`). The import runner holds one of these and dispatches
7
+ // on `meta.importer` through `get(key)` instead of a hardcoded
8
+ // `if (importer === ...)` chain (plan-strategy D-1).
9
+ //
10
+ // Shape mirrors the runtime LoaderRegistry + Console `Registry`
11
+ // (packages/console/src/registry.ts) register/lookup pattern (research
12
+ // Finding 8). The injected unit is an **object** `{ key, import }`, not a bare
13
+ // function, so a "one importer key -> one Importer" dispatch table is the
14
+ // natural shape.
15
+ //
16
+ // Fail-fast semantics (charter P3): `register` throws on a malformed importer
17
+ // (empty key or non-function `import`) at wire time, so a misconfigured host
18
+ // surfaces immediately rather than at the first import run. `register` is
19
+ // idempotent on a repeated key (last write wins, no throw) so re-wiring a
20
+ // registry across build invocations is safe.
21
+
22
+ import type { Importer } from '@forgeax/engine-types';
23
+
24
+ /**
25
+ * Injectable `meta.importer` -> {@link Importer} table held by the import
26
+ * runner.
27
+ *
28
+ * @example Wire + dispatch (build tooling side)
29
+ * ```ts
30
+ * import { ImporterRegistry } from '@forgeax/engine-import';
31
+ * import { gltfImporter } from '@forgeax/engine-gltf';
32
+ * const importers = new ImporterRegistry();
33
+ * importers.register(gltfImporter);
34
+ * const gltf = importers.get('gltf'); // Importer | undefined
35
+ * ```
36
+ */
37
+ export class ImporterRegistry {
38
+ private readonly importers = new Map<string, Importer>();
39
+
40
+ /**
41
+ * Register an importer for its `importer.key`. Fail-fast on a malformed
42
+ * importer (charter P3); idempotent on a repeated key (last write wins).
43
+ *
44
+ * @param importer the `{ key, import }` object to register.
45
+ * @throws TypeError when `importer.key` is empty or `importer.import` is not
46
+ * a function - a wire-time misconfiguration the host must fix.
47
+ */
48
+ register(importer: Importer): void {
49
+ if (typeof importer.key !== 'string' || importer.key.length === 0) {
50
+ throw new TypeError(
51
+ `ImporterRegistry.register: importer.key must be a non-empty string (got ${JSON.stringify(importer.key)})`,
52
+ );
53
+ }
54
+ if (typeof importer.import !== 'function') {
55
+ throw new TypeError(
56
+ `ImporterRegistry.register: importer.import must be a function for key "${importer.key}"`,
57
+ );
58
+ }
59
+ this.importers.set(importer.key, importer);
60
+ }
61
+
62
+ /**
63
+ * Look up the importer registered for `key`. Returns `undefined` when no
64
+ * importer is wired - the import runner maps that to a structured
65
+ * `ImportError(code='importer-not-registered')` with the registered keys in
66
+ * `.detail.registeredImporters` (charter P3).
67
+ */
68
+ get(key: string): Importer | undefined {
69
+ return this.importers.get(key);
70
+ }
71
+
72
+ /**
73
+ * The importer keys currently wired, in insertion order. Fed into the
74
+ * `importer-not-registered` error `.detail.registeredImporters` so AI users
75
+ * see exactly what is injectable.
76
+ */
77
+ registeredImporters(): readonly string[] {
78
+ return [...this.importers.keys()];
79
+ }
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ // @forgeax/engine-import — build-time asset import runner + ImporterRegistry.
2
+ //
3
+ // The build-time half of the engine's import/load split (the runtime half is
4
+ // the LoaderRegistry in @forgeax/engine-runtime). An Importer turns an external
5
+ // source (.gltf / .png / .ttf) plus its *.meta.json GUID declarations into
6
+ // in-memory ImportedAsset[] PODs; the import runner enforces the GUID
7
+ // import-stable iron law and writes the DDC (.pack.json / .bin).
8
+ //
9
+ // This package is build-time only. It MUST NOT enter the player runtime bundle
10
+ // (AC-06): @forgeax/engine-runtime / @forgeax/engine-app never depend on it.
11
+ //
12
+ // The import contract (Importer / ImportContext / ImportedAsset / ImportError /
13
+ // ImportErrorCode / ImportTransport) lives in @forgeax/engine-types (the
14
+ // math-free SSOT) and is re-exported here so build tooling has a single
15
+ // import surface.
16
+
17
+ export type {
18
+ AnimationClip,
19
+ AnimationGraph,
20
+ Asset,
21
+ AudioClipAsset,
22
+ EquirectAsset,
23
+ FontAsset,
24
+ MaterialAsset,
25
+ MeshAsset,
26
+ ParticleEffectAsset,
27
+ RenderPipelineAsset,
28
+ SamplerAsset,
29
+ SceneAsset,
30
+ SkeletonAsset,
31
+ SkinAsset,
32
+ TextureAsset,
33
+ TilesetAsset,
34
+ VideoAsset,
35
+ } from '@forgeax/engine-types';
36
+ export {
37
+ type CookProduct,
38
+ IMPORT_ERROR_HINTS,
39
+ type ImportContext,
40
+ ImportError,
41
+ type ImportErrorCode,
42
+ type ImportErrorDetail,
43
+ type ImportedAsset,
44
+ type Importer,
45
+ type ImportSubAsset,
46
+ type ImportTransport,
47
+ } from '@forgeax/engine-types';
48
+ export {
49
+ createMaterialImportProduct,
50
+ finalizeImportProducts,
51
+ type MaterialImportProduct,
52
+ type MaterialImportProductError,
53
+ type MaterialImportProductInput,
54
+ type MaterialImportRefs,
55
+ type MaterialSourceEvidence,
56
+ materialImportProductReady,
57
+ } from './import-product.js';
58
+ export {
59
+ type DdcPack,
60
+ type ImportRunnerFs,
61
+ normaliseForPack,
62
+ type RunImportMeta,
63
+ type RunImportOk,
64
+ type RunImportProductResult,
65
+ type RunImportResult,
66
+ runImport,
67
+ SHADER_RESERVED_IMPORTER_KEY,
68
+ } from './import-runner.js';
69
+ export { ImporterRegistry } from './importer-registry.js';
70
+ export {
71
+ type MeshBinEncodeError,
72
+ packMeshBinV4,
73
+ } from './mesh-bin.js';
74
+ export {
75
+ type AssetOutputInput,
76
+ type AssetOutputPayload,
77
+ type AssetOutputProducer,
78
+ AssetOutputProducerRegistry,
79
+ type AssetOutputProduct,
80
+ type BuildScriptablePackOptions,
81
+ buildScriptablePack,
82
+ type ScriptablePackAssetSnapshot,
83
+ type ScriptablePackAssetSnapshotSource,
84
+ type ScriptablePackBuildBridgeResult,
85
+ type ScriptablePackBuildProduct,
86
+ type ScriptablePackDomainError,
87
+ type ScriptablePackExternalEvidence,
88
+ type ScriptablePackExternalUsage,
89
+ type ScriptablePackSourceClosureEntry,
90
+ type ScriptablePackStagedOutput,
91
+ } from './scriptable-pack.js';
92
+ export {
93
+ createStandardAssetOutputProducerRegistry,
94
+ materialAssetOutputProducer,
95
+ meshAssetOutputProducer,
96
+ sceneAssetOutputProducer,
97
+ } from './scriptable-pack-output-producers.js';
98
+ export {
99
+ createScriptablePackStagedAssetSnapshotSource,
100
+ type ScriptablePackSnapshotError,
101
+ type ScriptablePackStagedOwner,
102
+ type ScriptablePackStagedSnapshotOptions,
103
+ } from './scriptable-pack-staged-snapshot.js';
@@ -0,0 +1,229 @@
1
+ import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
2
+ import {
3
+ MESH_BIN_HEADER_V4_BYTES,
4
+ type MeshBinContractError,
5
+ type MeshBinHeaderV4,
6
+ writeMeshBinHeader,
7
+ } from '@forgeax/engine-pack';
8
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
9
+ import {
10
+ err,
11
+ type MeshAsset,
12
+ ok,
13
+ type Result,
14
+ type VertexAttributeMap,
15
+ } from '@forgeax/engine-types';
16
+
17
+ type MeshPayloadIn = { readonly [Key in keyof MeshAsset]?: MeshAsset[Key] | undefined } & {
18
+ readonly vertexCount?: number;
19
+ };
20
+
21
+ export type MeshBinEncodeError =
22
+ | MeshBinContractError
23
+ | {
24
+ readonly code: 'mesh-bin-payload-invalid';
25
+ readonly subject: 'mesh-bin';
26
+ readonly sourceKey: string;
27
+ readonly expected: string;
28
+ readonly actual: string;
29
+ readonly recovery: string;
30
+ };
31
+
32
+ function failure(sourceKey: string, expected: string, actual: string): MeshBinEncodeError {
33
+ return {
34
+ code: 'mesh-bin-payload-invalid',
35
+ subject: 'mesh-bin',
36
+ sourceKey,
37
+ expected,
38
+ actual,
39
+ recovery: 're-cook the source with its Meta sidecar through the build-time importer',
40
+ };
41
+ }
42
+
43
+ function asAttributeMap(value: unknown): VertexAttributeMap {
44
+ return (value ?? {}) as VertexAttributeMap;
45
+ }
46
+
47
+ function jsonValue(value: unknown): unknown {
48
+ if (value instanceof Float32Array || value instanceof Uint16Array) return Array.from(value);
49
+ if (Array.isArray(value)) return value.map(jsonValue);
50
+ if (value !== null && typeof value === 'object') {
51
+ return Object.fromEntries(
52
+ Object.entries(value).map(([key, nested]) => [key, jsonValue(nested)]),
53
+ );
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function refsMeta(payload: MeshPayloadIn, refs: readonly string[]): Record<string, unknown> {
59
+ const materialSlots = (payload.materialSlots ?? [{ slotName: 'Default' }]).map(
60
+ (slot, slotIndex) => {
61
+ const defaultMaterial = slot.defaultMaterial;
62
+ let defaultMaterialRef: number | undefined;
63
+ if (defaultMaterial !== undefined) {
64
+ const guid = AssetGuid.format(defaultMaterial);
65
+ defaultMaterialRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
66
+ if (defaultMaterialRef < 0) {
67
+ throw new Error(
68
+ `material slot ${slotIndex} default material ${guid} is absent from refs`,
69
+ );
70
+ }
71
+ }
72
+ return {
73
+ slotName: slot.slotName,
74
+ ...(slot.sourceKey === undefined ? {} : { sourceKey: slot.sourceKey }),
75
+ ...(defaultMaterialRef === undefined ? {} : { defaultMaterialRef }),
76
+ };
77
+ },
78
+ );
79
+ return {
80
+ submeshes:
81
+ payload.submeshes === undefined || payload.submeshes.length === 0
82
+ ? [{ indexOffset: 0, indexCount: payload.indices?.length ?? 0, materialSlot: 0 }]
83
+ : payload.submeshes,
84
+ materialSlots,
85
+ ...(payload.aabb === undefined ? {} : { aabb: jsonValue(payload.aabb) }),
86
+ ...(payload.morphTargets === undefined
87
+ ? {}
88
+ : { morphTargets: jsonValue(payload.morphTargets) }),
89
+ ...(payload.morphWeights === undefined
90
+ ? {}
91
+ : { morphWeights: jsonValue(payload.morphWeights) }),
92
+ };
93
+ }
94
+
95
+ /** Encode one canonical v4 mesh binary. No legacy version is accepted or emitted. */
96
+ export function packMeshBinV4(
97
+ payload: MeshPayloadIn,
98
+ sourceKey: string,
99
+ refs: readonly string[] = [],
100
+ ): Result<Uint8Array, MeshBinEncodeError> {
101
+ try {
102
+ const vertices = payload.vertices;
103
+ const indices = payload.indices;
104
+ if (!(vertices instanceof Float32Array)) {
105
+ return err(
106
+ failure(sourceKey, 'Float32Array interleaved vertices', 'vertices is not Float32Array'),
107
+ );
108
+ }
109
+ if (
110
+ indices !== undefined &&
111
+ !(indices instanceof Uint16Array || indices instanceof Uint32Array)
112
+ ) {
113
+ return err(
114
+ failure(sourceKey, 'Uint16Array or Uint32Array indices', 'indices has an unsupported type'),
115
+ );
116
+ }
117
+ const attributes = asAttributeMap(payload.attributes);
118
+ const projection = deriveVertexLayoutProjection(attributes);
119
+ if (projection.attributes.length === 0 || projection.arrayStride === 0) {
120
+ return err(
121
+ failure(
122
+ sourceKey,
123
+ 'a non-empty canonical geometry projection',
124
+ 'projection has no attributes',
125
+ ),
126
+ );
127
+ }
128
+ const vertexCount = payload.vertexCount ?? vertices.byteLength / projection.arrayStride;
129
+ if (!Number.isSafeInteger(vertexCount) || vertexCount < 0) {
130
+ return err(
131
+ failure(sourceKey, 'a non-negative safe vertex cardinality', `vertexCount=${vertexCount}`),
132
+ );
133
+ }
134
+ if (vertices.byteLength !== vertexCount * projection.arrayStride) {
135
+ return err(
136
+ failure(
137
+ sourceKey,
138
+ `vertices.byteLength=${vertexCount * projection.arrayStride}`,
139
+ `vertices.byteLength=${vertices.byteLength}; stride=${projection.arrayStride}`,
140
+ ),
141
+ );
142
+ }
143
+ for (const attribute of projection.attributes) {
144
+ const value = attributes[attribute.key];
145
+ const components = attribute.byteLength / (attribute.format === 'uint16x4' ? 2 : 4);
146
+ if (
147
+ value === undefined ||
148
+ (!(value instanceof Float32Array) && !(value instanceof Uint16Array)) ||
149
+ value.length !== vertexCount * components
150
+ ) {
151
+ return err(
152
+ failure(
153
+ sourceKey,
154
+ `${attribute.key} cardinality=${vertexCount * components}`,
155
+ `${attribute.key} cardinality=${value?.byteLength ?? 'missing'}`,
156
+ ),
157
+ );
158
+ }
159
+ }
160
+ const interleaved = new Uint8Array(vertexCount * projection.arrayStride);
161
+ const interleavedView = new DataView(interleaved.buffer);
162
+ for (const attribute of projection.attributes) {
163
+ const value = attributes[attribute.key];
164
+ if (value === undefined) continue;
165
+ const components = attribute.byteLength / (attribute.format === 'uint16x4' ? 2 : 4);
166
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
167
+ for (let component = 0; component < components; component++) {
168
+ const sourceIndex = vertex * components + component;
169
+ const targetOffset =
170
+ vertex * projection.arrayStride +
171
+ attribute.offset +
172
+ component * (attribute.format === 'uint16x4' ? 2 : 4);
173
+ if (attribute.format === 'uint16x4') {
174
+ interleavedView.setUint16(targetOffset, (value as Uint16Array)[sourceIndex] ?? 0, true);
175
+ } else {
176
+ interleavedView.setFloat32(
177
+ targetOffset,
178
+ (value as Float32Array)[sourceIndex] ?? 0,
179
+ true,
180
+ );
181
+ }
182
+ }
183
+ }
184
+ }
185
+ const indexCount = indices?.length ?? 0;
186
+ const indexWidth = indices === undefined || indexCount === 0 ? 0 : indices.BYTES_PER_ELEMENT;
187
+ const indexBytes = indexCount * indexWidth;
188
+ if (!Number.isSafeInteger(indexBytes) || indexBytes > 0xffffffff) {
189
+ return err(failure(sourceKey, 'safe index payload byte length', `indexBytes=${indexBytes}`));
190
+ }
191
+ const meta = new TextEncoder().encode(JSON.stringify(refsMeta(payload, refs)));
192
+ const header: MeshBinHeaderV4 = {
193
+ version: 4,
194
+ projectionVersion: projection.schemaVersion,
195
+ mask: projection.mask,
196
+ digest: projection.digest,
197
+ stride: projection.arrayStride,
198
+ vertexCount,
199
+ vertexBytes: interleaved.byteLength,
200
+ indexCount,
201
+ indexWidth: indexWidth as 0 | 2 | 4,
202
+ indexBytes,
203
+ jsonBytes: meta.byteLength,
204
+ };
205
+ const total = MESH_BIN_HEADER_V4_BYTES + interleaved.byteLength + indexBytes + meta.byteLength;
206
+ if (!Number.isSafeInteger(total) || total > 0xffffffff) {
207
+ return err(failure(sourceKey, 'safe mesh binary byte length', `total=${total}`));
208
+ }
209
+ const out = new Uint8Array(total);
210
+ writeMeshBinHeader(header, out);
211
+ let offset = MESH_BIN_HEADER_V4_BYTES;
212
+ out.set(interleaved, offset);
213
+ offset += interleaved.byteLength;
214
+ if (indices !== undefined && indexBytes > 0) {
215
+ out.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength), offset);
216
+ offset += indexBytes;
217
+ }
218
+ out.set(meta, offset);
219
+ return ok(out);
220
+ } catch (error) {
221
+ return err(
222
+ failure(
223
+ sourceKey,
224
+ 'valid canonical mesh payload',
225
+ error instanceof Error ? error.message : String(error),
226
+ ),
227
+ );
228
+ }
229
+ }