@aura3d/cli 4.7.18 → 5.0.1
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/data/aura.exports.json +589 -87
- package/dist/admission/artDirection.d.ts.map +1 -1
- package/dist/admission/artDirection.js.map +1 -1
- package/dist/admission/audio.d.ts.map +1 -1
- package/dist/admission/audio.js.map +1 -1
- package/dist/admission/builder-patterns.d.ts.map +1 -1
- package/dist/admission/builder-patterns.js.map +1 -1
- package/dist/admission/cubeLut.d.ts.map +1 -1
- package/dist/admission/cubeLut.js.map +1 -1
- package/dist/admission/gates.d.ts.map +1 -1
- package/dist/admission/gates.js.map +1 -1
- package/dist/admission/glb.d.ts.map +1 -1
- package/dist/admission/glb.js.map +1 -1
- package/dist/admission/index.d.ts.map +1 -1
- package/dist/admission/index.js.map +1 -1
- package/dist/admission/profiles.d.ts.map +1 -1
- package/dist/admission/profiles.js.map +1 -1
- package/dist/admission/qrFlags.d.ts.map +1 -1
- package/dist/admission/qrFlags.js.map +1 -1
- package/dist/admission/routeGates.d.ts.map +1 -1
- package/dist/admission/routeGates.js.map +1 -1
- package/dist/admission/texel.d.ts.map +1 -1
- package/dist/admission/texel.js.map +1 -1
- package/dist/admission/textureStats.d.ts.map +1 -1
- package/dist/admission/textureStats.js.map +1 -1
- package/dist/animation-asset-profiles.d.ts.map +1 -1
- package/dist/animation-asset-profiles.js.map +1 -1
- package/dist/animation-asset-validator.d.ts.map +1 -1
- package/dist/animation-asset-validator.js.map +1 -1
- package/dist/asset-constants.d.ts +2 -0
- package/dist/asset-constants.d.ts.map +1 -1
- package/dist/asset-constants.js +2 -0
- package/dist/asset-constants.js.map +1 -1
- package/dist/asset-core-types.d.ts +43 -1
- package/dist/asset-core-types.d.ts.map +1 -1
- package/dist/asset-core-types.js.map +1 -1
- package/dist/asset-inspection-types.d.ts.map +1 -1
- package/dist/asset-inspection-types.js.map +1 -1
- package/dist/asset-intent.d.ts.map +1 -1
- package/dist/asset-intent.js.map +1 -1
- package/dist/asset-manifest.d.ts +1 -1
- package/dist/asset-manifest.d.ts.map +1 -1
- package/dist/asset-manifest.js +160 -6
- package/dist/asset-manifest.js.map +1 -1
- package/dist/asset-readiness-types.d.ts.map +1 -1
- package/dist/asset-readiness-types.js.map +1 -1
- package/dist/asset-role-admission.d.ts.map +1 -1
- package/dist/asset-role-admission.js.map +1 -1
- package/dist/asset-route-typegen.d.ts.map +1 -1
- package/dist/asset-route-typegen.js +90 -1
- package/dist/asset-route-typegen.js.map +1 -1
- package/dist/asset-screening-effects.d.ts.map +1 -1
- package/dist/asset-screening-effects.js.map +1 -1
- package/dist/asset-screening-pipeline.d.ts.map +1 -1
- package/dist/asset-screening-pipeline.js.map +1 -1
- package/dist/asset-source-ast.d.ts.map +1 -1
- package/dist/asset-source-ast.js.map +1 -1
- package/dist/asset-source-roles.d.ts.map +1 -1
- package/dist/asset-source-roles.js.map +1 -1
- package/dist/asset-source-validation.d.ts.map +1 -1
- package/dist/asset-source-validation.js.map +1 -1
- package/dist/character-assembly-types.d.ts.map +1 -1
- package/dist/character-assembly-types.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +16 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-options.d.ts +1 -1
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +2 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -2
- package/dist/cli.js.map +1 -1
- package/dist/codemods/camera-cast.d.ts.map +1 -1
- package/dist/codemods/camera-cast.js.map +1 -1
- package/dist/codemods/core-v2.d.ts.map +1 -1
- package/dist/codemods/core-v2.js.map +1 -1
- package/dist/codemods/engine-entry-imports.d.ts.map +1 -1
- package/dist/codemods/engine-entry-imports.js.map +1 -1
- package/dist/codemods/lean-imports.d.ts.map +1 -1
- package/dist/codemods/lean-imports.js.map +1 -1
- package/dist/codemods/post-v2.d.ts.map +1 -1
- package/dist/codemods/post-v2.js.map +1 -1
- package/dist/codemods/renderer-imports.d.ts.map +1 -1
- package/dist/codemods/renderer-imports.js +49 -4
- package/dist/codemods/renderer-imports.js.map +1 -1
- package/dist/codemods/renderer-mode.d.ts.map +1 -1
- package/dist/codemods/renderer-mode.js.map +1 -1
- package/dist/commands/assets/certifyCategories.d.ts.map +1 -1
- package/dist/commands/assets/certifyCategories.js.map +1 -1
- package/dist/commands/assets/export.d.ts +20 -0
- package/dist/commands/assets/export.d.ts.map +1 -0
- package/dist/commands/assets/export.js +431 -0
- package/dist/commands/assets/export.js.map +1 -0
- package/dist/commands/assets/gltfToolkit.d.ts.map +1 -1
- package/dist/commands/assets/gltfToolkit.js.map +1 -1
- package/dist/commands/assets/importMeshy.d.ts.map +1 -1
- package/dist/commands/assets/importMeshy.js.map +1 -1
- package/dist/commands/assets/index.d.ts.map +1 -1
- package/dist/commands/assets/index.js +8 -0
- package/dist/commands/assets/index.js.map +1 -1
- package/dist/commands/assets/mergeClips.d.ts.map +1 -1
- package/dist/commands/assets/mergeClips.js.map +1 -1
- package/dist/commands/assets/optimize.d.ts +9 -1
- package/dist/commands/assets/optimize.d.ts.map +1 -1
- package/dist/commands/assets/optimize.js +29 -5
- package/dist/commands/assets/optimize.js.map +1 -1
- package/dist/commands/assets/optimizeVerb.d.ts.map +1 -1
- package/dist/commands/assets/optimizeVerb.js +22 -1
- package/dist/commands/assets/optimizeVerb.js.map +1 -1
- package/dist/commands/evidence/index.d.ts.map +1 -1
- package/dist/commands/evidence/index.js.map +1 -1
- package/dist/commands/evidence/judge.d.ts.map +1 -1
- package/dist/commands/evidence/judge.js.map +1 -1
- package/dist/commands/evidence/judgeLib.d.ts.map +1 -1
- package/dist/commands/evidence/judgeLib.js.map +1 -1
- package/dist/commands/meshyLedger.d.ts.map +1 -1
- package/dist/commands/meshyLedger.js.map +1 -1
- package/dist/commands/prd01/index.d.ts.map +1 -1
- package/dist/commands/prd01/index.js.map +1 -1
- package/dist/commands/prd02/environmentsBake.d.ts.map +1 -1
- package/dist/commands/prd02/environmentsBake.js +23 -2
- package/dist/commands/prd02/environmentsBake.js.map +1 -1
- package/dist/commands/prd02/index.d.ts.map +1 -1
- package/dist/commands/prd02/index.js +1 -1
- package/dist/commands/prd02/index.js.map +1 -1
- package/dist/commands/prd02/migrateLighting.d.ts.map +1 -1
- package/dist/commands/prd02/migrateLighting.js.map +1 -1
- package/dist/commands/prd02/migrateLightingSweep.d.ts.map +1 -1
- package/dist/commands/prd02/migrateLightingSweep.js.map +1 -1
- package/dist/commands/prd02/probeFormatOptions.d.ts +8 -0
- package/dist/commands/prd02/probeFormatOptions.d.ts.map +1 -0
- package/dist/commands/prd02/probeFormatOptions.js +19 -0
- package/dist/commands/prd02/probeFormatOptions.js.map +1 -0
- package/dist/commands/prd03/index.d.ts.map +1 -1
- package/dist/commands/prd03/index.js.map +1 -1
- package/dist/commands/prd04/index.d.ts.map +1 -1
- package/dist/commands/prd04/index.js.map +1 -1
- package/dist/commands/prd04/modelTintReport.d.ts.map +1 -1
- package/dist/commands/prd04/modelTintReport.js.map +1 -1
- package/dist/commands/prd04/pinEmissiveDefaults.d.ts.map +1 -1
- package/dist/commands/prd04/pinEmissiveDefaults.js.map +1 -1
- package/dist/commands/prd05/admit.d.ts.map +1 -1
- package/dist/commands/prd05/admit.js.map +1 -1
- package/dist/commands/prd05/codemods/assetsRouteModules.d.ts.map +1 -1
- package/dist/commands/prd05/codemods/assetsRouteModules.js.map +1 -1
- package/dist/commands/prd05/index.d.ts.map +1 -1
- package/dist/commands/prd05/index.js +1 -1
- package/dist/commands/prd05/index.js.map +1 -1
- package/dist/commands/prd05/library.d.ts.map +1 -1
- package/dist/commands/prd05/library.js.map +1 -1
- package/dist/commands/prd05/lookdev.d.ts.map +1 -1
- package/dist/commands/prd05/lookdev.js.map +1 -1
- package/dist/commands/prd05/optimize.d.ts.map +1 -1
- package/dist/commands/prd05/optimize.js.map +1 -1
- package/dist/commands/prd05/prune.d.ts.map +1 -1
- package/dist/commands/prd05/prune.js.map +1 -1
- package/dist/commands/prd05/review.d.ts.map +1 -1
- package/dist/commands/prd05/review.js.map +1 -1
- package/dist/commands/prd06/index.d.ts.map +1 -1
- package/dist/commands/prd06/index.js.map +1 -1
- package/dist/commands/prd06/inspectAnimationClips.d.ts.map +1 -1
- package/dist/commands/prd06/inspectAnimationClips.js.map +1 -1
- package/dist/commands/prd06/validateHero.d.ts.map +1 -1
- package/dist/commands/prd06/validateHero.js.map +1 -1
- package/dist/commands/prd07/codemods.d.ts.map +1 -1
- package/dist/commands/prd07/codemods.js.map +1 -1
- package/dist/commands/prd07/index.d.ts.map +1 -1
- package/dist/commands/prd07/index.js.map +1 -1
- package/dist/commands/prd08/feelLint.d.ts.map +1 -1
- package/dist/commands/prd08/feelLint.js.map +1 -1
- package/dist/commands/prd08/index.d.ts.map +1 -1
- package/dist/commands/prd08/index.js.map +1 -1
- package/dist/commands/prd08/motionReport.d.ts.map +1 -1
- package/dist/commands/prd08/motionReport.js.map +1 -1
- package/dist/commands/prd09/codemods/audio-wrapper.d.ts.map +1 -1
- package/dist/commands/prd09/codemods/audio-wrapper.js.map +1 -1
- package/dist/commands/prd09/codemods/capture-branches.d.ts.map +1 -1
- package/dist/commands/prd09/codemods/capture-branches.js.map +1 -1
- package/dist/commands/prd09/codemods/evidence-globals.d.ts.map +1 -1
- package/dist/commands/prd09/codemods/evidence-globals.js.map +1 -1
- package/dist/commands/prd09/codemods/perf-script.d.ts.map +1 -1
- package/dist/commands/prd09/codemods/perf-script.js.map +1 -1
- package/dist/commands/prd09/index.d.ts.map +1 -1
- package/dist/commands/prd09/index.js.map +1 -1
- package/dist/commands/prd09/perf-report.d.ts.map +1 -1
- package/dist/commands/prd09/perf-report.js.map +1 -1
- package/dist/commands/prd09/sfx-admit.d.ts.map +1 -1
- package/dist/commands/prd09/sfx-admit.js.map +1 -1
- package/dist/commands/prd10/index.d.ts.map +1 -1
- package/dist/commands/prd10/index.js.map +1 -1
- package/dist/commands/prd11/batchOptout.d.ts.map +1 -1
- package/dist/commands/prd11/batchOptout.js.map +1 -1
- package/dist/commands/prd11/index.d.ts.map +1 -1
- package/dist/commands/prd11/index.js.map +1 -1
- package/dist/commands/prd11/perfGate.d.ts.map +1 -1
- package/dist/commands/prd11/perfGate.js.map +1 -1
- package/dist/commands/prd13/index.d.ts.map +1 -1
- package/dist/commands/prd13/index.js.map +1 -1
- package/dist/commands/prd13/run.d.ts.map +1 -1
- package/dist/commands/prd13/run.js.map +1 -1
- package/dist/commands/prd14/index.d.ts.map +1 -1
- package/dist/commands/prd14/index.js.map +1 -1
- package/dist/commands/prd15/index.d.ts.map +1 -1
- package/dist/commands/prd15/index.js.map +1 -1
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js.map +1 -1
- package/dist/contracts/assetManifest.d.ts.map +1 -1
- package/dist/contracts/assetManifest.js.map +1 -1
- package/dist/contracts/commands.d.ts.map +1 -1
- package/dist/contracts/commands.js.map +1 -1
- package/dist/deploy-browser-check.d.ts.map +1 -1
- package/dist/deploy-browser-check.js.map +1 -1
- package/dist/game-asset-profiles.d.ts.map +1 -1
- package/dist/game-asset-profiles.js.map +1 -1
- package/dist/game-asset-validator.d.ts.map +1 -1
- package/dist/game-asset-validator.js.map +1 -1
- package/dist/glb-dependency-validation.d.ts +41 -0
- package/dist/glb-dependency-validation.d.ts.map +1 -0
- package/dist/glb-dependency-validation.js +574 -0
- package/dist/glb-dependency-validation.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1276 -35
- package/dist/index.js.map +1 -1
- package/dist/library-manifest.d.ts.map +1 -1
- package/dist/library-manifest.js.map +1 -1
- package/dist/lint/feelSourceScan.d.ts.map +1 -1
- package/dist/lint/feelSourceScan.js.map +1 -1
- package/dist/look/capture.d.ts.map +1 -1
- package/dist/look/capture.js.map +1 -1
- package/dist/look/judge.d.ts.map +1 -1
- package/dist/look/judge.js.map +1 -1
- package/dist/look/lint-static.d.ts.map +1 -1
- package/dist/look/lint-static.js.map +1 -1
- package/dist/look/rubric.d.ts.map +1 -1
- package/dist/look/rubric.js.map +1 -1
- package/dist/meshy/admission.d.ts.map +1 -1
- package/dist/meshy/admission.js.map +1 -1
- package/dist/meshy/evidence.d.ts.map +1 -1
- package/dist/meshy/evidence.js.map +1 -1
- package/dist/meshy/import.d.ts.map +1 -1
- package/dist/meshy/import.js.map +1 -1
- package/dist/meshy/index.d.ts.map +1 -1
- package/dist/meshy/index.js.map +1 -1
- package/dist/meshy/metadata.d.ts.map +1 -1
- package/dist/meshy/metadata.js.map +1 -1
- package/dist/meshy/provenance.d.ts.map +1 -1
- package/dist/meshy/provenance.js.map +1 -1
- package/dist/meshy/thumbnail.d.ts.map +1 -1
- package/dist/meshy/thumbnail.js.map +1 -1
- package/dist/meshy/validation.d.ts.map +1 -1
- package/dist/meshy/validation.js.map +1 -1
- package/dist/migrate-three/CompatibilityWarnings.d.ts.map +1 -1
- package/dist/migrate-three/CompatibilityWarnings.js.map +1 -1
- package/dist/migrate-three/ImportMap.d.ts.map +1 -1
- package/dist/migrate-three/ImportMap.js.map +1 -1
- package/dist/migrate-three/R3fMigration.d.ts.map +1 -1
- package/dist/migrate-three/R3fMigration.js.map +1 -1
- package/dist/migrate-three/ThreeToA3DAdapter.d.ts.map +1 -1
- package/dist/migrate-three/ThreeToA3DAdapter.js.map +1 -1
- package/dist/migrate-three/emit.d.ts.map +1 -1
- package/dist/migrate-three/emit.js.map +1 -1
- package/dist/migrate-three/mappings.d.ts.map +1 -1
- package/dist/migrate-three/mappings.js +32 -17
- package/dist/migrate-three/mappings.js.map +1 -1
- package/dist/migrate-three/parse.d.ts.map +1 -1
- package/dist/migrate-three/parse.js.map +1 -1
- package/dist/migrate-three/report.d.ts.map +1 -1
- package/dist/migrate-three/report.js.map +1 -1
- package/dist/pull-bridge/adapters.d.ts.map +1 -1
- package/dist/pull-bridge/adapters.js.map +1 -1
- package/dist/pull-bridge/download.d.ts.map +1 -1
- package/dist/pull-bridge/download.js.map +1 -1
- package/dist/pull-bridge/profiles.d.ts.map +1 -1
- package/dist/pull-bridge/profiles.js.map +1 -1
- package/dist/pull-bridge/provenance.d.ts.map +1 -1
- package/dist/pull-bridge/provenance.js.map +1 -1
- package/dist/pull-bridge/pullable.d.ts.map +1 -1
- package/dist/pull-bridge/pullable.js.map +1 -1
- package/dist/pull-bridge/scoring.d.ts.map +1 -1
- package/dist/pull-bridge/scoring.js.map +1 -1
- package/dist/pull-bridge/search.d.ts.map +1 -1
- package/dist/pull-bridge/search.js.map +1 -1
- package/dist/pull-bridge/types.d.ts.map +1 -1
- package/dist/pull-bridge/types.js.map +1 -1
- package/dist/pull-bridge/vector3.d.ts.map +1 -1
- package/dist/pull-bridge/vector3.js.map +1 -1
- package/dist/pull-bridge.d.ts.map +1 -1
- package/dist/pull-bridge.js.map +1 -1
- package/dist/resolve-ladder.d.ts.map +1 -1
- package/dist/resolve-ladder.js.map +1 -1
- package/dist/skin/analysis.d.ts.map +1 -1
- package/dist/skin/analysis.js.map +1 -1
- package/dist/skin/command.d.ts.map +1 -1
- package/dist/skin/command.js.map +1 -1
- package/dist/skin/glbEdit.d.ts.map +1 -1
- package/dist/skin/glbEdit.js.map +1 -1
- package/dist/skin/gltf.d.ts +15 -0
- package/dist/skin/gltf.d.ts.map +1 -1
- package/dist/skin/gltf.js.map +1 -1
- package/dist/skin/rebuildLimbs.d.ts.map +1 -1
- package/dist/skin/rebuildLimbs.js.map +1 -1
- package/dist/skin/refitSkeleton.d.ts.map +1 -1
- package/dist/skin/refitSkeleton.js.map +1 -1
- package/dist/skin/repair.d.ts.map +1 -1
- package/dist/skin/repair.js.map +1 -1
- package/package.json +7 -6
- package/skills/aura3d-browser-game/references/kits-and-hooks.md +1 -1
- package/skills/aura3d-scene-authoring/SKILL.md +2 -2
- package/skills/llms.txt +2 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, copyFileSync, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { readSkinAllowlist, skinReleaseFailures } from "./skin/command.js";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { inflateSync } from "node:zlib";
|
|
9
9
|
import { writeAgentSkills } from "create-aura3d";
|
|
10
10
|
import { measureLoudness } from "@aura3d/audio";
|
|
11
|
+
import { inspectNRRDDataReference, OBJLoader, parseModel3MF, parsePLY, parseSTL } from "@aura3d/assets";
|
|
11
12
|
import { admitAssetForRole } from "./asset-role-admission.js";
|
|
13
|
+
import { assertStagedGlbFile, captureGlbSource, checkGlbDependencies, referencedGlbResourcePaths } from "./glb-dependency-validation.js";
|
|
12
14
|
import { CERTIFY_CATEGORY_RULES } from "./commands/assets/certifyCategories.js";
|
|
13
15
|
import { hasStylizedFlatApproval } from "./admission/artDirection.js";
|
|
14
16
|
import { AUDIO_SYNTH_RELEASE_ALLOWLIST, isSynthesizedAudioProvenance } from "./admission/audio.js";
|
|
15
17
|
import { validateCubeLut } from "./admission/cubeLut.js";
|
|
16
18
|
import { profileForRole, requiresPbrTextures } from "./admission/profiles.js";
|
|
17
|
-
import { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN } from "./asset-constants.js";
|
|
19
|
+
import { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN, NATIVE_CATALOG_MODEL_FORMATS } from "./asset-constants.js";
|
|
18
20
|
import { shouldScanSource, validateAssetSource } from "./asset-source-validation.js";
|
|
19
21
|
import { listAssets, readAssetManifest, writeAssetManifest, writeTypedAssets } from "./asset-manifest.js";
|
|
20
22
|
export { animationCliAssetProfiles, getAnimationAssetProfileDefinition } from "./animation-asset-profiles.js";
|
|
@@ -149,38 +151,367 @@ function measureAudioLoudness(sourcePath) {
|
|
|
149
151
|
return undefined;
|
|
150
152
|
}
|
|
151
153
|
}
|
|
154
|
+
/** Synchronous admission remains available where inspection is synchronous. */
|
|
152
155
|
export function addAsset(options) {
|
|
156
|
+
const sourcePath = resolve(options.projectDir ?? process.cwd(), options.file);
|
|
157
|
+
const format = extname(sourcePath).slice(1).toLowerCase();
|
|
158
|
+
if (format === "3mf")
|
|
159
|
+
throw new Error("Aura3D assets add failed: 3MF archive inspection is asynchronous; use addAssetAsync or the assets add command.");
|
|
160
|
+
if (format === "pvr")
|
|
161
|
+
throw new Error("Aura3D assets add failed: PVR texture inspection is asynchronous; use addAssetAsync or the assets add command.");
|
|
162
|
+
if (format === "vox")
|
|
163
|
+
throw new Error("Aura3D assets add failed: VOX model inspection is asynchronous; use addAssetAsync or the assets add command.");
|
|
164
|
+
if (format === "nrrd" || format === "nhdr")
|
|
165
|
+
throw new Error("Aura3D assets add failed: scalar-volume inspection is asynchronous; use addAssetAsync or the assets add command.");
|
|
166
|
+
// Without strict inspection the deprecated legacy path stays available and is
|
|
167
|
+
// recorded as legacy-unverified; parser admission for catalog formats is async.
|
|
168
|
+
if (NATIVE_CATALOG_MODEL_FORMATS.has(format) && options.modelInspection === "strict") {
|
|
169
|
+
throw new Error(`Aura3D assets add failed: strict bounded parser inspection is unavailable synchronously for .${format}; native-format admission is asynchronous, use addAssetAsync or the assets add command.`);
|
|
170
|
+
}
|
|
171
|
+
if (format === "glb") {
|
|
172
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
173
|
+
const captured = captureGlbSource(projectDir, sourcePath);
|
|
174
|
+
if (captured.dependencies.length > 0) {
|
|
175
|
+
const inspection = inspectGlb(captured.bytes);
|
|
176
|
+
return withScalarVolumeAdmissionLock(projectDir, () => addAssetPrepared(options, {
|
|
177
|
+
bytes: captured.bytes,
|
|
178
|
+
inspector: "glb",
|
|
179
|
+
dependencies: captured.dependencies,
|
|
180
|
+
inspection: { ...inspection, dependencies: captured.dependencies.map((item) => item.uri) },
|
|
181
|
+
assertSourceUnchanged: captured.assertUnchanged
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
// Keep single-file GLB names and no-copy behavior unchanged, but use the
|
|
185
|
+
// captured bytes so an embedded document cannot turn into an unchecked
|
|
186
|
+
// multi-file document between inspection and copy.
|
|
187
|
+
return addAssetPrepared(options, undefined, captured);
|
|
188
|
+
}
|
|
189
|
+
let native = format === "ply" || format === "stl" ? inspectNativeMeshFile(sourcePath, format) : undefined;
|
|
190
|
+
if (format === "obj" && (options.type === undefined || options.type === "model")) {
|
|
191
|
+
try {
|
|
192
|
+
native = inspectNativeObjFile(sourcePath);
|
|
193
|
+
checkNativeAdmissionDestination(options, native);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (options.modelInspection === "strict")
|
|
197
|
+
throw error;
|
|
198
|
+
// Existing explicit OBJ admission remains usable during deprecation, but
|
|
199
|
+
// it is explicitly recorded as unverified rather than a parser pass.
|
|
200
|
+
native = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return addAssetPrepared(options, native);
|
|
204
|
+
}
|
|
205
|
+
/** Inspect asynchronous formats before publishing any asset, manifest or generated source. */
|
|
206
|
+
export async function addAssetAsync(options) {
|
|
207
|
+
const sourcePath = resolve(options.projectDir ?? process.cwd(), options.file);
|
|
208
|
+
const format = extname(sourcePath).slice(1).toLowerCase();
|
|
209
|
+
if (format === "pvr") {
|
|
210
|
+
// The parser is asynchronous, so retain the admission destination and
|
|
211
|
+
// supplied digest chosen when the operation began.
|
|
212
|
+
const admission = { ...options };
|
|
213
|
+
if (admission.type !== undefined && admission.type !== "texture") {
|
|
214
|
+
throw new Error("Aura3D assets add failed: .pvr must use --type texture.");
|
|
215
|
+
}
|
|
216
|
+
// Keep one bounded snapshot for inspection, hashing, and the staged copy.
|
|
217
|
+
// No GPU formats are claimed at admission time; the parser may use its
|
|
218
|
+
// supported CPU fallback or reject a format needing a device/transcoder.
|
|
219
|
+
const bytes = readBoundedNativeFile(sourcePath);
|
|
220
|
+
const { parseImageFormat } = await import("@aura3d/assets/importers");
|
|
221
|
+
const parsed = await parseImageFormat(bytes, "pvr", {
|
|
222
|
+
maxFileBytes: NATIVE_IMPORT_MAX_BYTES,
|
|
223
|
+
maxPixels: 16 * 1024 * 1024,
|
|
224
|
+
maxDecodedBytes: 64 * 1024 * 1024
|
|
225
|
+
});
|
|
226
|
+
const metadata = parsed.metadata.metadata;
|
|
227
|
+
const pixelFormat = parsed.metadata.pixelFormat;
|
|
228
|
+
const premultiplied = (typeof parsed.metadata.flags === "number" && (parsed.metadata.flags & 2) !== 0)
|
|
229
|
+
|| (Array.isArray(pixelFormat) && pixelFormat[1] === 0 && (pixelFormat[0] === 8 || pixelFormat[0] === 10));
|
|
230
|
+
const formatWarnings = [
|
|
231
|
+
"PVR container inspection does not establish renderer, texture-upload, or device support.",
|
|
232
|
+
...(parsed.levels.some((level) => level.encoding !== "rgba8" && level.encoding !== "rgba16")
|
|
233
|
+
? ["The parsed PVR pixel encoding may not be supported by the current native texture bridge."] : []),
|
|
234
|
+
...(metadata instanceof Uint8Array && metadata.byteLength > 0
|
|
235
|
+
? ["The current native texture bridge does not support PVR orientation or other metadata."] : []),
|
|
236
|
+
...(premultiplied ? ["Premultiplied PVR conversion supports opaque RGBA8 or UNORM16 pixels and transparent linear RGBA8 pixels; compressed GPU payloads and other cases may be rejected."] : []),
|
|
237
|
+
...parsed.diagnostics
|
|
238
|
+
];
|
|
239
|
+
return addAssetPrepared(admission, {
|
|
240
|
+
bytes,
|
|
241
|
+
inspector: "pvr",
|
|
242
|
+
dependencies: [],
|
|
243
|
+
inspection: { ...inspectAssetFile(sourcePath, format), nativeWarnings: formatWarnings }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (format === "vox") {
|
|
247
|
+
const admission = {
|
|
248
|
+
...options,
|
|
249
|
+
...(options.provenanceEvidence ? { provenanceEvidence: [...options.provenanceEvidence] } : {})
|
|
250
|
+
};
|
|
251
|
+
if (admission.type !== undefined && admission.type !== "model") {
|
|
252
|
+
throw new Error("Aura3D assets add failed: .vox must use --type model.");
|
|
253
|
+
}
|
|
254
|
+
// The parser consumes one bounded snapshot; the same bytes are hashed and
|
|
255
|
+
// staged below even if the source file changes during the dynamic import.
|
|
256
|
+
const bytes = readBoundedNativeFile(sourcePath);
|
|
257
|
+
const { parseVOX } = await import("@aura3d/assets/importers");
|
|
258
|
+
const parsed = parseVOX(bytes, {
|
|
259
|
+
frame: 0,
|
|
260
|
+
maxFileBytes: NATIVE_IMPORT_MAX_BYTES,
|
|
261
|
+
maxOutputBytes: NATIVE_RENDER_BUFFER_MAX_BYTES,
|
|
262
|
+
maxVoxelCount: 1_000_000,
|
|
263
|
+
maxNodes: 100_000,
|
|
264
|
+
maxElements: 2_000_000,
|
|
265
|
+
maxListItems: 12_000_000
|
|
266
|
+
});
|
|
267
|
+
const prepared = prepareNativeMeshData(sourcePath, "vox", bytes, parsed.mesh, [
|
|
268
|
+
"VOX inspection uses frame 0 and flattens visible scene transforms into a Y-up mesh; animation and hierarchy are not retained by this adapter.",
|
|
269
|
+
...parsed.diagnostics
|
|
270
|
+
]);
|
|
271
|
+
return addAssetPrepared(admission, { ...prepared, formatMetadata: { version: String(parsed.version) } });
|
|
272
|
+
}
|
|
273
|
+
if (format === "nrrd" || format === "nhdr") {
|
|
274
|
+
const admission = {
|
|
275
|
+
...options,
|
|
276
|
+
...(options.provenanceEvidence ? { provenanceEvidence: [...options.provenanceEvidence] } : {})
|
|
277
|
+
};
|
|
278
|
+
if (admission.type !== undefined && admission.type !== "scalar-volume") {
|
|
279
|
+
throw new Error(`Aura3D assets add failed: .${format} must use --type scalar-volume.`);
|
|
280
|
+
}
|
|
281
|
+
// Read and retain the exact root and child snapshots before inspection;
|
|
282
|
+
// neither staged file is reread from the source after the parser yields.
|
|
283
|
+
const bytes = readBoundedNativeFile(sourcePath);
|
|
284
|
+
const reference = inspectNRRDDataReference(bytes, { maxBytes: NATIVE_IMPORT_MAX_BYTES });
|
|
285
|
+
const dependencies = [];
|
|
286
|
+
if (reference !== undefined) {
|
|
287
|
+
const path = safeNRRDDependencyPath(reference, `volume.${format}`);
|
|
288
|
+
const sourceDir = dirname(sourcePath);
|
|
289
|
+
const dependency = resolve(sourceDir, path);
|
|
290
|
+
assertNoNativeSymlinks(sourceDir, dependency);
|
|
291
|
+
const remaining = NATIVE_IMPORT_MAX_BYTES - bytes.byteLength;
|
|
292
|
+
if (remaining <= 0)
|
|
293
|
+
throw new Error("Aura3D assets add failed: NRRD root and dependency exceed the 64 MiB input budget.");
|
|
294
|
+
dependencies.push({ uri: reference, path, bytes: readBoundedNativeFile(dependency, remaining) });
|
|
295
|
+
}
|
|
296
|
+
const { parseNRRD, inspectScalarVolume } = await import("@aura3d/assets/importers");
|
|
297
|
+
const parsed = await parseNRRD(bytes, {
|
|
298
|
+
maxBytes: NATIVE_IMPORT_MAX_BYTES,
|
|
299
|
+
maxElements: 16_777_216,
|
|
300
|
+
maxOutputBytes: 256 * 1024 * 1024,
|
|
301
|
+
...(dependencies[0] ? { detachedData: dependencies[0].bytes } : {})
|
|
302
|
+
});
|
|
303
|
+
const volume = inspectScalarVolume(parsed, { maxVoxels: 16_777_216 });
|
|
304
|
+
const inspection = inspectScalarVolumeBounds(sourcePath, format, volume.dataToLocal);
|
|
305
|
+
return withScalarVolumeAdmissionLock(resolve(admission.projectDir ?? process.cwd()), () => addAssetPrepared(admission, {
|
|
306
|
+
bytes, inspector: "nrrd", dependencies,
|
|
307
|
+
inspection: { ...inspection, dependencies: reference === undefined ? [] : [reference],
|
|
308
|
+
nativeWarnings: volume.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`) }
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
if (NATIVE_CATALOG_MODEL_FORMATS.has(format)) {
|
|
312
|
+
if (options.type !== undefined && options.type !== "model")
|
|
313
|
+
throw new Error(`Aura3D assets add failed: .${format} must use --type model.`);
|
|
314
|
+
// One bounded snapshot is admitted by the format's real parser (and glTF
|
|
315
|
+
// projection) under the shared catalog limits, then hashed and staged.
|
|
316
|
+
const bytes = readBoundedNativeFile(sourcePath);
|
|
317
|
+
const { admitNativeFormat } = await import("@aura3d/assets/importers");
|
|
318
|
+
let admitted;
|
|
319
|
+
try {
|
|
320
|
+
admitted = await admitNativeFormat(bytes, format, { limits: { maxFileBytes: NATIVE_IMPORT_MAX_BYTES } });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
// An explicit legacy request keeps the deprecated, unverified path.
|
|
324
|
+
if (options.modelInspection === "legacy")
|
|
325
|
+
return addAsset(options);
|
|
326
|
+
throw new Error(`Aura3D assets add failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
327
|
+
}
|
|
328
|
+
const inspection = {
|
|
329
|
+
...inspectAssetFile(sourcePath, format),
|
|
330
|
+
nativeWarnings: [
|
|
331
|
+
`${admitted.format} admitted by the bounded native parser: ${JSON.stringify(admitted.summary)}.`,
|
|
332
|
+
...admitted.warnings
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
return addAssetPrepared(options, { bytes, inspection, inspector: "native", dependencies: [] });
|
|
336
|
+
}
|
|
337
|
+
if (format !== "3mf")
|
|
338
|
+
return addAsset(options);
|
|
339
|
+
const bytes = readBoundedNativeFile(sourcePath);
|
|
340
|
+
const parsed = await parseModel3MF(bytes);
|
|
341
|
+
const inspection = {
|
|
342
|
+
...inspectGltf(parsed.gltf),
|
|
343
|
+
nativeWarnings: parsed.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
|
|
344
|
+
};
|
|
345
|
+
return addAssetPrepared(options, { bytes, inspection, inspector: "3mf", dependencies: [] });
|
|
346
|
+
}
|
|
347
|
+
function scalarVolumeNamespace(format, rootHash, dependencies) {
|
|
348
|
+
const members = dependencies.map((item) => [item.uri, createHash("sha256").update(item.bytes).digest("hex")]);
|
|
349
|
+
const identity = createHash("sha256").update(JSON.stringify(["aura3d.scalar-volume/1", format, rootHash, members])).digest("hex");
|
|
350
|
+
return `scalar-volumes/${identity}`;
|
|
351
|
+
}
|
|
352
|
+
function sameScalarVolumeDependencies(previous, format, dependencies) {
|
|
353
|
+
if (previous.type !== "scalar-volume" || previous.format !== format || previous.integrity?.requireDependencies !== true)
|
|
354
|
+
return false;
|
|
355
|
+
const actual = previous.integrity.dependencies ?? {};
|
|
356
|
+
if (Object.keys(actual).length !== dependencies.length)
|
|
357
|
+
return false;
|
|
358
|
+
return dependencies.every((item) => {
|
|
359
|
+
const expected = createHash("sha256").update(item.bytes).digest("hex");
|
|
360
|
+
const found = actual[item.uri];
|
|
361
|
+
return typeof found === "string" && new RegExp(`^(?:sha256-)?${expected}$`, "i").test(found);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function glbBundleNamespace(rootHash, dependencies) {
|
|
365
|
+
return glbBundleNamespaceFromHashes(rootHash, dependencies.map((item) => ({
|
|
366
|
+
uri: item.uri,
|
|
367
|
+
hash: createHash("sha256").update(item.bytes).digest("hex")
|
|
368
|
+
})));
|
|
369
|
+
}
|
|
370
|
+
function glbBundleNamespaceFromHashes(rootHash, dependencies) {
|
|
371
|
+
const members = dependencies.map((item) => [item.uri, item.hash.replace(/^sha256-/i, "").toLowerCase()])
|
|
372
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
373
|
+
const identity = createHash("sha256").update(JSON.stringify(["aura3d.glb-bundle/1", rootHash.replace(/^sha256-/i, "").toLowerCase(), members])).digest("hex");
|
|
374
|
+
return `glb-bundles/${identity}`;
|
|
375
|
+
}
|
|
376
|
+
function glbBundleDocumentName(dependencies) {
|
|
377
|
+
const paths = dependencies.map((item) => item.path.normalize("NFC").toLowerCase());
|
|
378
|
+
for (let index = 0; index <= paths.length; index++) {
|
|
379
|
+
const candidate = index ? `document-${index}.glb` : "document.glb";
|
|
380
|
+
if (!paths.some((path) => path === candidate || path.startsWith(`${candidate}/`)))
|
|
381
|
+
return candidate;
|
|
382
|
+
}
|
|
383
|
+
throw new Error("Aura3D assets add failed: unable to choose a non-colliding GLB document name.");
|
|
384
|
+
}
|
|
385
|
+
function sameGlbDependencies(previous, dependencies) {
|
|
386
|
+
if (previous.format !== "glb")
|
|
387
|
+
return false;
|
|
388
|
+
const actual = previous.integrity?.dependencies ?? {};
|
|
389
|
+
if (Object.keys(actual).length !== dependencies.length ||
|
|
390
|
+
(dependencies.length > 0 && previous.integrity?.requireDependencies !== true))
|
|
391
|
+
return false;
|
|
392
|
+
return dependencies.every((item) => {
|
|
393
|
+
const expected = createHash("sha256").update(item.bytes).digest("hex");
|
|
394
|
+
const found = actual[item.uri];
|
|
395
|
+
return typeof found === "string" && new RegExp(`^(?:sha256-)?${expected}$`, "i").test(found);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function addAssetPrepared(options, native, embeddedGlb) {
|
|
153
399
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
154
400
|
const sourcePath = resolve(projectDir, options.file);
|
|
155
401
|
if (!existsSync(sourcePath)) {
|
|
156
402
|
throw new Error(`Aura3D assets add failed: "${options.file}" does not exist. Suggested fix: pass a real local GLB/glTF/texture path.`);
|
|
157
403
|
}
|
|
158
404
|
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
405
|
+
// Capture the shared manifest before reading it; do not read through a link
|
|
406
|
+
// or overwrite another writer's changes made during this admission.
|
|
407
|
+
const priorGlbManifest = native?.inspector === "glb" ? captureGlbPublicationTarget(projectDir, manifestPath) : undefined;
|
|
159
408
|
const current = readAssetManifest(projectDir);
|
|
409
|
+
if (native?.inspector === "glb")
|
|
410
|
+
assertGlbPublicationTarget(projectDir, manifestPath, priorGlbManifest);
|
|
160
411
|
// Re-adding (e.g. resolve overwriting) must not silently drop hand-authored
|
|
161
412
|
// provenance the new add does not re-supply (license/attribution/author/etc.).
|
|
162
|
-
const
|
|
413
|
+
const previous = current.assets.find((asset) => asset.id === options.name);
|
|
163
414
|
const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
|
|
164
415
|
const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
|
|
165
416
|
const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
|
|
166
|
-
const hash = hashFile(sourcePath);
|
|
167
417
|
const format = extname(sourcePath).slice(1).toLowerCase();
|
|
168
418
|
const type = options.type ?? inferAssetType(format);
|
|
419
|
+
if (options.modelInspection !== undefined && options.modelInspection !== "legacy" && options.modelInspection !== "strict") {
|
|
420
|
+
throw new Error("Aura3D assets add failed: modelInspection must be legacy or strict.");
|
|
421
|
+
}
|
|
422
|
+
if (native && native.inspector === "pvr" && type !== "texture")
|
|
423
|
+
throw new Error("Aura3D assets add failed: .pvr must use --type texture.");
|
|
424
|
+
if (native && native.inspector === "nrrd" && type !== "scalar-volume")
|
|
425
|
+
throw new Error(`Aura3D assets add failed: .${format} must use --type scalar-volume.`);
|
|
426
|
+
if (native && native.inspector !== "pvr" && native.inspector !== "nrrd" && type !== "model")
|
|
427
|
+
throw new Error(`Aura3D assets add failed: .${format} must use --type model.`);
|
|
428
|
+
if (type === "model" && options.modelInspection === "strict" && (!native || native.inspector === "glb")) {
|
|
429
|
+
throw new Error(`Aura3D assets add failed: strict bounded parser inspection is unavailable for .${format || "unknown"}; glTF currently has metadata-only inspection.`);
|
|
430
|
+
}
|
|
431
|
+
if (native)
|
|
432
|
+
validateNativeAdmissionPaths(projectDir, options.name, outputDir, typegen, publicPath);
|
|
433
|
+
if (native?.inspector === "glb")
|
|
434
|
+
validateGlbPortablePaths(outputDir, typegen, publicPath);
|
|
435
|
+
const snapshot = native?.bytes ?? embeddedGlb?.bytes;
|
|
436
|
+
const hash = snapshot ? createHash("sha256").update(snapshot).digest("hex") : hashFile(sourcePath);
|
|
437
|
+
if (native && options.sha256 !== undefined && !new RegExp(`^(?:sha256-)?${hash}$`, "i").test(options.sha256)) {
|
|
438
|
+
throw new Error("Aura3D assets add failed: supplied provenance SHA-256 does not match the inspected asset bytes.");
|
|
439
|
+
}
|
|
440
|
+
// Rights, render probes and quality from an earlier file do not attest to a
|
|
441
|
+
// different native asset merely because it is admitted under the same id.
|
|
442
|
+
const changedRoot = native && previous && (previous.hash ?? previous.sha256 ?? "").replace(/^sha256-/i, "").toLowerCase() !== hash;
|
|
443
|
+
const changedVolume = native?.inspector === "nrrd" && previous && !sameScalarVolumeDependencies(previous, format, native.dependencies);
|
|
444
|
+
const glbDependencies = native?.inspector === "glb" ? native.dependencies : embeddedGlb?.dependencies;
|
|
445
|
+
const previousGlbRootMatches = previous && (previous.hash ?? previous.sha256 ?? "").replace(/^sha256-/i, "").toLowerCase() === hash;
|
|
446
|
+
const changedGlb = glbDependencies && previous && (previous.type !== type || !previousGlbRootMatches ||
|
|
447
|
+
!sameGlbDependencies(previous, glbDependencies));
|
|
448
|
+
const existing = changedRoot || changedVolume || changedGlb ? undefined : previous;
|
|
449
|
+
const priorProvenance = (native || embeddedGlb) && existing?.provenance?.sha256 &&
|
|
450
|
+
!new RegExp(`^(?:sha256-)?${hash}$`, "i").test(existing.provenance.sha256)
|
|
451
|
+
? { ...existing.provenance, sha256: undefined }
|
|
452
|
+
: existing?.provenance;
|
|
169
453
|
if (type === "lut")
|
|
170
454
|
admitCubeLut(sourcePath, format);
|
|
171
|
-
const inspection = inspectAssetFile(sourcePath, format);
|
|
172
|
-
|
|
455
|
+
const inspection = native?.inspection ?? (embeddedGlb ? inspectGlb(embeddedGlb.bytes) : inspectAssetFile(sourcePath, format));
|
|
456
|
+
// An unchanged root cannot supply fresh rights evidence for substituted
|
|
457
|
+
// sidecars (or an incomplete prior bundle). On later re-adds, use only the
|
|
458
|
+
// already identity-bound provenance or explicitly supplied new evidence;
|
|
459
|
+
// otherwise the same embedded text would silently resurrect stale rights.
|
|
460
|
+
const detectedProvenance = native?.inspector === "glb" && previousGlbRootMatches
|
|
461
|
+
? undefined
|
|
462
|
+
: inspection.provenance;
|
|
463
|
+
// Keep a detached header and its unmodified relative child under an
|
|
464
|
+
// immutable namespace bound to both snapshots, not just the header hash.
|
|
465
|
+
const namespace = native?.inspector === "nrrd" ? scalarVolumeNamespace(format, hash, native.dependencies)
|
|
466
|
+
: native?.inspector === "glb" ? glbBundleNamespace(hash, native.dependencies) : undefined;
|
|
467
|
+
const outputFileName = namespace
|
|
468
|
+
? `${namespace}/${native?.inspector === "glb" ? glbBundleDocumentName(native.dependencies) : `volume.${format}`}`
|
|
469
|
+
: `${options.name}.${hash.slice(0, 8)}.${format}`;
|
|
173
470
|
const outputPath = join(outputDir, outputFileName);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
471
|
+
const stagedNative = native && namespace ? {
|
|
472
|
+
...native,
|
|
473
|
+
dependencies: native.dependencies.map((item) => ({ ...item, path: `${namespace}/${item.path}` }))
|
|
474
|
+
} : native;
|
|
475
|
+
if (stagedNative?.inspector === "nrrd") {
|
|
476
|
+
assertScalarVolumeTargetLayout(projectDir, outputPath, outputDir, typegen, options.name, stagedNative.dependencies);
|
|
477
|
+
}
|
|
478
|
+
const glbTargets = native?.inspector === "glb" && stagedNative && namespace
|
|
479
|
+
? assertGlbTargetLayout(projectDir, sourcePath, outputPath, outputDir, typegen, options.name, namespace, native.dependencies, stagedNative.dependencies, current, priorGlbManifest)
|
|
480
|
+
: undefined;
|
|
481
|
+
if (native && options.copy === false) {
|
|
482
|
+
const stagedPath = resolve(projectDir, outputPath);
|
|
483
|
+
assertNoNativeSymlinks(projectDir, stagedPath);
|
|
484
|
+
if (native.inspector === "glb")
|
|
485
|
+
assertStagedGlbFile(projectDir, stagedPath, native.bytes, "copy:false requires the matching hashed file at the output path");
|
|
486
|
+
else if (!existsSync(stagedPath) || !statSync(stagedPath).isFile() || statSync(stagedPath).size !== native.bytes.length || !readBoundedNativeFile(stagedPath).equals(native.bytes)) {
|
|
487
|
+
throw new Error("Aura3D assets add failed: copy:false requires the matching hashed file to already exist at the output path.");
|
|
488
|
+
}
|
|
489
|
+
for (const dependency of stagedNative.dependencies) {
|
|
490
|
+
const stagedDependency = resolve(projectDir, outputDir, dependency.path);
|
|
491
|
+
assertNoNativeSymlinks(projectDir, stagedDependency);
|
|
492
|
+
if (native.inspector === "glb")
|
|
493
|
+
assertStagedGlbFile(projectDir, stagedDependency, dependency.bytes, `copy:false requires the matching dependency ${dependency.uri} at the output path`);
|
|
494
|
+
else if (!existsSync(stagedDependency) || !statSync(stagedDependency).isFile() || statSync(stagedDependency).size !== dependency.bytes.length || !readBoundedNativeFile(stagedDependency).equals(dependency.bytes)) {
|
|
495
|
+
throw new Error(`Aura3D assets add failed: copy:false requires the matching dependency ${dependency.uri} at the output path.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
178
498
|
}
|
|
179
|
-
const thumbnailUrl =
|
|
499
|
+
const thumbnailUrl = `${publicPath}${options.name}.thumb.svg`;
|
|
180
500
|
const entry = {
|
|
181
501
|
id: options.name,
|
|
182
502
|
type,
|
|
183
503
|
format,
|
|
504
|
+
...(native?.formatMetadata ? { formatMetadata: native.formatMetadata } : {}),
|
|
505
|
+
...(type === "model" ? {
|
|
506
|
+
modelInspection: native && native.inspector !== "pvr" && native.inspector !== "nrrd" && native.inspector !== "glb"
|
|
507
|
+
? { version: 1, status: "parser-inspected", inspector: (native.inspector === "native" ? format : native.inspector), digest: `sha256-${hash}` }
|
|
508
|
+
: { version: 1, status: (format === "glb" || format === "gltf" ? "metadata-only" : "legacy-unverified") }
|
|
509
|
+
} : {}),
|
|
510
|
+
...(native ? { integrity: {
|
|
511
|
+
version: 1,
|
|
512
|
+
digest: `sha256-${hash}`,
|
|
513
|
+
...(native.inspector === "obj" || native.inspector === "nrrd" || native.dependencies.length ? { dependencies: Object.fromEntries(native.dependencies.map((item) => [item.uri, `sha256-${createHash("sha256").update(item.bytes).digest("hex")}`])), requireDependencies: true } : {})
|
|
514
|
+
} } : {}),
|
|
184
515
|
/*
|
|
185
516
|
* Record a source path that still exists after the command finishes.
|
|
186
517
|
*
|
|
@@ -198,7 +529,7 @@ export function addAsset(options) {
|
|
|
198
529
|
outputPath: normalizeRelativePath(outputPath),
|
|
199
530
|
url: `${publicPath}${outputFileName}`,
|
|
200
531
|
hash: `sha256-${hash}`,
|
|
201
|
-
sizeBytes: statSync(sourcePath).size,
|
|
532
|
+
sizeBytes: snapshot?.byteLength ?? statSync(sourcePath).size,
|
|
202
533
|
bounds: inspection.bounds,
|
|
203
534
|
boundsMetadata: inspection.boundsMetadata,
|
|
204
535
|
materials: inspection.materials,
|
|
@@ -209,7 +540,7 @@ export function addAsset(options) {
|
|
|
209
540
|
skeleton: inspection.skeleton,
|
|
210
541
|
morphTargets: inspection.morphTargets,
|
|
211
542
|
hierarchy: inspection.hierarchy,
|
|
212
|
-
provenance: createAssetProvenance(projectDir, sourcePath, options, options.replaceProvenanceEvidence ?
|
|
543
|
+
provenance: createAssetProvenance(projectDir, sourcePath, options, options.replaceProvenanceEvidence ? detectedProvenance : mergeDetectedProvenance(priorProvenance, detectedProvenance), outputPath),
|
|
213
544
|
textures: inspection.textures,
|
|
214
545
|
dependencies: inspection.dependencies,
|
|
215
546
|
orientation: options.orientation ?? (existing?.orientation?.source === "manifest-override" && existing.orientation.assetHash === `sha256-${hash}`
|
|
@@ -245,7 +576,13 @@ export function addAsset(options) {
|
|
|
245
576
|
...(c17AnimationClips(inspection.animation) ?? existing?.animationClips
|
|
246
577
|
? { animationClips: c17AnimationClips(inspection.animation) ?? existing?.animationClips }
|
|
247
578
|
: {}),
|
|
248
|
-
warnings:
|
|
579
|
+
warnings: [
|
|
580
|
+
...createAssetWarnings(sourcePath, inspection, snapshot?.byteLength),
|
|
581
|
+
...(inspection.nativeWarnings ?? []),
|
|
582
|
+
...(type === "model" && !native && format !== "gltf" && format !== "glb"
|
|
583
|
+
? [`Legacy model admission: .${format || "unknown"} bytes were not parser-inspected; this behavior is deprecated and does not establish adapter or format support.`]
|
|
584
|
+
: [])
|
|
585
|
+
]
|
|
249
586
|
};
|
|
250
587
|
if (options.orientation) {
|
|
251
588
|
if (options.orientation.source !== "manifest-override") {
|
|
@@ -268,8 +605,30 @@ export function addAsset(options) {
|
|
|
268
605
|
entry
|
|
269
606
|
]
|
|
270
607
|
});
|
|
271
|
-
|
|
272
|
-
|
|
608
|
+
if (native) {
|
|
609
|
+
const glbBeforeCommit = native.inspector === "glb" ? () => {
|
|
610
|
+
native.assertSourceUnchanged?.();
|
|
611
|
+
assertStagedGlbFile(projectDir, resolve(projectDir, outputPath), native.bytes, "staged GLB changed during admission");
|
|
612
|
+
for (const dependency of stagedNative.dependencies) {
|
|
613
|
+
assertStagedGlbFile(projectDir, resolve(projectDir, outputDir, dependency.path), dependency.bytes, `staged GLB dependency ${dependency.uri} changed during admission`);
|
|
614
|
+
}
|
|
615
|
+
} : undefined;
|
|
616
|
+
publishNativeAdmission(projectDir, manifest, outputPath, sourcePath, options.name, inspection.bounds, options.copy === false ? undefined : stagedNative, glbBeforeCommit, glbTargets);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
embeddedGlb?.assertUnchanged();
|
|
620
|
+
if (options.copy !== false) {
|
|
621
|
+
mkdirSync(resolve(projectDir, outputDir), { recursive: true });
|
|
622
|
+
if (embeddedGlb)
|
|
623
|
+
writeFileSync(resolve(projectDir, outputPath), embeddedGlb.bytes);
|
|
624
|
+
else
|
|
625
|
+
copyFileSync(sourcePath, resolve(projectDir, outputPath));
|
|
626
|
+
copyAssetDependencies(projectDir, sourcePath, outputDir, inspection.dependencies);
|
|
627
|
+
}
|
|
628
|
+
writeThumbnail(projectDir, outputDir, publicPath, options.name, inspection.bounds);
|
|
629
|
+
writeAssetManifest(projectDir, manifest);
|
|
630
|
+
writeTypedAssets(projectDir, manifest);
|
|
631
|
+
}
|
|
273
632
|
return {
|
|
274
633
|
ok: true,
|
|
275
634
|
manifestPath,
|
|
@@ -277,7 +636,11 @@ export function addAsset(options) {
|
|
|
277
636
|
messages: [
|
|
278
637
|
`Added ${entry.id} -> ${entry.url}`,
|
|
279
638
|
`Wrote ${DEFAULT_AURA_ASSET_MANIFEST}`,
|
|
280
|
-
`Wrote ${manifest.typegen}
|
|
639
|
+
`Wrote ${manifest.typegen}`,
|
|
640
|
+
...(native && native.inspector !== "pvr" && native.inspector !== "nrrd" && native.inspector !== "glb" ? [`Register a renderer-capable ${format.toUpperCase()} adapter in the app's FormatRegistry before loading this asset.`] : []),
|
|
641
|
+
...(native?.inspector === "pvr" ? ["PVR bytes were parser-inspected; texture upload and device support have not been established by admission."] : []),
|
|
642
|
+
...(native?.inspector === "obj" ? ["OBJ inspection covers geometry, material references, and image signatures; image decoding and rendered fidelity remain unverified."] : []),
|
|
643
|
+
...(type === "model" && !native && format !== "gltf" && format !== "glb" ? ["Legacy model admission is unverified and deprecated; use --model-inspection strict after a bounded parser is available."] : [])
|
|
281
644
|
]
|
|
282
645
|
};
|
|
283
646
|
}
|
|
@@ -756,9 +1119,64 @@ export function validateAssets(options = {}) {
|
|
|
756
1119
|
failures.push(`Missing asset output for "${asset.id}": ${asset.outputPath}`);
|
|
757
1120
|
continue;
|
|
758
1121
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1122
|
+
let scalarRoot;
|
|
1123
|
+
let glbJson;
|
|
1124
|
+
let actualHash;
|
|
1125
|
+
if (asset.format === "glb") {
|
|
1126
|
+
try {
|
|
1127
|
+
const checked = checkGlbDependencies(projectDir, outputPath, asset);
|
|
1128
|
+
actualHash = checked.hash;
|
|
1129
|
+
glbJson = checked.json;
|
|
1130
|
+
failures.push(...checked.failures);
|
|
1131
|
+
validateGlbBundleNamespace(projectDir, outputPath, asset, checked, failures);
|
|
1132
|
+
}
|
|
1133
|
+
catch (error) {
|
|
1134
|
+
failures.push(`${asset.id}: GLB dependency validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
else if (asset.type === "scalar-volume" && (asset.format === "nrrd" || asset.format === "nhdr")) {
|
|
1139
|
+
try {
|
|
1140
|
+
assertNoNativeSymlinks(projectDir, outputPath);
|
|
1141
|
+
scalarRoot = readBoundedNativeFile(outputPath);
|
|
1142
|
+
actualHash = `sha256-${createHash("sha256").update(scalarRoot).digest("hex")}`;
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
failures.push(`${asset.id}: asset output could not be read safely: ${error instanceof Error ? error.message : String(error)}`);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
else
|
|
1150
|
+
actualHash = `sha256-${hashFile(outputPath)}`;
|
|
1151
|
+
const expectedHash = asset.format === "glb" ? asset.integrity?.digest ?? asset.hash ?? asset.sha256 : asset.hash;
|
|
1152
|
+
if (actualHash !== expectedHash)
|
|
1153
|
+
failures.push(`Hash mismatch for "${asset.id}": expected ${expectedHash}, found ${actualHash}`);
|
|
1154
|
+
if (scalarRoot)
|
|
1155
|
+
failures.push(...validateScalarVolumeDependencies(projectDir, manifest.outputDir, asset, outputPath, scalarRoot));
|
|
1156
|
+
if (asset.format === "obj" && asset.modelInspection?.status === "parser-inspected") {
|
|
1157
|
+
if (!asset.integrity?.requireDependencies)
|
|
1158
|
+
failures.push(`${asset.id}: inspected OBJ must require dependency integrity.`);
|
|
1159
|
+
const digests = asset.integrity?.dependencies ?? {};
|
|
1160
|
+
for (const uri of asset.dependencies ?? []) {
|
|
1161
|
+
if (!Object.hasOwn(digests, uri))
|
|
1162
|
+
failures.push(`${asset.id}: missing dependency integrity for ${uri}.`);
|
|
1163
|
+
}
|
|
1164
|
+
for (const [uri, digest] of Object.entries(digests)) {
|
|
1165
|
+
try {
|
|
1166
|
+
const url = new URL(uri, "https://aura3d.invalid/project/model.obj");
|
|
1167
|
+
const { path } = nativeObjDependencyPath(url);
|
|
1168
|
+
const dependency = resolve(dirname(outputPath), path);
|
|
1169
|
+
assertNoNativeSymlinks(projectDir, dependency);
|
|
1170
|
+
if (!existsSync(dependency) || !statSync(dependency).isFile())
|
|
1171
|
+
failures.push(`${asset.id}: missing OBJ dependency ${uri}.`);
|
|
1172
|
+
else if (`sha256-${createHash("sha256").update(readBoundedNativeFile(dependency)).digest("hex")}` !== digest.toLowerCase())
|
|
1173
|
+
failures.push(`${asset.id}: OBJ dependency hash mismatch for ${uri}.`);
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
failures.push(`${asset.id}: invalid or unsafe OBJ dependency ${uri}.`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
762
1180
|
const provenance = resolveAssetProvenance(asset, externalProvenance);
|
|
763
1181
|
if (noPlaceholders && isPlaceholderAsset(asset, provenance)) {
|
|
764
1182
|
failures.push(`Placeholder asset is not allowed in strict release validation: "${asset.id}". Replace it with a real typed asset and provenance.`);
|
|
@@ -793,7 +1211,7 @@ export function validateAssets(options = {}) {
|
|
|
793
1211
|
skinAllow ??= readSkinAllowlist(projectDir);
|
|
794
1212
|
failures.push(...skinReleaseFailures(projectDir, asset, skinAllow).failures);
|
|
795
1213
|
}
|
|
796
|
-
warnings.push(...createDerivedMetadataDriftWarnings(outputPath, asset));
|
|
1214
|
+
warnings.push(...createDerivedMetadataDriftWarnings(outputPath, asset, glbJson));
|
|
797
1215
|
if (release)
|
|
798
1216
|
warnings.push(...createReleaseStructuredQualityWarnings(projectDir, asset));
|
|
799
1217
|
if (release)
|
|
@@ -846,6 +1264,80 @@ export function validateAssets(options = {}) {
|
|
|
846
1264
|
messages: failures.length === 0 ? [release ? "Asset manifest is release-valid." : "Asset manifest is valid."] : failures
|
|
847
1265
|
};
|
|
848
1266
|
}
|
|
1267
|
+
/** A bundle path is an immutable identity of the actual document and sidecars. */
|
|
1268
|
+
function validateGlbBundleNamespace(projectDir, outputPath, asset, checked, failures) {
|
|
1269
|
+
const parts = relative(projectDir, outputPath).split(sep);
|
|
1270
|
+
const marker = parts.lastIndexOf("glb-bundles");
|
|
1271
|
+
if (marker < 0 || parts[0] === ".." || isAbsolute(relative(projectDir, outputPath)))
|
|
1272
|
+
return;
|
|
1273
|
+
// Only reserve the layout emitted by admission. An unrelated legacy asset
|
|
1274
|
+
// may happen to live in a directory named "glb-bundles".
|
|
1275
|
+
if (parts.length !== marker + 3 || !/^[a-f0-9]{64}$/.test(parts[marker + 1] ?? "") ||
|
|
1276
|
+
!/^document(?:-[1-9][0-9]*)?\.glb$/.test(parts[marker + 2] ?? ""))
|
|
1277
|
+
return;
|
|
1278
|
+
const recorded = asset.integrity?.dependencies ?? {};
|
|
1279
|
+
if (!checked.uris.length || asset.integrity?.requireDependencies !== true ||
|
|
1280
|
+
Object.keys(recorded).length !== checked.uris.length ||
|
|
1281
|
+
checked.uris.some((uri) => !Object.hasOwn(recorded, uri))) {
|
|
1282
|
+
failures.push(`${asset.id}: GLB bundle dependency integrity must exactly match the document references.`);
|
|
1283
|
+
}
|
|
1284
|
+
// Missing or unreadable children already have their own failures; never
|
|
1285
|
+
// invent bytes or calculate a namespace from an incomplete bundle.
|
|
1286
|
+
if (checked.references.length !== checked.uris.length)
|
|
1287
|
+
return;
|
|
1288
|
+
const outputDir = resolve(projectDir, ...parts.slice(0, marker));
|
|
1289
|
+
const expected = resolve(outputDir, glbBundleNamespaceFromHashes(checked.hash, checked.references), glbBundleDocumentName(checked.references));
|
|
1290
|
+
if (expected !== outputPath)
|
|
1291
|
+
failures.push(`${asset.id}: GLB bundle namespace does not match its root and dependency bytes.`);
|
|
1292
|
+
}
|
|
1293
|
+
/** Re-read the declared child reference from the same root bytes being hashed. */
|
|
1294
|
+
function validateScalarVolumeDependencies(projectDir, outputDir, asset, outputPath, root) {
|
|
1295
|
+
const failures = [];
|
|
1296
|
+
try {
|
|
1297
|
+
const reference = inspectNRRDDataReference(root, { maxBytes: NATIVE_IMPORT_MAX_BYTES });
|
|
1298
|
+
const integrity = asset.integrity;
|
|
1299
|
+
const digests = integrity?.dependencies ?? {};
|
|
1300
|
+
const keys = Object.keys(digests);
|
|
1301
|
+
const listed = asset.dependencies ?? [];
|
|
1302
|
+
if (integrity?.requireDependencies !== true)
|
|
1303
|
+
failures.push(`${asset.id}: scalar volume must require dependency integrity.`);
|
|
1304
|
+
if (reference === undefined) {
|
|
1305
|
+
if (keys.length || listed.length)
|
|
1306
|
+
failures.push(`${asset.id}: embedded NRRD has unexpected dependency metadata.`);
|
|
1307
|
+
validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, [], failures);
|
|
1308
|
+
return failures;
|
|
1309
|
+
}
|
|
1310
|
+
const path = safeNRRDDependencyPath(reference, basename(outputPath));
|
|
1311
|
+
if (keys.length !== 1 || !Object.hasOwn(digests, reference) || listed.length !== 1 || listed[0] !== reference) {
|
|
1312
|
+
failures.push(`${asset.id}: NRRD dependency metadata does not match the declared reference.`);
|
|
1313
|
+
return failures;
|
|
1314
|
+
}
|
|
1315
|
+
const child = resolve(dirname(outputPath), path);
|
|
1316
|
+
assertNoNativeSymlinks(projectDir, child);
|
|
1317
|
+
const remaining = NATIVE_IMPORT_MAX_BYTES - root.byteLength;
|
|
1318
|
+
if (remaining <= 0)
|
|
1319
|
+
throw new Error("NRRD root and dependency exceed the 64 MiB input budget");
|
|
1320
|
+
const bytes = readBoundedNativeFile(child, remaining);
|
|
1321
|
+
const digest = `sha256-${createHash("sha256").update(bytes).digest("hex")}`;
|
|
1322
|
+
if (digest !== digests[reference].toLowerCase())
|
|
1323
|
+
failures.push(`${asset.id}: NRRD dependency hash mismatch for ${reference}.`);
|
|
1324
|
+
validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, [{ uri: reference, path, bytes }], failures);
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
failures.push(`${asset.id}: NRRD dependency validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1328
|
+
}
|
|
1329
|
+
return failures;
|
|
1330
|
+
}
|
|
1331
|
+
function validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, dependencies, failures) {
|
|
1332
|
+
const base = resolve(projectDir, outputDir, "scalar-volumes");
|
|
1333
|
+
const rel = relative(base, outputPath);
|
|
1334
|
+
if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel))
|
|
1335
|
+
return;
|
|
1336
|
+
const rootHash = createHash("sha256").update(root).digest("hex");
|
|
1337
|
+
const expected = resolve(projectDir, outputDir, scalarVolumeNamespace(asset.format, rootHash, dependencies), `volume.${asset.format}`);
|
|
1338
|
+
if (expected !== outputPath)
|
|
1339
|
+
failures.push(`${asset.id}: scalar-volume namespace does not match its root and dependency bytes.`);
|
|
1340
|
+
}
|
|
849
1341
|
export function createAssetThumbnails(options = {}) {
|
|
850
1342
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
851
1343
|
const manifest = readAssetManifest(projectDir);
|
|
@@ -1798,17 +2290,23 @@ function pushUnique(target, values) {
|
|
|
1798
2290
|
function createDuplicateHashWarnings(manifest, externalProvenance) {
|
|
1799
2291
|
const byHash = new Map();
|
|
1800
2292
|
for (const asset of manifest.assets) {
|
|
1801
|
-
|
|
2293
|
+
// A document can be byte-identical while its referenced resources differ.
|
|
2294
|
+
// For admitted multi-file assets, the complete identity defines duplication.
|
|
2295
|
+
const multiFile = asset.type === "scalar-volume" || (asset.format === "glb" && asset.integrity?.requireDependencies === true);
|
|
2296
|
+
const key = multiFile
|
|
2297
|
+
? JSON.stringify([asset.type, asset.format, asset.hash, Object.entries(asset.integrity?.dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))])
|
|
2298
|
+
: JSON.stringify(["root", asset.hash]);
|
|
2299
|
+
const existing = byHash.get(key) ?? [];
|
|
1802
2300
|
existing.push(asset);
|
|
1803
|
-
byHash.set(
|
|
2301
|
+
byHash.set(key, existing);
|
|
1804
2302
|
}
|
|
1805
2303
|
const warnings = [];
|
|
1806
|
-
for (const
|
|
2304
|
+
for (const assets of byHash.values()) {
|
|
1807
2305
|
if (assets.length < 2)
|
|
1808
2306
|
continue;
|
|
1809
2307
|
const unexplained = assets.filter((asset) => !hasDuplicateHashAllowlist(resolveAssetProvenance(asset, externalProvenance)));
|
|
1810
2308
|
if (unexplained.length > 0) {
|
|
1811
|
-
warnings.push(`duplicate asset hash ${hash} used by ${assets.map((asset) => `"${asset.id}"`).join(", ")} without duplicate-ok provenance evidence.`);
|
|
2309
|
+
warnings.push(`duplicate asset hash ${assets[0].hash} used by ${assets.map((asset) => `"${asset.id}"`).join(", ")} without duplicate-ok provenance evidence.`);
|
|
1812
2310
|
}
|
|
1813
2311
|
}
|
|
1814
2312
|
return warnings;
|
|
@@ -1873,6 +2371,495 @@ function defaultAttachPoint(slot) {
|
|
|
1873
2371
|
return "spine";
|
|
1874
2372
|
return "root";
|
|
1875
2373
|
}
|
|
2374
|
+
const NATIVE_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
|
|
2375
|
+
const NATIVE_RENDER_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
2376
|
+
function readBoundedNativeFile(path, maxBytes = NATIVE_IMPORT_MAX_BYTES) {
|
|
2377
|
+
if (!existsSync(path))
|
|
2378
|
+
throw new Error(`Aura3D assets add failed: "${path}" does not exist.`);
|
|
2379
|
+
const descriptor = openSync(path, "r");
|
|
2380
|
+
try {
|
|
2381
|
+
const stat = fstatSync(descriptor);
|
|
2382
|
+
if (!stat.isFile() || stat.size > maxBytes) {
|
|
2383
|
+
throw new Error(`Aura3D assets add failed: native asset must be a regular file within the ${maxBytes}-byte remaining input budget (at most 64 MiB).`);
|
|
2384
|
+
}
|
|
2385
|
+
const bytes = Buffer.allocUnsafe(stat.size);
|
|
2386
|
+
let offset = 0;
|
|
2387
|
+
while (offset < bytes.length) {
|
|
2388
|
+
const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
|
2389
|
+
if (!count)
|
|
2390
|
+
throw new Error("Aura3D assets add failed: native file changed while being read.");
|
|
2391
|
+
offset += count;
|
|
2392
|
+
}
|
|
2393
|
+
const extra = Buffer.alloc(1);
|
|
2394
|
+
if (readSync(descriptor, extra, 0, 1, offset) !== 0)
|
|
2395
|
+
throw new Error("Aura3D assets add failed: native file grew while being read.");
|
|
2396
|
+
return bytes;
|
|
2397
|
+
}
|
|
2398
|
+
finally {
|
|
2399
|
+
closeSync(descriptor);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/** Only local, portable relative NRRD children can be staged unchanged. */
|
|
2403
|
+
function safeNRRDDependencyPath(reference, rootName) {
|
|
2404
|
+
if (!reference || reference.length > 2048 || reference.normalize("NFC") !== reference ||
|
|
2405
|
+
isAbsolute(reference) || reference.startsWith("/") || /[\0-\x1f\x7f\\?#%:]/.test(reference)) {
|
|
2406
|
+
throw new Error("Aura3D assets add failed: NRRD dependency must be a safe local relative path.");
|
|
2407
|
+
}
|
|
2408
|
+
const segments = reference.split("/");
|
|
2409
|
+
if (segments.some((part) => !part || part === "." || part === ".." || part.endsWith(".") || part.endsWith(" ") ||
|
|
2410
|
+
Buffer.byteLength(part, "utf8") > 255 || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
|
|
2411
|
+
throw new Error("Aura3D assets add failed: NRRD dependency path is not portable or contains traversal.");
|
|
2412
|
+
}
|
|
2413
|
+
if (segments.length === 1 && reference.toLowerCase() === rootName.toLowerCase()) {
|
|
2414
|
+
throw new Error("Aura3D assets add failed: NRRD dependency collides with its staged header.");
|
|
2415
|
+
}
|
|
2416
|
+
return reference;
|
|
2417
|
+
}
|
|
2418
|
+
function assertScalarVolumeTargetLayout(projectDir, outputPath, outputDir, typegen, name, dependencies) {
|
|
2419
|
+
const targets = [
|
|
2420
|
+
resolve(projectDir, outputPath),
|
|
2421
|
+
...dependencies.map((item) => resolve(projectDir, outputDir, item.path)),
|
|
2422
|
+
resolve(projectDir, outputDir, `${name}.thumb.svg`),
|
|
2423
|
+
resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
2424
|
+
resolve(projectDir, typegen),
|
|
2425
|
+
resolve(projectDir, ".aura3d-scalar-admission.lock")
|
|
2426
|
+
].map((path) => path.normalize("NFC").replaceAll("\\", "/").toLowerCase());
|
|
2427
|
+
for (let i = 0; i < targets.length; i++)
|
|
2428
|
+
for (let j = i + 1; j < targets.length; j++) {
|
|
2429
|
+
const a = targets[i], b = targets[j];
|
|
2430
|
+
if (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)) {
|
|
2431
|
+
throw new Error("Aura3D assets add failed: scalar-volume output, dependency, thumbnail, manifest and typegen paths collide.");
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function captureGlbPublicationTarget(projectDir, path) {
|
|
2436
|
+
assertNoNativeSymlinks(projectDir, path);
|
|
2437
|
+
try {
|
|
2438
|
+
const stat = lstatSync(path);
|
|
2439
|
+
if (!stat.isFile())
|
|
2440
|
+
throw new Error(`Aura3D assets add failed: generated output is not a regular file: ${path}`);
|
|
2441
|
+
return stat;
|
|
2442
|
+
}
|
|
2443
|
+
catch (error) {
|
|
2444
|
+
if (error.code === "ENOENT")
|
|
2445
|
+
return undefined;
|
|
2446
|
+
throw error;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function validateGlbPortablePaths(outputDir, typegen, publicPath) {
|
|
2450
|
+
for (const [label, path] of [["output", outputDir], ["typegen", typegen]]) {
|
|
2451
|
+
if (path.split("/").some((part) => /[\u0000-\u001f\u007f\uD800-\uDFFF<>:"|*]/u.test(part) ||
|
|
2452
|
+
part.endsWith(".") || part.endsWith(" ") || Buffer.byteLength(part, "utf8") > 255 ||
|
|
2453
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
|
|
2454
|
+
throw new Error(`Aura3D assets add failed: GLB ${label} path contains a nonportable name.`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (/[\u0000-\u001f\u007f\uD800-\uDFFF]/u.test(publicPath)) {
|
|
2458
|
+
throw new Error("Aura3D assets add failed: GLB public path contains an unsafe character.");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function sameGlbPublicationTarget(left, right) {
|
|
2462
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
|
|
2463
|
+
left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
2464
|
+
}
|
|
2465
|
+
function assertGlbPublicationTarget(projectDir, path, expected) {
|
|
2466
|
+
const actual = captureGlbPublicationTarget(projectDir, path);
|
|
2467
|
+
if ((!expected && actual) || (expected && (!actual || !sameGlbPublicationTarget(expected, actual)))) {
|
|
2468
|
+
throw new Error(`Aura3D assets add failed: generated output changed or was created during GLB admission: ${path}`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
function assertGlbTargetLayout(projectDir, sourcePath, outputPath, outputDir, typegen, name, namespace, dependencies, stagedDependencies, current, priorManifest) {
|
|
2472
|
+
const canonicalProject = realpathSync(projectDir);
|
|
2473
|
+
const projectPath = (path) => resolve(canonicalProject, relative(projectDir, path));
|
|
2474
|
+
const folded = (path) => path.normalize("NFC").replaceAll("\\", "/").toLowerCase();
|
|
2475
|
+
const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
2476
|
+
const assetPaths = [...new Set([
|
|
2477
|
+
resolve(projectDir, outputPath),
|
|
2478
|
+
...stagedDependencies.map((item) => resolve(projectDir, outputDir, item.path))
|
|
2479
|
+
])].map((path) => folded(projectPath(path)));
|
|
2480
|
+
const generatedFiles = [
|
|
2481
|
+
resolve(projectDir, outputDir, `${name}.thumb.svg`),
|
|
2482
|
+
resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
2483
|
+
resolve(projectDir, typegen),
|
|
2484
|
+
resolve(projectDir, ".aura3d-scalar-admission.lock")
|
|
2485
|
+
];
|
|
2486
|
+
const generatedPaths = generatedFiles.map((path) => folded(projectPath(path)));
|
|
2487
|
+
const generatedSnapshots = new Map(generatedFiles.slice(0, 3).map((path) => [path, captureGlbPublicationTarget(projectDir, path)]));
|
|
2488
|
+
assertGlbPublicationTarget(projectDir, generatedFiles[1], priorManifest);
|
|
2489
|
+
const targets = [...assetPaths, ...generatedPaths];
|
|
2490
|
+
for (let left = 0; left < targets.length; left++)
|
|
2491
|
+
for (let right = left + 1; right < targets.length; right++) {
|
|
2492
|
+
if (overlaps(targets[left], targets[right])) {
|
|
2493
|
+
throw new Error("Aura3D assets add failed: GLB output, dependency, thumbnail, manifest, typegen or lock paths collide.");
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const bundleDir = folded(projectPath(resolve(projectDir, outputDir, namespace)));
|
|
2497
|
+
if (generatedPaths.some((path) => overlaps(path, bundleDir))) {
|
|
2498
|
+
throw new Error("Aura3D assets add failed: generated files cannot be placed inside a GLB bundle namespace.");
|
|
2499
|
+
}
|
|
2500
|
+
const sourcePaths = [sourcePath, ...dependencies.map((item) => resolve(dirname(sourcePath), item.path))]
|
|
2501
|
+
.map((path) => folded(realpathSync(path)));
|
|
2502
|
+
if (sourcePaths.some((source) => generatedPaths.some((target) => overlaps(source, target)))) {
|
|
2503
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite a GLB source or dependency.");
|
|
2504
|
+
}
|
|
2505
|
+
const previous = current.assets.find((asset) => asset.id === name);
|
|
2506
|
+
const previousThumbnail = previous?.thumbnailUrl && resolvePublicArtifactPath(projectDir, current, previous.thumbnailUrl);
|
|
2507
|
+
if (generatedSnapshots.get(generatedFiles[0]) &&
|
|
2508
|
+
(!previousThumbnail || folded(projectPath(previousThumbnail)) !== generatedPaths[0])) {
|
|
2509
|
+
throw new Error("Aura3D assets add failed: cannot overwrite an existing unowned thumbnail.");
|
|
2510
|
+
}
|
|
2511
|
+
if (generatedSnapshots.get(generatedFiles[2]) &&
|
|
2512
|
+
(!priorManifest || folded(projectPath(resolve(projectDir, current.typegen))) !== generatedPaths[2])) {
|
|
2513
|
+
throw new Error("Aura3D assets add failed: cannot overwrite an existing unowned typed asset module.");
|
|
2514
|
+
}
|
|
2515
|
+
for (const asset of current.assets) {
|
|
2516
|
+
const priorOutput = resolve(projectDir, asset.outputPath);
|
|
2517
|
+
const priorPath = folded(projectPath(priorOutput));
|
|
2518
|
+
if (generatedPaths.some((target) => overlaps(target, priorPath))) {
|
|
2519
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing asset.");
|
|
2520
|
+
}
|
|
2521
|
+
const priorDir = dirname(priorOutput);
|
|
2522
|
+
if (["glb-bundles", "scalar-volumes"].includes(basename(dirname(priorDir))) &&
|
|
2523
|
+
/^[a-f0-9]{64}$/.test(basename(priorDir))) {
|
|
2524
|
+
const priorBundle = folded(projectPath(priorDir));
|
|
2525
|
+
if (generatedPaths.some((target) => overlaps(target, priorBundle))) {
|
|
2526
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing asset namespace.");
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
else if (asset.format === "glb" &&
|
|
2530
|
+
generatedPaths.some((target) => overlaps(target, folded(projectPath(priorDir))))) {
|
|
2531
|
+
// A legacy GLB's recorded `dependencies` may be stale or absent. Read
|
|
2532
|
+
// its bounded JSON so generating a thumbnail or typed module cannot
|
|
2533
|
+
// overwrite (or fabricate) an actually referenced sidecar.
|
|
2534
|
+
let references;
|
|
2535
|
+
try {
|
|
2536
|
+
references = referencedGlbResourcePaths(projectDir, priorOutput);
|
|
2537
|
+
}
|
|
2538
|
+
catch (error) {
|
|
2539
|
+
throw new Error(`Aura3D assets add failed: cannot protect existing GLB dependencies for "${asset.id}".`, { cause: error });
|
|
2540
|
+
}
|
|
2541
|
+
if (references.some((path) => generatedPaths.some((target) => overlaps(target, folded(projectPath(path)))))) {
|
|
2542
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing GLB dependency.");
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (asset.id !== name && asset.thumbnailUrl) {
|
|
2546
|
+
const priorThumbnail = resolvePublicArtifactPath(projectDir, current, asset.thumbnailUrl);
|
|
2547
|
+
if (priorThumbnail && folded(projectPath(priorThumbnail)) === generatedPaths[0]) {
|
|
2548
|
+
throw new Error("Aura3D assets add failed: thumbnail collides with another asset.");
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
return generatedSnapshots;
|
|
2553
|
+
}
|
|
2554
|
+
/** Serialize scalar and external-GLB admissions before reading their shared manifest. */
|
|
2555
|
+
function withScalarVolumeAdmissionLock(projectDir, operation) {
|
|
2556
|
+
const path = resolve(projectDir, ".aura3d-scalar-admission.lock");
|
|
2557
|
+
let descriptor;
|
|
2558
|
+
try {
|
|
2559
|
+
descriptor = openSync(path, "wx", 0o600);
|
|
2560
|
+
}
|
|
2561
|
+
catch (error) {
|
|
2562
|
+
if (error.code === "EEXIST") {
|
|
2563
|
+
throw new Error("Aura3D assets add failed: another scalar-volume admission or external GLB admission is in progress or its lock remains.");
|
|
2564
|
+
}
|
|
2565
|
+
throw error;
|
|
2566
|
+
}
|
|
2567
|
+
const identity = fstatSync(descriptor);
|
|
2568
|
+
try {
|
|
2569
|
+
return operation();
|
|
2570
|
+
}
|
|
2571
|
+
finally {
|
|
2572
|
+
closeSync(descriptor);
|
|
2573
|
+
// Do not remove a lock file replaced by another owner.
|
|
2574
|
+
try {
|
|
2575
|
+
const current = lstatSync(path);
|
|
2576
|
+
if (current.dev === identity.dev && current.ino === identity.ino)
|
|
2577
|
+
rmSync(path);
|
|
2578
|
+
}
|
|
2579
|
+
catch (error) {
|
|
2580
|
+
if (error.code !== "ENOENT")
|
|
2581
|
+
throw error;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
/** The scalar loader maps the sample-center unit cube into node-local space. */
|
|
2586
|
+
function inspectScalarVolumeBounds(path, format, matrix) {
|
|
2587
|
+
const min = [Infinity, Infinity, Infinity];
|
|
2588
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
2589
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
2590
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2591
|
+
const value = matrix[12 + axis] + ((corner & 1) ? matrix[axis] : 0)
|
|
2592
|
+
+ ((corner & 2) ? matrix[4 + axis] : 0) + ((corner & 4) ? matrix[8 + axis] : 0);
|
|
2593
|
+
min[axis] = Math.min(min[axis], value);
|
|
2594
|
+
max[axis] = Math.max(max[axis], value);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const canonical = (value) => {
|
|
2598
|
+
const rounded = round(value);
|
|
2599
|
+
return rounded === 0 ? 0 : rounded;
|
|
2600
|
+
};
|
|
2601
|
+
const size = min.map((value, axis) => canonical(max[axis] - value));
|
|
2602
|
+
const boundsMetadata = {
|
|
2603
|
+
min: min.map(canonical),
|
|
2604
|
+
max: max.map(canonical),
|
|
2605
|
+
size,
|
|
2606
|
+
center: min.map((value, axis) => canonical((value + max[axis]) / 2)),
|
|
2607
|
+
maxDimension: Math.max(...size),
|
|
2608
|
+
grounded: Math.abs(canonical(min[1])) <= 0.08
|
|
2609
|
+
};
|
|
2610
|
+
return { ...inspectAssetFile(path, format), bounds: size, boundsMetadata };
|
|
2611
|
+
}
|
|
2612
|
+
function inspectNativeMeshFile(path, format) {
|
|
2613
|
+
const bytes = readBoundedNativeFile(path);
|
|
2614
|
+
const mesh = format === "ply" ? parsePLY(bytes) : parseSTL(bytes);
|
|
2615
|
+
if (format === "ply" && "elements" in mesh) {
|
|
2616
|
+
const properties = mesh.elements.vertex?.properties ?? {};
|
|
2617
|
+
const splatFields = ["scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3", "opacity", "f_dc_0", "f_dc_1", "f_dc_2"];
|
|
2618
|
+
if (splatFields.every((field) => Object.hasOwn(properties, field))) {
|
|
2619
|
+
throw new Error("Aura3D assets add failed: Gaussian splat PLY needs a splat pipeline; mesh admission would discard its splat attributes.");
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return prepareNativeMeshData(path, format, bytes, mesh);
|
|
2623
|
+
}
|
|
2624
|
+
function prepareNativeMeshData(path, format, bytes, mesh, warnings = []) {
|
|
2625
|
+
validateNativeMeshRenderBudget(mesh);
|
|
2626
|
+
// Bounds are serialized into JSON, where -0 is written as 0. Return the
|
|
2627
|
+
// same canonical value in memory so a fresh admission and a reloaded
|
|
2628
|
+
// manifest have identical public metadata.
|
|
2629
|
+
const roundBound = (value) => {
|
|
2630
|
+
const rounded = round(value);
|
|
2631
|
+
return rounded === 0 ? 0 : rounded;
|
|
2632
|
+
};
|
|
2633
|
+
const min = [Infinity, Infinity, Infinity];
|
|
2634
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
2635
|
+
for (let offset = 0; offset < mesh.positions.length; offset += 3) {
|
|
2636
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2637
|
+
min[axis] = Math.min(min[axis], mesh.positions[offset + axis]);
|
|
2638
|
+
max[axis] = Math.max(max[axis], mesh.positions[offset + axis]);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
const size = min.map((value, axis) => roundBound(max[axis] - value));
|
|
2642
|
+
const boundsMetadata = {
|
|
2643
|
+
min: min.map(roundBound),
|
|
2644
|
+
max: max.map(roundBound),
|
|
2645
|
+
size,
|
|
2646
|
+
center: min.map((value, axis) => roundBound((value + max[axis]) / 2)),
|
|
2647
|
+
maxDimension: Math.max(...size),
|
|
2648
|
+
grounded: Math.abs(roundBound(min[1])) <= 0.08
|
|
2649
|
+
};
|
|
2650
|
+
return {
|
|
2651
|
+
bytes,
|
|
2652
|
+
inspector: format,
|
|
2653
|
+
dependencies: [],
|
|
2654
|
+
inspection: {
|
|
2655
|
+
...inspectAssetFile(path, format),
|
|
2656
|
+
vertexCount: mesh.positions.length / 3,
|
|
2657
|
+
bounds: size,
|
|
2658
|
+
boundsMetadata,
|
|
2659
|
+
nativeWarnings: [...mesh.diagnostics, ...warnings]
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
/** Inspect OBJ and exactly the material resources consumed by its runtime loader. */
|
|
2664
|
+
function inspectNativeObjFile(path) {
|
|
2665
|
+
const bytes = readBoundedNativeFile(path);
|
|
2666
|
+
const decode = (value) => new TextDecoder("utf-8", { fatal: true }).decode(value);
|
|
2667
|
+
const loader = new OBJLoader();
|
|
2668
|
+
const parsed = loader.inspect(decode(bytes));
|
|
2669
|
+
if (parsed.materialLibraries.length > 256)
|
|
2670
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: more than 256 material references");
|
|
2671
|
+
const usedMaterials = new Set(parsed.materialNames);
|
|
2672
|
+
const rootUrl = "https://aura3d.invalid/project/";
|
|
2673
|
+
const documentUrl = `${rootUrl}model.obj`;
|
|
2674
|
+
const sourceDir = dirname(path);
|
|
2675
|
+
const dependencies = new Map();
|
|
2676
|
+
const paths = new Map();
|
|
2677
|
+
const materials = new Map();
|
|
2678
|
+
let totalBytes = 0;
|
|
2679
|
+
const readDependency = (url) => {
|
|
2680
|
+
const { uri, path: local } = nativeObjDependencyPath(url);
|
|
2681
|
+
const existing = dependencies.get(uri);
|
|
2682
|
+
if (existing)
|
|
2683
|
+
return existing;
|
|
2684
|
+
const folded = local.toLowerCase();
|
|
2685
|
+
if (paths.has(folded))
|
|
2686
|
+
throw new Error("OBJ_DEPENDENCY_PATH: distinct resource URLs collide on the local filesystem");
|
|
2687
|
+
if (dependencies.size >= 256)
|
|
2688
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: more than 256 dependencies");
|
|
2689
|
+
const file = resolve(sourceDir, local);
|
|
2690
|
+
assertNoNativeSymlinks(sourceDir, file);
|
|
2691
|
+
const remaining = NATIVE_IMPORT_MAX_BYTES - totalBytes;
|
|
2692
|
+
if (statSync(file).size > remaining)
|
|
2693
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: aggregate dependencies exceed 64 MiB");
|
|
2694
|
+
const input = readBoundedNativeFile(file, remaining);
|
|
2695
|
+
totalBytes += input.byteLength;
|
|
2696
|
+
if (totalBytes > NATIVE_IMPORT_MAX_BYTES)
|
|
2697
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: aggregate dependencies exceed 64 MiB");
|
|
2698
|
+
const result = { uri, path: local, bytes: input };
|
|
2699
|
+
dependencies.set(uri, result);
|
|
2700
|
+
paths.set(folded, uri);
|
|
2701
|
+
return result;
|
|
2702
|
+
};
|
|
2703
|
+
const dependencyUrl = (reference, base) => {
|
|
2704
|
+
if (!reference || /[\\\0]/.test(reference) || /%(?:2e|2f|5c)/i.test(reference))
|
|
2705
|
+
throw new Error("OBJ_DEPENDENCY_PATH: unsafe dependency reference");
|
|
2706
|
+
try {
|
|
2707
|
+
return new URL(reference, base);
|
|
2708
|
+
}
|
|
2709
|
+
catch {
|
|
2710
|
+
throw new Error("OBJ_DEPENDENCY_PATH: invalid dependency URL");
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const requireRelativeReference = (reference) => {
|
|
2714
|
+
if (reference.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(reference)) {
|
|
2715
|
+
throw new Error("OBJ_DEPENDENCY_PATH: resource references must be relative to their source file");
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
for (const library of parsed.materialLibraries) {
|
|
2719
|
+
requireRelativeReference(library);
|
|
2720
|
+
const url = dependencyUrl(library, documentUrl);
|
|
2721
|
+
const mtl = readDependency(url);
|
|
2722
|
+
for (const material of loader.inspectMaterialLibrary(decode(mtl.bytes), url.href)) {
|
|
2723
|
+
materials.set(material.name, material);
|
|
2724
|
+
if (materials.size > 65_536)
|
|
2725
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: more than 65536 material definitions");
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
let textureReferences = 0;
|
|
2729
|
+
for (const material of materials.values()) {
|
|
2730
|
+
if (!usedMaterials.has(material.name))
|
|
2731
|
+
continue;
|
|
2732
|
+
if (material.textures.length && !parsed.hasTexcoords)
|
|
2733
|
+
throw new Error(`OBJ material ${material.name} uses a texture but the mesh has no texture coordinates.`);
|
|
2734
|
+
for (let index = 0; index < material.textures.length; index++) {
|
|
2735
|
+
const texture = material.textures[index];
|
|
2736
|
+
requireRelativeReference(material.textureReferences[index]);
|
|
2737
|
+
if (++textureReferences > 4096)
|
|
2738
|
+
throw new Error("OBJ_DEPENDENCY_LIMIT: more than 4096 texture references");
|
|
2739
|
+
const item = readDependency(dependencyUrl(texture, rootUrl));
|
|
2740
|
+
const png = item.bytes.length >= 8 && [137, 80, 78, 71, 13, 10, 26, 10].every((value, index) => item.bytes[index] === value);
|
|
2741
|
+
const jpeg = item.bytes.length >= 3 && item.bytes[0] === 255 && item.bytes[1] === 216 && item.bytes[2] === 255;
|
|
2742
|
+
if (!png && !jpeg)
|
|
2743
|
+
throw new Error(`OBJ_TEXTURE_FORMAT: ${item.uri} is not a PNG or JPEG`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
const min = parsed.min.map(round);
|
|
2747
|
+
const max = parsed.max.map(round);
|
|
2748
|
+
const size = parsed.min.map((value, axis) => round(parsed.max[axis] - value));
|
|
2749
|
+
const boundsMetadata = {
|
|
2750
|
+
min, max, size,
|
|
2751
|
+
center: parsed.min.map((value, axis) => round((value + parsed.max[axis]) / 2)),
|
|
2752
|
+
maxDimension: Math.max(...size),
|
|
2753
|
+
grounded: Math.abs(min[1]) <= 0.08
|
|
2754
|
+
};
|
|
2755
|
+
return {
|
|
2756
|
+
bytes,
|
|
2757
|
+
inspector: "obj",
|
|
2758
|
+
dependencies: [...dependencies.values()],
|
|
2759
|
+
inspection: {
|
|
2760
|
+
...inspectAssetFile(path, "obj"),
|
|
2761
|
+
vertexCount: parsed.vertexCount,
|
|
2762
|
+
bounds: size,
|
|
2763
|
+
boundsMetadata,
|
|
2764
|
+
materials: parsed.materialNames,
|
|
2765
|
+
textures: [...materials.values()].filter((item) => usedMaterials.has(item.name)).flatMap((item) => item.textures.map((value) => new URL(value).pathname.slice("/project/".length))),
|
|
2766
|
+
dependencies: [...dependencies.values()].map((item) => item.uri),
|
|
2767
|
+
nativeWarnings: [...parsed.diagnostics, ...[...materials.values()].flatMap((item) => item.diagnostics)]
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
function nativeObjDependencyPath(url) {
|
|
2772
|
+
if (url.origin !== "https://aura3d.invalid" || !url.pathname.startsWith("/project/") || url.search || url.hash || url.username || url.password || /%(?:2e|2f|5c)/i.test(url.pathname)) {
|
|
2773
|
+
throw new Error("OBJ_DEPENDENCY_PATH: dependencies must be local and remain inside the source directory");
|
|
2774
|
+
}
|
|
2775
|
+
const uri = url.pathname.slice("/project/".length);
|
|
2776
|
+
let path;
|
|
2777
|
+
try {
|
|
2778
|
+
path = decodeURIComponent(uri);
|
|
2779
|
+
}
|
|
2780
|
+
catch {
|
|
2781
|
+
throw new Error("OBJ_DEPENDENCY_PATH: invalid URI encoding");
|
|
2782
|
+
}
|
|
2783
|
+
if (!path || /[\\\0]/.test(path) || path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
2784
|
+
throw new Error("OBJ_DEPENDENCY_PATH: invalid local path");
|
|
2785
|
+
return { uri, path };
|
|
2786
|
+
}
|
|
2787
|
+
function checkNativeAdmissionDestination(options, native) {
|
|
2788
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
2789
|
+
const current = readAssetManifest(projectDir);
|
|
2790
|
+
const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
|
|
2791
|
+
const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
|
|
2792
|
+
const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
|
|
2793
|
+
validateNativeAdmissionPaths(projectDir, options.name, outputDir, typegen, publicPath);
|
|
2794
|
+
const hash = createHash("sha256").update(native.bytes).digest("hex");
|
|
2795
|
+
const output = resolve(projectDir, outputDir, `${options.name}.${hash.slice(0, 8)}.obj`);
|
|
2796
|
+
const generatedTargets = [
|
|
2797
|
+
output,
|
|
2798
|
+
...native.dependencies.map((item) => resolve(projectDir, outputDir, item.path)),
|
|
2799
|
+
resolve(projectDir, outputDir, `${options.name}.thumb.svg`),
|
|
2800
|
+
resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
2801
|
+
resolve(projectDir, typegen)
|
|
2802
|
+
];
|
|
2803
|
+
if (new Set(generatedTargets.map((target) => target.toLowerCase())).size !== generatedTargets.length) {
|
|
2804
|
+
throw new Error("Aura3D assets add failed: native output paths collide.");
|
|
2805
|
+
}
|
|
2806
|
+
const sourcePath = resolve(projectDir, options.file);
|
|
2807
|
+
if (generatedTargets.some((target) => target === sourcePath && target !== output))
|
|
2808
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite the source asset.");
|
|
2809
|
+
for (const target of generatedTargets) {
|
|
2810
|
+
assertNoNativeSymlinks(projectDir, target);
|
|
2811
|
+
if (existsSync(target) && !statSync(target).isFile())
|
|
2812
|
+
throw new Error(`Aura3D assets add failed: output is not a regular file: ${target}`);
|
|
2813
|
+
let parent = dirname(target);
|
|
2814
|
+
while (!existsSync(parent))
|
|
2815
|
+
parent = dirname(parent);
|
|
2816
|
+
if (!statSync(parent).isDirectory())
|
|
2817
|
+
throw new Error(`Aura3D assets add failed: output parent is not a directory: ${parent}`);
|
|
2818
|
+
}
|
|
2819
|
+
if (options.copy === false) {
|
|
2820
|
+
const paths = [
|
|
2821
|
+
{ path: resolve(projectDir, outputDir, `${options.name}.${hash.slice(0, 8)}.obj`), bytes: native.bytes },
|
|
2822
|
+
...native.dependencies.map((item) => ({ path: resolve(projectDir, outputDir, item.path), bytes: item.bytes }))
|
|
2823
|
+
];
|
|
2824
|
+
for (const item of paths) {
|
|
2825
|
+
assertNoNativeSymlinks(projectDir, item.path);
|
|
2826
|
+
if (!existsSync(item.path) || !statSync(item.path).isFile() || statSync(item.path).size !== item.bytes.length || !readBoundedNativeFile(item.path).equals(item.bytes)) {
|
|
2827
|
+
throw new Error("Aura3D assets add failed: copy:false requires identical pre-staged native files.");
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
// The parser's allocation limits are wider than its render bridge's 32 MiB
|
|
2833
|
+
// buffer and 4096-group limits. Check the same bridge constraints at admission.
|
|
2834
|
+
function validateNativeMeshRenderBudget(mesh) {
|
|
2835
|
+
const vertexCount = mesh.positions.length / 3;
|
|
2836
|
+
if (!vertexCount || !mesh.indices.length)
|
|
2837
|
+
throw new Error("MESH_GEOMETRY: no renderable vertices or indices");
|
|
2838
|
+
const groups = (mesh.groups ?? [{ name: "mesh", start: 0, count: mesh.indices.length }]).filter((group) => group.count > 0);
|
|
2839
|
+
if (groups.length > 4096)
|
|
2840
|
+
throw new Error("MESH_ADAPTER_LIMIT: more than 4096 nonempty groups");
|
|
2841
|
+
const vertexStride = 4 * (3 + (mesh.normals ? 3 : 0) + (mesh.texcoords ? 2 : 0) + (mesh.colors ? 4 : 0));
|
|
2842
|
+
let bytes = 0;
|
|
2843
|
+
for (const group of groups) {
|
|
2844
|
+
// The bridge remaps multiple groups, duplicating only referenced vertices.
|
|
2845
|
+
if (bytes + group.count * 4 > NATIVE_RENDER_BUFFER_MAX_BYTES)
|
|
2846
|
+
throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
|
|
2847
|
+
let count = vertexCount;
|
|
2848
|
+
if (groups.length > 1) {
|
|
2849
|
+
const referenced = new Set();
|
|
2850
|
+
for (let index = group.start; index < group.start + group.count; index++) {
|
|
2851
|
+
referenced.add(mesh.indices[index]);
|
|
2852
|
+
if (bytes + group.count * 4 + referenced.size * vertexStride > NATIVE_RENDER_BUFFER_MAX_BYTES) {
|
|
2853
|
+
throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
count = referenced.size;
|
|
2857
|
+
}
|
|
2858
|
+
bytes += count * vertexStride + group.count * 4;
|
|
2859
|
+
if (bytes > NATIVE_RENDER_BUFFER_MAX_BYTES)
|
|
2860
|
+
throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
1876
2863
|
function inspectAssetFile(path, format) {
|
|
1877
2864
|
if (format === "gltf")
|
|
1878
2865
|
return inspectGltf(JSON.parse(readFileSync(path, "utf8")), dirname(path));
|
|
@@ -2562,9 +3549,9 @@ function createGlbComplexityWarnings(sizeBytes, vertexCount, format) {
|
|
|
2562
3549
|
}
|
|
2563
3550
|
return warnings;
|
|
2564
3551
|
}
|
|
2565
|
-
function createAssetWarnings(path, inspection) {
|
|
3552
|
+
function createAssetWarnings(path, inspection, inspectedSize) {
|
|
2566
3553
|
const warnings = [];
|
|
2567
|
-
const size = statSync(path).size;
|
|
3554
|
+
const size = inspectedSize ?? statSync(path).size;
|
|
2568
3555
|
const format = extname(path).slice(1).toLowerCase();
|
|
2569
3556
|
warnings.push(...createGlbComplexityWarnings(size, inspection.vertexCount, format));
|
|
2570
3557
|
if (size > 25 * 1024 * 1024)
|
|
@@ -2821,7 +3808,7 @@ function createDurableReleaseProvenanceWarnings(asset, provenance) {
|
|
|
2821
3808
|
* Reported as warnings rather than failures because release validation already promotes
|
|
2822
3809
|
* warnings to blocking failures for release candidates, while non-release callers keep working.
|
|
2823
3810
|
*/
|
|
2824
|
-
function createDerivedMetadataDriftWarnings(outputPath, asset) {
|
|
3811
|
+
function createDerivedMetadataDriftWarnings(outputPath, asset, glbJson) {
|
|
2825
3812
|
if (asset.type !== "model")
|
|
2826
3813
|
return [];
|
|
2827
3814
|
if (asset.format !== "glb" && asset.format !== "gltf")
|
|
@@ -2830,7 +3817,9 @@ function createDerivedMetadataDriftWarnings(outputPath, asset) {
|
|
|
2830
3817
|
return [];
|
|
2831
3818
|
let inspection;
|
|
2832
3819
|
try {
|
|
2833
|
-
|
|
3820
|
+
// The GLB was already inspected and hashed from a bounded JSON read above;
|
|
3821
|
+
// do not read the whole binary container again just to compare metadata.
|
|
3822
|
+
inspection = glbJson ? inspectGltf(glbJson) : inspectAssetFile(outputPath, asset.format);
|
|
2834
3823
|
}
|
|
2835
3824
|
catch {
|
|
2836
3825
|
// A file we cannot inspect is already reported by the hash and dependency checks.
|
|
@@ -3086,6 +4075,9 @@ function hasValidManifestOrientationOverride(asset) {
|
|
|
3086
4075
|
}
|
|
3087
4076
|
function releaseStoredAssetWarnings(projectDir, asset) {
|
|
3088
4077
|
let warnings = asset.warnings ?? [];
|
|
4078
|
+
// Older native admissions stored this informational notice as a warning.
|
|
4079
|
+
// Registry selection is an app runtime concern, so it is not a release gate.
|
|
4080
|
+
warnings = warnings.filter((warning) => !/^(?:OBJ|PLY|STL|3MF) requires explicit registration of a renderer-capable adapter in the app's FormatRegistry\.$/.test(warning));
|
|
3089
4081
|
if (hasValidManifestOrientationOverride(asset)) {
|
|
3090
4082
|
warnings = warnings.filter((warning) => !/orientation metadata missing; facing direction cannot be validated/i.test(warning));
|
|
3091
4083
|
}
|
|
@@ -3515,10 +4507,257 @@ function writeThumbnail(projectDir, outputDir, publicPath, id, bounds) {
|
|
|
3515
4507
|
const fileName = `${id}.thumb.svg`;
|
|
3516
4508
|
const outputPath = resolve(projectDir, outputDir, fileName);
|
|
3517
4509
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
3518
|
-
|
|
3519
|
-
writeFileSync(outputPath, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">${escapeXml(label)}</text></svg>`);
|
|
4510
|
+
writeFileSync(outputPath, thumbnailContents(id, bounds));
|
|
3520
4511
|
return `${publicPath}${fileName}`;
|
|
3521
4512
|
}
|
|
4513
|
+
function thumbnailContents(id, bounds) {
|
|
4514
|
+
const label = `${id}${bounds ? ` ${bounds.join("x")}` : ""}`;
|
|
4515
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">${escapeXml(label)}</text></svg>`;
|
|
4516
|
+
}
|
|
4517
|
+
function validateNativeAdmissionPaths(projectDir, name, outputDir, typegen, publicPath) {
|
|
4518
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name) || name === "__proto__") {
|
|
4519
|
+
throw new Error("Aura3D assets add failed: native asset name must contain only letters, digits, underscores, or hyphens.");
|
|
4520
|
+
}
|
|
4521
|
+
for (const [label, path] of [["output", outputDir], ["typegen", typegen]]) {
|
|
4522
|
+
if (!path || isAbsolute(path) || /^[A-Za-z]:/.test(path) || path.split("/").some((part) => !part || part === "." || part === "..") || /[\0?#%]/.test(path)) {
|
|
4523
|
+
throw new Error(`Aura3D assets add failed: ${label} must be a safe project-relative path.`);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
if ([DEFAULT_AURA_ASSET_MANIFEST, "package.json"].includes(typegen))
|
|
4527
|
+
throw new Error("Aura3D assets add failed: typegen cannot overwrite the manifest or package.json.");
|
|
4528
|
+
let decoded;
|
|
4529
|
+
try {
|
|
4530
|
+
decoded = decodeURIComponent(publicPath);
|
|
4531
|
+
}
|
|
4532
|
+
catch {
|
|
4533
|
+
decoded = "";
|
|
4534
|
+
}
|
|
4535
|
+
if (!publicPath.startsWith("/") || publicPath.startsWith("//") || /[\\?#\0]/.test(publicPath) || decoded !== publicPath ||
|
|
4536
|
+
(publicPath !== "/" && publicPath.slice(1, -1).split("/").some((part) => !part || part === "." || part === ".."))) {
|
|
4537
|
+
throw new Error("Aura3D assets add failed: native asset public path must be a same-origin absolute path without traversal.");
|
|
4538
|
+
}
|
|
4539
|
+
assertNoNativeSymlinks(projectDir, resolve(projectDir, outputDir));
|
|
4540
|
+
assertNoNativeSymlinks(projectDir, resolve(projectDir, typegen));
|
|
4541
|
+
assertNoNativeSymlinks(projectDir, resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST));
|
|
4542
|
+
}
|
|
4543
|
+
function assertNoNativeSymlinks(projectDir, path) {
|
|
4544
|
+
const relativePath = relative(projectDir, path);
|
|
4545
|
+
if (!relativePath || relativePath.startsWith(`..${sep}`) || relativePath === ".." || isAbsolute(relativePath)) {
|
|
4546
|
+
throw new Error("Aura3D assets add failed: native output path must remain inside the project.");
|
|
4547
|
+
}
|
|
4548
|
+
let cursor = projectDir;
|
|
4549
|
+
for (const segment of relativePath.split(sep)) {
|
|
4550
|
+
cursor = join(cursor, segment);
|
|
4551
|
+
try {
|
|
4552
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
4553
|
+
throw new Error("Aura3D assets add failed: native output paths may not contain symbolic links.");
|
|
4554
|
+
}
|
|
4555
|
+
catch (error) {
|
|
4556
|
+
if (error.code !== "ENOENT")
|
|
4557
|
+
throw error;
|
|
4558
|
+
break;
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
/** Stage every generated file, then roll back already-published paths if a later write fails. */
|
|
4563
|
+
function publishNativeAdmission(projectDir, manifest, outputPath, sourcePath, name, bounds, native, beforeCommit, glbTargets) {
|
|
4564
|
+
const immutable = native?.inspector === "nrrd" || native?.inspector === "glb";
|
|
4565
|
+
const suppliedAssets = native ? [
|
|
4566
|
+
{ target: resolve(projectDir, outputPath), bytes: native.bytes },
|
|
4567
|
+
...native.dependencies.map((item) => ({ target: resolve(projectDir, manifest.outputDir, item.path), bytes: item.bytes }))
|
|
4568
|
+
] : [];
|
|
4569
|
+
// Distinct URI spellings can resolve to the same local resource. Publish it
|
|
4570
|
+
// only once, while retaining every original URI in the integrity map.
|
|
4571
|
+
const assetsByTarget = new Map();
|
|
4572
|
+
for (const item of suppliedAssets) {
|
|
4573
|
+
const prior = assetsByTarget.get(item.target);
|
|
4574
|
+
if (prior && !prior.equals(item.bytes))
|
|
4575
|
+
throw new Error("Aura3D assets add failed: dependency paths resolve to different bytes.");
|
|
4576
|
+
assetsByTarget.set(item.target, item.bytes);
|
|
4577
|
+
}
|
|
4578
|
+
const assetFiles = [...assetsByTarget].map(([target, bytes]) => ({ target, bytes }));
|
|
4579
|
+
const assertImmutable = (target, bytes) => {
|
|
4580
|
+
if (native?.inspector === "glb")
|
|
4581
|
+
assertStagedGlbFile(projectDir, target, bytes, "existing GLB namespace contains different or unsafe bytes");
|
|
4582
|
+
else
|
|
4583
|
+
assertImmutableScalarFile(target, bytes);
|
|
4584
|
+
};
|
|
4585
|
+
const pendingAssets = assetFiles.filter((item) => {
|
|
4586
|
+
if (!immutable)
|
|
4587
|
+
return true;
|
|
4588
|
+
assertNoNativeSymlinks(projectDir, item.target);
|
|
4589
|
+
if (!existsSync(item.target))
|
|
4590
|
+
return true;
|
|
4591
|
+
assertImmutable(item.target, item.bytes);
|
|
4592
|
+
return false;
|
|
4593
|
+
});
|
|
4594
|
+
const targets = [
|
|
4595
|
+
...pendingAssets.map((item) => item.target),
|
|
4596
|
+
resolve(projectDir, manifest.outputDir, `${name}.thumb.svg`),
|
|
4597
|
+
resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
4598
|
+
resolve(projectDir, manifest.typegen)
|
|
4599
|
+
];
|
|
4600
|
+
if (new Set(targets).size !== targets.length)
|
|
4601
|
+
throw new Error("Aura3D assets add failed: output, thumbnail, manifest and typegen paths must be distinct.");
|
|
4602
|
+
if (targets.some((target) => target === sourcePath && target !== resolve(projectDir, outputPath))) {
|
|
4603
|
+
throw new Error("Aura3D assets add failed: generated files cannot overwrite the source asset.");
|
|
4604
|
+
}
|
|
4605
|
+
for (const target of targets) {
|
|
4606
|
+
assertNoNativeSymlinks(projectDir, target);
|
|
4607
|
+
if (existsSync(target) && !statSync(target).isFile())
|
|
4608
|
+
throw new Error(`Aura3D assets add failed: output is not a regular file: ${target}`);
|
|
4609
|
+
}
|
|
4610
|
+
const stage = mkdtempSync(join(projectDir, ".aura3d-admission-"));
|
|
4611
|
+
const createdDirs = [];
|
|
4612
|
+
const published = [];
|
|
4613
|
+
let retainStage = false;
|
|
4614
|
+
try {
|
|
4615
|
+
const packagePath = resolve(projectDir, "package.json");
|
|
4616
|
+
if (existsSync(packagePath))
|
|
4617
|
+
copyFileSync(packagePath, join(stage, "package.json"));
|
|
4618
|
+
writeAssetManifest(stage, manifest);
|
|
4619
|
+
const generated = writeTypedAssets(stage, { ...manifest, typegen: "native-generated.ts" });
|
|
4620
|
+
const thumb = join(stage, "thumbnail.svg");
|
|
4621
|
+
writeFileSync(thumb, thumbnailContents(name, bounds));
|
|
4622
|
+
const stagedFiles = [];
|
|
4623
|
+
for (let index = 0; index < pendingAssets.length; index++) {
|
|
4624
|
+
const file = join(stage, `asset-${index}`);
|
|
4625
|
+
writeFileSync(file, pendingAssets[index].bytes);
|
|
4626
|
+
stagedFiles.push(file);
|
|
4627
|
+
}
|
|
4628
|
+
stagedFiles.push(thumb, join(stage, DEFAULT_AURA_ASSET_MANIFEST), generated);
|
|
4629
|
+
for (const target of targets) {
|
|
4630
|
+
let parent = dirname(target);
|
|
4631
|
+
const missing = [];
|
|
4632
|
+
while (!existsSync(parent)) {
|
|
4633
|
+
missing.push(parent);
|
|
4634
|
+
parent = dirname(parent);
|
|
4635
|
+
}
|
|
4636
|
+
if (!statSync(parent).isDirectory())
|
|
4637
|
+
throw new Error(`Aura3D assets add failed: output parent is not a directory: ${parent}`);
|
|
4638
|
+
for (const dir of missing.reverse()) {
|
|
4639
|
+
if (glbTargets)
|
|
4640
|
+
assertNoNativeSymlinks(projectDir, dir);
|
|
4641
|
+
mkdirSync(dir);
|
|
4642
|
+
createdDirs.push(dir);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
for (let index = 0; index < targets.length; index++) {
|
|
4646
|
+
// All asset hard-links now exist, but no thumbnail, manifest, or typed
|
|
4647
|
+
// module has been changed. A failed recheck rolls those links back.
|
|
4648
|
+
if (index === pendingAssets.length)
|
|
4649
|
+
beforeCommit?.();
|
|
4650
|
+
const target = targets[index];
|
|
4651
|
+
assertNoNativeSymlinks(projectDir, target);
|
|
4652
|
+
if (immutable && index < pendingAssets.length) {
|
|
4653
|
+
try {
|
|
4654
|
+
// An exclusive hard link publishes complete bytes without replacing
|
|
4655
|
+
// another writer's existing content-addressed file.
|
|
4656
|
+
const identity = native?.inspector === "glb" ? lstatSync(stagedFiles[index]) : undefined;
|
|
4657
|
+
linkSync(stagedFiles[index], target);
|
|
4658
|
+
published.push({ target, ...(identity ? { identity: { dev: identity.dev, ino: identity.ino } } : {}) });
|
|
4659
|
+
}
|
|
4660
|
+
catch (error) {
|
|
4661
|
+
if (error.code !== "EEXIST")
|
|
4662
|
+
throw error;
|
|
4663
|
+
assertNoNativeSymlinks(projectDir, target);
|
|
4664
|
+
assertImmutable(target, pendingAssets[index].bytes);
|
|
4665
|
+
}
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
if (glbTargets) {
|
|
4669
|
+
if (!glbTargets.has(target))
|
|
4670
|
+
throw new Error("Aura3D assets add failed: unplanned GLB publication target.");
|
|
4671
|
+
const expected = glbTargets.get(target);
|
|
4672
|
+
assertGlbPublicationTarget(projectDir, target, expected);
|
|
4673
|
+
const staged = lstatSync(stagedFiles[index]);
|
|
4674
|
+
const record = { target };
|
|
4675
|
+
if (expected) {
|
|
4676
|
+
record.backup = join(stage, `backup-${index}`);
|
|
4677
|
+
renameSync(target, record.backup);
|
|
4678
|
+
published.push(record);
|
|
4679
|
+
const saved = lstatSync(record.backup);
|
|
4680
|
+
// Renaming can update ctime; the inode, size and mtime must still be
|
|
4681
|
+
// the same file that was approved before the transaction began.
|
|
4682
|
+
if (saved.dev !== expected.dev || saved.ino !== expected.ino ||
|
|
4683
|
+
saved.size !== expected.size || saved.mtimeMs !== expected.mtimeMs) {
|
|
4684
|
+
throw new Error(`Aura3D assets add failed: generated output changed during GLB publication: ${target}`);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
// Never replace a file created after the snapshot, even for generated
|
|
4688
|
+
// outputs. A failed exclusive link leaves that other writer's file intact.
|
|
4689
|
+
linkSync(stagedFiles[index], target);
|
|
4690
|
+
record.identity = { dev: staged.dev, ino: staged.ino };
|
|
4691
|
+
if (!expected)
|
|
4692
|
+
published.push(record);
|
|
4693
|
+
continue;
|
|
4694
|
+
}
|
|
4695
|
+
const backup = existsSync(target) ? join(stage, `backup-${index}`) : undefined;
|
|
4696
|
+
if (backup)
|
|
4697
|
+
renameSync(target, backup);
|
|
4698
|
+
published.push({ target, backup });
|
|
4699
|
+
renameSync(stagedFiles[index], target);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
catch (error) {
|
|
4703
|
+
const rollbackFailures = [];
|
|
4704
|
+
for (const { target, backup, identity } of published.reverse()) {
|
|
4705
|
+
try {
|
|
4706
|
+
if (glbTargets) {
|
|
4707
|
+
if (identity) {
|
|
4708
|
+
let current;
|
|
4709
|
+
try {
|
|
4710
|
+
current = lstatSync(target);
|
|
4711
|
+
}
|
|
4712
|
+
catch (error) {
|
|
4713
|
+
if (error.code !== "ENOENT")
|
|
4714
|
+
throw error;
|
|
4715
|
+
}
|
|
4716
|
+
if (current) {
|
|
4717
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino)
|
|
4718
|
+
throw new Error("published file was replaced");
|
|
4719
|
+
rmSync(target);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
// Linking the backup is exclusive: rollback never erases another
|
|
4723
|
+
// writer's replacement. Retain the stage if restoration is blocked.
|
|
4724
|
+
if (backup)
|
|
4725
|
+
linkSync(backup, target);
|
|
4726
|
+
}
|
|
4727
|
+
else {
|
|
4728
|
+
if (existsSync(target))
|
|
4729
|
+
rmSync(target);
|
|
4730
|
+
if (backup)
|
|
4731
|
+
renameSync(backup, target);
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
catch {
|
|
4735
|
+
rollbackFailures.push(target);
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
for (const dir of createdDirs.reverse()) {
|
|
4739
|
+
try {
|
|
4740
|
+
rmdirSync(dir);
|
|
4741
|
+
}
|
|
4742
|
+
catch { /* Leave unrelated concurrent content intact. */ }
|
|
4743
|
+
}
|
|
4744
|
+
if (rollbackFailures.length) {
|
|
4745
|
+
// A failed restore must never delete the original backups with the stage.
|
|
4746
|
+
retainStage = true;
|
|
4747
|
+
throw new Error(`Aura3D assets add rollback could not restore ${rollbackFailures.join(", ")}; original files remain in ${stage}.`, { cause: error });
|
|
4748
|
+
}
|
|
4749
|
+
throw error;
|
|
4750
|
+
}
|
|
4751
|
+
finally {
|
|
4752
|
+
if (!retainStage)
|
|
4753
|
+
rmSync(stage, { recursive: true, force: true });
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
function assertImmutableScalarFile(path, bytes) {
|
|
4757
|
+
if (!statSync(path).isFile() || statSync(path).size !== bytes.length || !readBoundedNativeFile(path).equals(bytes)) {
|
|
4758
|
+
throw new Error("Aura3D assets add failed: existing scalar-volume namespace contains different bytes.");
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
3522
4761
|
/** GU47 E-C: `--type lut` admits only a valid 3-D `.cube` (the S10b display-grade LUT). */
|
|
3523
4762
|
function admitCubeLut(sourcePath, format) {
|
|
3524
4763
|
if (format !== "cube") {
|
|
@@ -3532,19 +4771,21 @@ function admitCubeLut(sourcePath, format) {
|
|
|
3532
4771
|
}
|
|
3533
4772
|
}
|
|
3534
4773
|
function inferAssetType(format) {
|
|
3535
|
-
if (["glb", "gltf"].includes(format))
|
|
4774
|
+
if (["glb", "gltf", "obj", "ply", "stl", "3mf", "vox"].includes(format) || NATIVE_CATALOG_MODEL_FORMATS.has(format))
|
|
3536
4775
|
return "model";
|
|
3537
|
-
if (["png", "jpg", "jpeg", "webp", "ktx2"].includes(format))
|
|
4776
|
+
if (["png", "jpg", "jpeg", "webp", "ktx2", "pvr"].includes(format))
|
|
3538
4777
|
return "texture";
|
|
3539
4778
|
if (["hdr", "exr"].includes(format))
|
|
3540
4779
|
return "environment";
|
|
3541
4780
|
if (["mp3", "wav", "ogg"].includes(format))
|
|
3542
4781
|
return "audio";
|
|
4782
|
+
if (format === "nrrd" || format === "nhdr")
|
|
4783
|
+
return "scalar-volume";
|
|
3543
4784
|
if (format === "navmesh")
|
|
3544
4785
|
return "navigation";
|
|
3545
4786
|
if (format === "cube")
|
|
3546
4787
|
return "lut";
|
|
3547
|
-
throw new Error(`Unsupported Aura3D asset format: ${format || "unknown"}. Suggested fix: use glb, gltf, png, jpg, webp, ktx2, hdr, exr, mp3, wav, ogg, navmesh, or cube.`);
|
|
4788
|
+
throw new Error(`Unsupported Aura3D asset format: ${format || "unknown"}. Suggested fix: use glb, gltf, obj, ply, stl, 3mf, vox, png, jpg, webp, ktx2, pvr, hdr, exr, mp3, wav, ogg, nrrd, nhdr, navmesh, or cube.`);
|
|
3548
4789
|
}
|
|
3549
4790
|
function hashFile(path) {
|
|
3550
4791
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|