@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// check-extensions.ts - KHR / vendor extension gate.
|
|
2
|
+
//
|
|
3
|
+
// v1 required-extension support contains EXT_mesh_gpu_instancing,
|
|
4
|
+
// KHR_lights_punctual, and KHR_texture_transform. The exported legacy list remains the original mesh
|
|
5
|
+
// extension list for callers that display the v1 mesh-only surface.
|
|
6
|
+
// (feat-20260518-gltf-instancing-and-name-component plan-strategy section
|
|
7
|
+
// 2 D-1 / D-3). Any extension listed in `extensionsRequired[]` outside
|
|
8
|
+
// this allowlist triggers `gltf-extension-unsupported` (hard fail).
|
|
9
|
+
// Extensions listed in `extensionsUsed[]` (but not required and not in
|
|
10
|
+
// allowlist) are recorded in `importSettings.diagnostics
|
|
11
|
+
// .unsupportedExtensions` so AI users can observe them downstream — and
|
|
12
|
+
// nothing else: `extensionsUsed` is purely informational per the glTF spec
|
|
13
|
+
// (only `extensionsRequired` is binding), and exporters routinely
|
|
14
|
+
// over-declare it (e.g. KHR_materials_unlit listed but referenced by no
|
|
15
|
+
// material). A stderr warn for those would be a false positive, so the
|
|
16
|
+
// diagnostics list is the single channel (no `console.error`).
|
|
17
|
+
//
|
|
18
|
+
// Future expansion (KHR_materials_unlit, ...) extends
|
|
19
|
+
// `EXTENSION_ALLOWLIST` in place; each addition lands under its own feat-*
|
|
20
|
+
// loop with breaking-change registry entry.
|
|
21
|
+
|
|
22
|
+
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
23
|
+
|
|
24
|
+
/** Hard-coded v1 allowlist (plan-strategy decision section 2 D-1 / D-3). */
|
|
25
|
+
export const EXTENSION_ALLOWLIST: readonly string[] = [
|
|
26
|
+
'EXT_mesh_gpu_instancing',
|
|
27
|
+
'EXT_meshopt_compression',
|
|
28
|
+
];
|
|
29
|
+
const SUPPORTED_EXTENSIONS: readonly string[] = [
|
|
30
|
+
...EXTENSION_ALLOWLIST,
|
|
31
|
+
'KHR_lights_punctual',
|
|
32
|
+
'KHR_texture_transform',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export interface ExtensionsCheckResult {
|
|
36
|
+
/** Names listed in extensionsUsed but not in the allowlist. */
|
|
37
|
+
readonly unsupportedUsed: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GltfExtensionsJson {
|
|
41
|
+
readonly extensionsRequired?: readonly string[];
|
|
42
|
+
readonly extensionsUsed?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate the glTF JSON's extension declarations against the v1 allowlist.
|
|
47
|
+
*
|
|
48
|
+
* Returns:
|
|
49
|
+
* - `Result.err(gltf-extension-unsupported)` for the FIRST entry of
|
|
50
|
+
* `extensionsRequired[]` that is not allowlisted (fail-fast; the
|
|
51
|
+
* remaining required entries are not surfaced; first wins because the
|
|
52
|
+
* diagnostic carries one extension name + source slot only).
|
|
53
|
+
* - `Result.ok({ unsupportedUsed })` otherwise; the `unsupportedUsed`
|
|
54
|
+
* array names every extensionsUsed entry not in the allowlist (used as
|
|
55
|
+
* input for `importSettings.diagnostics.unsupportedExtensions`).
|
|
56
|
+
* extensionsUsed entries never fail parsing and never write to stderr —
|
|
57
|
+
* they are informational per the glTF spec and routinely over-declared.
|
|
58
|
+
*
|
|
59
|
+
* Pure function: no fs / network / stderr.
|
|
60
|
+
*/
|
|
61
|
+
export function checkExtensions(
|
|
62
|
+
json: GltfExtensionsJson,
|
|
63
|
+
): Result<ExtensionsCheckResult, GltfError> {
|
|
64
|
+
const required = json.extensionsRequired ?? [];
|
|
65
|
+
for (const ext of required) {
|
|
66
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
67
|
+
return err(
|
|
68
|
+
gltfErr('gltf-extension-unsupported', {
|
|
69
|
+
extension: ext,
|
|
70
|
+
source: 'extensionsRequired',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const used = json.extensionsUsed ?? [];
|
|
77
|
+
const unsupportedUsed: string[] = [];
|
|
78
|
+
for (const ext of used) {
|
|
79
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
80
|
+
unsupportedUsed.push(ext);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return ok({ unsupportedUsed });
|
|
85
|
+
}
|
package/src/cli-gltf.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @forgeax/engine-gltf/src/cli-gltf — `forgeax-engine-remote-gltf` plugin
|
|
3
|
+
// bin (feat-20260516-console-dependency-inversion plan-strategy section
|
|
4
|
+
// 2.9). Discovered by the base bin via the kubectl 4th-path
|
|
5
|
+
// `forgeax-engine-remote-` prefix scanner.
|
|
6
|
+
//
|
|
7
|
+
// Single subcommand `import` for v1 (UX break vs the prior
|
|
8
|
+
// `forgeax-engine-remote-asset import`):
|
|
9
|
+
//
|
|
10
|
+
// write mode `forgeax-engine-remote-gltf import <gltf-or-glb>`
|
|
11
|
+
// Parses the source via parseGlb / parseGltf and writes the
|
|
12
|
+
// sibling `<source>.meta.json` sidecar (sorted-keys, LF line
|
|
13
|
+
// ending — byte-stable so a clean reimport produces no diff).
|
|
14
|
+
//
|
|
15
|
+
// --check `forgeax-engine-remote-gltf import --check <dir>`
|
|
16
|
+
// Dry-run: traverse <dir> reusing SCANNER_BLACKLIST from
|
|
17
|
+
// @forgeax/engine-pack/scanner and surface the first orphan
|
|
18
|
+
// .gltf / .glb whose `<source>.meta.json` is absent as
|
|
19
|
+
// gltf-meta-missing JSON Lines on stderr.
|
|
20
|
+
//
|
|
21
|
+
// stderr contract (plan-strategy section 2.3): every error path emits a
|
|
22
|
+
// single JSON Lines record with code / expected / hint; detail when
|
|
23
|
+
// available.
|
|
24
|
+
|
|
25
|
+
import { readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { dirname, join, resolve } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { parseArgs } from 'node:util';
|
|
29
|
+
import { GLTF_ERROR_HINTS } from './errors.js';
|
|
30
|
+
import { parseGlb, parseGltf, toAssetPack } from './parse-gltf.js';
|
|
31
|
+
import { serializeMetaJson } from './serialize-meta.js';
|
|
32
|
+
|
|
33
|
+
interface AssetCtx {
|
|
34
|
+
readonly stdoutWrite: (line: string) => void;
|
|
35
|
+
readonly stderrWrite: (line: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ErrShape {
|
|
39
|
+
code: string;
|
|
40
|
+
expected: string;
|
|
41
|
+
hint: string;
|
|
42
|
+
detail?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function emitError(ctx: AssetCtx, err: ErrShape): number {
|
|
46
|
+
const payload: Record<string, unknown> = {
|
|
47
|
+
code: err.code,
|
|
48
|
+
expected: err.expected,
|
|
49
|
+
hint: err.hint,
|
|
50
|
+
};
|
|
51
|
+
if (err.detail !== undefined) payload.detail = err.detail;
|
|
52
|
+
ctx.stderrWrite(JSON.stringify(payload));
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function helpBody(): string {
|
|
57
|
+
return [
|
|
58
|
+
'forgeax-engine-remote-gltf — glTF / GLB sidecar importer (produces texture/mesh/material/scene sub-assets)',
|
|
59
|
+
'',
|
|
60
|
+
'Usage:',
|
|
61
|
+
' forgeax-engine-remote-gltf import <path.gltf|path.glb>',
|
|
62
|
+
' forgeax-engine-remote-gltf import --check <dir>',
|
|
63
|
+
'',
|
|
64
|
+
'Produces a sibling <source>.meta.json sidecar with sub-asset entries',
|
|
65
|
+
'for every glTF asset category: texture, mesh, material, scene.',
|
|
66
|
+
'',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runCliGltf(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
71
|
+
const [sub, ...subRest] = rest;
|
|
72
|
+
if (sub === undefined || sub === '--help' || sub === '-h') {
|
|
73
|
+
ctx.stdoutWrite(helpBody());
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (sub !== 'import') {
|
|
77
|
+
return emitError(ctx, {
|
|
78
|
+
code: 'unknown-subcommand',
|
|
79
|
+
expected: "subcommand 'import'",
|
|
80
|
+
hint: "run 'forgeax-engine-remote-gltf --help' for usage",
|
|
81
|
+
detail: { subcommand: sub },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return runImport(subRest, ctx);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function runImport(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
88
|
+
let positionals: string[];
|
|
89
|
+
let check = false;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = parseArgs({
|
|
92
|
+
args: rest,
|
|
93
|
+
allowPositionals: true,
|
|
94
|
+
strict: true,
|
|
95
|
+
options: { check: { type: 'boolean' } },
|
|
96
|
+
});
|
|
97
|
+
positionals = [...parsed.positionals];
|
|
98
|
+
check = parsed.values.check === true;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
101
|
+
return emitError(ctx, {
|
|
102
|
+
code: 'cli-parse-error',
|
|
103
|
+
expected: 'forgeax-engine-remote-gltf import [--check] <path>',
|
|
104
|
+
hint: "run 'forgeax-engine-remote-gltf --help' for usage",
|
|
105
|
+
detail: { message },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const target = positionals[0];
|
|
109
|
+
if (target === undefined) {
|
|
110
|
+
return emitError(ctx, {
|
|
111
|
+
code: 'cli-parse-error',
|
|
112
|
+
expected: check
|
|
113
|
+
? 'forgeax-engine-remote-gltf import --check <dir>'
|
|
114
|
+
: 'forgeax-engine-remote-gltf import <path.gltf|path.glb>',
|
|
115
|
+
hint: 'pass a positional <gltf-or-glb> argument; with --check pass a directory',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (check) {
|
|
119
|
+
return runCheck(target, ctx);
|
|
120
|
+
}
|
|
121
|
+
return runWrite(target, ctx);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const SCANNER_BLACKLIST_FALLBACK: ReadonlySet<string> = new Set([
|
|
125
|
+
'node_modules',
|
|
126
|
+
'.forgeax-harness',
|
|
127
|
+
'.git',
|
|
128
|
+
'dist',
|
|
129
|
+
'.forgeax-asset-cache',
|
|
130
|
+
'forgeax-engine-assets',
|
|
131
|
+
'coverage',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
async function loadScannerBlacklist(): Promise<ReadonlySet<string>> {
|
|
135
|
+
try {
|
|
136
|
+
const mod = (await import('@forgeax/engine-pack/scanner')) as {
|
|
137
|
+
SCANNER_BLACKLIST?: ReadonlySet<string>;
|
|
138
|
+
};
|
|
139
|
+
return mod.SCANNER_BLACKLIST ?? SCANNER_BLACKLIST_FALLBACK;
|
|
140
|
+
} catch {
|
|
141
|
+
return SCANNER_BLACKLIST_FALLBACK;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function findGltfSources(
|
|
146
|
+
dir: string,
|
|
147
|
+
blacklist: ReadonlySet<string>,
|
|
148
|
+
out: string[],
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
let entries: import('node:fs').Dirent[];
|
|
151
|
+
try {
|
|
152
|
+
entries = (await readdir(dir, { withFileTypes: true })) as import('node:fs').Dirent[];
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const full = join(dir, entry.name);
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
if (blacklist.has(entry.name)) continue;
|
|
160
|
+
await findGltfSources(full, blacklist, out);
|
|
161
|
+
} else if (entry.isFile()) {
|
|
162
|
+
const lower = entry.name.toLowerCase();
|
|
163
|
+
if (lower.endsWith('.gltf') || lower.endsWith('.glb')) {
|
|
164
|
+
out.push(full);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function expectedMetaPathFor(filePath: string): string {
|
|
171
|
+
// feat-20260521 unify-sidecar-meta-dispatch-by-content: sidecar is
|
|
172
|
+
// `<source>.meta.json` next to the source file; dispatch is driven by
|
|
173
|
+
// top-level importer in the JSON, not by the filename suffix.
|
|
174
|
+
return `${filePath}.meta.json`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
178
|
+
try {
|
|
179
|
+
await stat(path);
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runCheck(target: string, ctx: AssetCtx): Promise<number> {
|
|
187
|
+
const root = resolve(target);
|
|
188
|
+
let st: import('node:fs').Stats;
|
|
189
|
+
try {
|
|
190
|
+
st = await stat(root);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
193
|
+
return emitError(ctx, {
|
|
194
|
+
code: 'cli-parse-error',
|
|
195
|
+
expected: 'forgeax-engine-remote-gltf import --check <dir>',
|
|
196
|
+
hint: 'pass a directory that exists and is readable',
|
|
197
|
+
detail: { path: target, message },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const blacklist = await loadScannerBlacklist();
|
|
201
|
+
const found: string[] = [];
|
|
202
|
+
if (st.isDirectory()) {
|
|
203
|
+
await findGltfSources(root, blacklist, found);
|
|
204
|
+
} else if (st.isFile()) {
|
|
205
|
+
const lower = root.toLowerCase();
|
|
206
|
+
if (lower.endsWith('.gltf') || lower.endsWith('.glb')) {
|
|
207
|
+
found.push(root);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
found.sort();
|
|
211
|
+
for (const sourcePath of found) {
|
|
212
|
+
const metaPath = expectedMetaPathFor(sourcePath);
|
|
213
|
+
if (!(await fileExists(metaPath))) {
|
|
214
|
+
return emitError(ctx, {
|
|
215
|
+
code: 'gltf-meta-missing',
|
|
216
|
+
expected: "sidecar <source>.meta.json (importer: 'gltf') present in same directory",
|
|
217
|
+
hint: GLTF_ERROR_HINTS['gltf-meta-missing'],
|
|
218
|
+
detail: { filePath: sourcePath, expectedMetaPath: metaPath },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runWrite(target: string, ctx: AssetCtx): Promise<number> {
|
|
226
|
+
const sourcePath = resolve(target);
|
|
227
|
+
const lower = sourcePath.toLowerCase();
|
|
228
|
+
const isGlb = lower.endsWith('.glb');
|
|
229
|
+
const isGltf = lower.endsWith('.gltf');
|
|
230
|
+
if (!isGlb && !isGltf) {
|
|
231
|
+
return emitError(ctx, {
|
|
232
|
+
code: 'cli-parse-error',
|
|
233
|
+
expected: '<path>.gltf or <path>.glb',
|
|
234
|
+
hint: 'pass a .gltf / .glb source file; for batch dry-run use --check <dir>',
|
|
235
|
+
detail: { path: target },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let docResult:
|
|
239
|
+
| { readonly ok: true; readonly value: unknown }
|
|
240
|
+
| {
|
|
241
|
+
readonly ok: false;
|
|
242
|
+
readonly error: { code: string; expected: string; hint: string; detail: unknown };
|
|
243
|
+
};
|
|
244
|
+
if (isGlb) {
|
|
245
|
+
let buf: Buffer;
|
|
246
|
+
try {
|
|
247
|
+
buf = await readFile(sourcePath);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
250
|
+
return emitError(ctx, {
|
|
251
|
+
code: 'cli-parse-error',
|
|
252
|
+
expected: 'readable .glb at <path>',
|
|
253
|
+
hint: 'check the file exists and the process has read access',
|
|
254
|
+
detail: { path: target, message },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
|
|
258
|
+
docResult = await parseGlb(ab, sourcePath);
|
|
259
|
+
} else {
|
|
260
|
+
let text: string;
|
|
261
|
+
try {
|
|
262
|
+
text = await readFile(sourcePath, 'utf-8');
|
|
263
|
+
} catch (e) {
|
|
264
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
265
|
+
return emitError(ctx, {
|
|
266
|
+
code: 'cli-parse-error',
|
|
267
|
+
expected: 'readable .gltf at <path>',
|
|
268
|
+
hint: 'check the file exists and the process has read access',
|
|
269
|
+
detail: { path: target, message },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
let json: unknown;
|
|
273
|
+
try {
|
|
274
|
+
json = JSON.parse(text);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
277
|
+
return emitError(ctx, {
|
|
278
|
+
code: 'gltf-malformed-header',
|
|
279
|
+
expected: 'valid JSON document at the .gltf root',
|
|
280
|
+
hint: GLTF_ERROR_HINTS['gltf-malformed-header'],
|
|
281
|
+
detail: { filePath: sourcePath, byteOffset: 0, parseError: message },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const baseDir = dirname(sourcePath);
|
|
285
|
+
const externalLoader = async (uri: string): Promise<ArrayBuffer> => {
|
|
286
|
+
const abs = resolve(baseDir, uri);
|
|
287
|
+
const buf = await readFile(abs);
|
|
288
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
|
|
289
|
+
};
|
|
290
|
+
docResult = await parseGltf(json, externalLoader, sourcePath);
|
|
291
|
+
}
|
|
292
|
+
if (!docResult.ok) {
|
|
293
|
+
return emitError(ctx, {
|
|
294
|
+
code: docResult.error.code,
|
|
295
|
+
expected: docResult.error.expected,
|
|
296
|
+
hint: docResult.error.hint,
|
|
297
|
+
detail: docResult.error.detail,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const metaPath = expectedMetaPathFor(sourcePath);
|
|
301
|
+
let existingMeta: unknown;
|
|
302
|
+
if (await fileExists(metaPath)) {
|
|
303
|
+
try {
|
|
304
|
+
const raw = await readFile(metaPath, 'utf-8');
|
|
305
|
+
existingMeta = JSON.parse(raw);
|
|
306
|
+
} catch {
|
|
307
|
+
existingMeta = undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const sourceRelative = sourcePath.slice(dirname(sourcePath).length + 1);
|
|
311
|
+
const pack = toAssetPack(
|
|
312
|
+
docResult.value as Parameters<typeof toAssetPack>[0],
|
|
313
|
+
existingMeta as Parameters<typeof toAssetPack>[1],
|
|
314
|
+
sourceRelative,
|
|
315
|
+
);
|
|
316
|
+
if (!pack.ok) {
|
|
317
|
+
return emitError(ctx, pack.error);
|
|
318
|
+
}
|
|
319
|
+
await writeFile(metaPath, serializeMetaJson(pack.value.meta), 'utf-8');
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const isBinEntry = await (async (): Promise<boolean> => {
|
|
324
|
+
const argv1 = process.argv[1];
|
|
325
|
+
if (typeof argv1 !== 'string') return false;
|
|
326
|
+
const argv1Real = await realpath(argv1).catch(() => argv1);
|
|
327
|
+
const selfReal = await realpath(fileURLToPath(import.meta.url)).catch(() =>
|
|
328
|
+
fileURLToPath(import.meta.url),
|
|
329
|
+
);
|
|
330
|
+
return argv1Real === selfReal;
|
|
331
|
+
})();
|
|
332
|
+
|
|
333
|
+
if (isBinEntry) {
|
|
334
|
+
const exitCode = await runCliGltf(process.argv.slice(2), {
|
|
335
|
+
stdoutWrite: (line: string) => process.stdout.write(`${line}\n`),
|
|
336
|
+
stderrWrite: (line: string) => process.stderr.write(`${line}\n`),
|
|
337
|
+
});
|
|
338
|
+
process.exit(exitCode);
|
|
339
|
+
}
|
package/src/data-uri.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DATA_URI_BASE64_RE = /^data:[^;,]*(?:;[^,;]+)*;base64,(.*)$/;
|
|
2
|
+
|
|
3
|
+
export class Base64DecodeError extends Error {
|
|
4
|
+
constructor(cause: unknown) {
|
|
5
|
+
super(
|
|
6
|
+
`base64 payload decode failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
7
|
+
);
|
|
8
|
+
this.name = 'Base64DecodeError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dataUriBase64Payload(uri: string): string | undefined {
|
|
13
|
+
const match = DATA_URI_BASE64_RE.exec(uri);
|
|
14
|
+
return match === null ? undefined : (match[1] ?? '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decodeBase64(b64: string): Uint8Array {
|
|
18
|
+
let binary: string;
|
|
19
|
+
try {
|
|
20
|
+
binary = atob(b64);
|
|
21
|
+
} catch (cause) {
|
|
22
|
+
throw new Base64DecodeError(cause);
|
|
23
|
+
}
|
|
24
|
+
const out = new Uint8Array(binary.length);
|
|
25
|
+
for (let i = 0; i < binary.length; i++) {
|
|
26
|
+
out[i] = binary.charCodeAt(i);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|