@jgengine/assets 0.18.1 → 0.21.0
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/CHANGELOG.md +504 -7
- package/README.md +22 -3
- package/dist/assetBudget.d.ts +36 -0
- package/dist/assetBudget.js +110 -0
- package/dist/catalogs/build.d.ts +1 -0
- package/dist/catalogs/build.js +1 -0
- package/dist/cli/pull.js +56 -1
- package/dist/glbMetrics.d.ts +24 -0
- package/dist/glbMetrics.js +291 -0
- package/dist/gltfMaterials.d.ts +79 -0
- package/dist/gltfMaterials.js +183 -0
- package/dist/importSpec.d.ts +15 -6
- package/dist/importSpec.js +217 -45
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/indexGen.d.ts +3 -1
- package/dist/indexGen.js +14 -4
- package/dist/manifest.d.ts +7 -0
- package/dist/materials.d.ts +3 -3
- package/dist/materials.js +3 -3
- package/dist/snippet.js +1 -1
- package/dist/sources/ambientcg.js +37 -31
- package/dist/staticPrefabBake.d.ts +58 -0
- package/dist/staticPrefabBake.js +315 -0
- package/dist/verify.js +2 -2
- package/package.json +5 -2
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-assets/SKILL.md +24 -3
- package/skills/jgengine-assets/api.md +119 -44
- package/skills/jgengine-assets/capabilities.md +41 -0
- package/skills/jgengine-assets/references/custom-imports.md +55 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { GLTF } from "@gltf-transform/core";
|
|
2
|
+
import type { MaterialTextureMetadata, MaterialTextureRole } from "@jgengine/core/material/materialAsset";
|
|
3
|
+
/** Native material/texture extensions implemented by Three.js GLTFLoader r182. Decoders are still required. */
|
|
4
|
+
export declare const GLTF_MATERIAL_EXTENSIONS_R182: readonly string[];
|
|
5
|
+
/** A native glTF texture use; embedded images retain their source index instead of an invented URL. */
|
|
6
|
+
export interface GltfMaterialTexture {
|
|
7
|
+
/** Bump is native normal perturbation, distinct from authored height displacement. */
|
|
8
|
+
role: MaterialTextureRole | "bump";
|
|
9
|
+
texture: number;
|
|
10
|
+
image: number;
|
|
11
|
+
sourceExtension?: string;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
bufferView?: number;
|
|
14
|
+
metadata: Omit<MaterialTextureMetadata, "url"> & {
|
|
15
|
+
url?: string;
|
|
16
|
+
};
|
|
17
|
+
/** Packed source channel meanings; anisotropy RG direction is remapped to [-1, 1]. */
|
|
18
|
+
channels: Readonly<Record<string, string>>;
|
|
19
|
+
/** Complete native textureInfo, including unrecognized extensions. */
|
|
20
|
+
native: GLTF.ITextureInfo;
|
|
21
|
+
}
|
|
22
|
+
/** One imported material with the native definition retained as an independent JSON snapshot. */
|
|
23
|
+
export interface GltfImportedMaterial {
|
|
24
|
+
/** Stable within the pinned model bytes; duplicate or absent material names are safe. */
|
|
25
|
+
id: string;
|
|
26
|
+
index: number;
|
|
27
|
+
name?: string;
|
|
28
|
+
alphaMode: "OPAQUE" | "MASK" | "BLEND";
|
|
29
|
+
alphaCutoff: number;
|
|
30
|
+
doubleSided: boolean;
|
|
31
|
+
extensions: readonly string[];
|
|
32
|
+
textures: readonly GltfMaterialTexture[];
|
|
33
|
+
native: GLTF.IMaterial;
|
|
34
|
+
}
|
|
35
|
+
/** A primitive slot retains mesh and material indices even when display names collide. */
|
|
36
|
+
export interface GltfMaterialSlot {
|
|
37
|
+
id: string;
|
|
38
|
+
mesh: number;
|
|
39
|
+
meshName?: string;
|
|
40
|
+
primitive: number;
|
|
41
|
+
material?: number;
|
|
42
|
+
materialId?: string;
|
|
43
|
+
materialName?: string;
|
|
44
|
+
nodes: readonly {
|
|
45
|
+
index: number;
|
|
46
|
+
name?: string;
|
|
47
|
+
}[];
|
|
48
|
+
uvSets: readonly number[];
|
|
49
|
+
hasNormals: boolean;
|
|
50
|
+
hasTangents: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Actionable fidelity warning or error; this inspector never changes imported surfaces. */
|
|
53
|
+
export interface GltfMaterialDiagnostic {
|
|
54
|
+
severity: "warning" | "error";
|
|
55
|
+
code: "unsupported-extension" | "missing-uv" | "missing-tangent-space" | "incompatible-extensions" | "missing-transmission" | "different-direction-uv";
|
|
56
|
+
path: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}
|
|
59
|
+
/** Native material inventory, primitive selectors, attribution, and renderer compatibility diagnostics. */
|
|
60
|
+
export interface GltfMaterialInspection {
|
|
61
|
+
assetId: string;
|
|
62
|
+
materials: readonly GltfImportedMaterial[];
|
|
63
|
+
slots: readonly GltfMaterialSlot[];
|
|
64
|
+
diagnostics: readonly GltfMaterialDiagnostic[];
|
|
65
|
+
copyright?: string;
|
|
66
|
+
extras?: unknown;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Inventory validated glTF JSON without flattening slots, decoding compressed geometry, or changing materials.
|
|
70
|
+
* IDs use the supplied model namespace plus source indices, and remain stable while its pinned bytes remain stable.
|
|
71
|
+
* @capability gltf-material-inspection inspect native material slots, packed maps, attribution and compatibility
|
|
72
|
+
*/
|
|
73
|
+
export declare function inspectGltfMaterials(json: GLTF.IGLTF, assetId: string): GltfMaterialInspection;
|
|
74
|
+
/**
|
|
75
|
+
* Inspect the JSON chunk of complete GLB v2 bytes, including models with compressed geometry.
|
|
76
|
+
* This measures material metadata only; it never decodes geometry or resolves image URLs.
|
|
77
|
+
* @capability gltf-material-inspection inspect material slots and texture metadata directly from complete GLB bytes
|
|
78
|
+
*/
|
|
79
|
+
export declare function inspectGlbMaterials(bytes: Uint8Array, assetId: string): GltfMaterialInspection;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** Native material/texture extensions implemented by Three.js GLTFLoader r182. Decoders are still required. */
|
|
2
|
+
export const GLTF_MATERIAL_EXTENSIONS_R182 = [
|
|
3
|
+
"KHR_materials_anisotropy", "KHR_materials_clearcoat", "KHR_materials_dispersion",
|
|
4
|
+
"KHR_materials_emissive_strength", "KHR_materials_ior", "KHR_materials_iridescence",
|
|
5
|
+
"KHR_materials_sheen", "KHR_materials_specular", "KHR_materials_transmission",
|
|
6
|
+
"KHR_materials_unlit", "KHR_materials_volume", "EXT_materials_bump",
|
|
7
|
+
"KHR_texture_transform", "KHR_texture_basisu", "EXT_texture_webp", "EXT_texture_avif",
|
|
8
|
+
];
|
|
9
|
+
const maps = {
|
|
10
|
+
pbrMetallicRoughness: [
|
|
11
|
+
["color", "baseColorTexture", "srgb", "rgba", { rgb: "base color", a: "linear alpha coverage" }],
|
|
12
|
+
["roughness", "metallicRoughnessTexture", "linear", "g", { g: "roughness", b: "metalness" }],
|
|
13
|
+
["metalness", "metallicRoughnessTexture", "linear", "b", { g: "roughness", b: "metalness" }],
|
|
14
|
+
],
|
|
15
|
+
material: [
|
|
16
|
+
["normal", "normalTexture", "linear", "rgb", { rgb: "OpenGL tangent normal" }],
|
|
17
|
+
["ao", "occlusionTexture", "linear", "r", { r: "ambient occlusion" }],
|
|
18
|
+
["emissive", "emissiveTexture", "srgb", "rgb", { rgb: "emissive color" }],
|
|
19
|
+
],
|
|
20
|
+
KHR_materials_sheen: [
|
|
21
|
+
["sheenColor", "sheenColorTexture", "srgb", "rgb", { rgb: "sheen color" }],
|
|
22
|
+
["sheenRoughness", "sheenRoughnessTexture", "linear", "a", { a: "sheen roughness" }],
|
|
23
|
+
],
|
|
24
|
+
KHR_materials_anisotropy: [["anisotropy", "anisotropyTexture", "linear", "rgb", { rg: "tangent direction [-1, 1]", b: "anisotropy strength" }]],
|
|
25
|
+
KHR_materials_clearcoat: [
|
|
26
|
+
["clearcoat", "clearcoatTexture", "linear", "r", { r: "clearcoat intensity" }],
|
|
27
|
+
["clearcoatRoughness", "clearcoatRoughnessTexture", "linear", "g", { g: "clearcoat roughness" }],
|
|
28
|
+
["clearcoatNormal", "clearcoatNormalTexture", "linear", "rgb", { rgb: "OpenGL tangent normal" }],
|
|
29
|
+
],
|
|
30
|
+
KHR_materials_specular: [
|
|
31
|
+
["specularIntensity", "specularTexture", "linear", "a", { a: "specular intensity" }],
|
|
32
|
+
["specularColor", "specularColorTexture", "srgb", "rgb", { rgb: "specular color" }],
|
|
33
|
+
],
|
|
34
|
+
KHR_materials_transmission: [["transmission", "transmissionTexture", "linear", "r", { r: "transmission" }]],
|
|
35
|
+
KHR_materials_volume: [["thickness", "thicknessTexture", "linear", "g", { g: "volume thickness" }]],
|
|
36
|
+
KHR_materials_iridescence: [
|
|
37
|
+
["iridescence", "iridescenceTexture", "linear", "r", { r: "iridescence intensity" }],
|
|
38
|
+
["iridescenceThickness", "iridescenceThicknessTexture", "linear", "g", { g: "iridescence thickness" }],
|
|
39
|
+
],
|
|
40
|
+
EXT_materials_bump: [["bump", "bumpTexture", "linear", "r", { r: "normal perturbation height" }]],
|
|
41
|
+
};
|
|
42
|
+
const wraps = { 10497: "repeat", 33071: "clamp", 33648: "mirror" };
|
|
43
|
+
const filters = { 9728: "nearest", 9729: "linear", 9984: "nearest-mipmap-nearest", 9985: "linear-mipmap-nearest", 9986: "nearest-mipmap-linear", 9987: "linear-mipmap-linear" };
|
|
44
|
+
function binding(json, info, spec, path) {
|
|
45
|
+
const [role, , colorSpace, channel, channels] = spec;
|
|
46
|
+
const texture = json.textures?.[info.index];
|
|
47
|
+
if (!Number.isSafeInteger(info.index) || texture === undefined)
|
|
48
|
+
throw new Error(`${path}: missing texture ${info.index}`);
|
|
49
|
+
const sourceExtension = ["KHR_texture_basisu", "EXT_texture_webp", "EXT_texture_avif"].find((name) => texture.extensions?.[name] !== undefined);
|
|
50
|
+
const extension = sourceExtension === undefined ? undefined : texture.extensions?.[sourceExtension];
|
|
51
|
+
const imageIndex = extension?.source ?? texture.source;
|
|
52
|
+
const image = imageIndex === undefined ? undefined : json.images?.[imageIndex];
|
|
53
|
+
if (image === undefined || imageIndex === undefined || !Number.isSafeInteger(imageIndex))
|
|
54
|
+
throw new Error(`${path}: missing image ${imageIndex}`);
|
|
55
|
+
const transform = info.extensions?.KHR_texture_transform;
|
|
56
|
+
const uvSet = transform?.texCoord ?? info.texCoord ?? 0;
|
|
57
|
+
if (!Number.isInteger(uvSet) || uvSet < 0 || uvSet > 3)
|
|
58
|
+
throw new Error(`${path}: Three.js r182 supports UV sets 0–3; found ${uvSet}`);
|
|
59
|
+
const sampler = texture.sampler === undefined ? undefined : json.samplers?.[texture.sampler];
|
|
60
|
+
if (texture.sampler !== undefined && sampler === undefined)
|
|
61
|
+
throw new Error(`${path}: missing sampler ${texture.sampler}`);
|
|
62
|
+
const compression = sourceExtension === "KHR_texture_basisu" ? "basisu" : sourceExtension === "EXT_texture_webp" ? "webp" : image.mimeType === "image/ktx2" ? "ktx2" : undefined;
|
|
63
|
+
return {
|
|
64
|
+
role, texture: info.index, image: imageIndex, ...(sourceExtension === undefined ? {} : { sourceExtension }),
|
|
65
|
+
...(image.mimeType === undefined ? {} : { mimeType: image.mimeType }),
|
|
66
|
+
...(image.bufferView === undefined ? {} : { bufferView: image.bufferView }),
|
|
67
|
+
metadata: {
|
|
68
|
+
...(image.uri === undefined ? {} : { url: image.uri }), colorSpace, channel, uvSet: uvSet,
|
|
69
|
+
...(role === "normal" || role === "clearcoatNormal" ? { normalConvention: "opengl" } : {}),
|
|
70
|
+
...(transform === undefined ? {} : { transform: {
|
|
71
|
+
...(transform.offset === undefined ? {} : { offset: [...transform.offset] }),
|
|
72
|
+
...(transform.scale === undefined ? {} : { scale: [...transform.scale] }),
|
|
73
|
+
...(transform.rotation === undefined ? {} : { rotation: transform.rotation }),
|
|
74
|
+
} }),
|
|
75
|
+
...(sampler === undefined ? {} : { sampler: {
|
|
76
|
+
wrapS: wraps[(sampler.wrapS ?? 10497)], wrapT: wraps[(sampler.wrapT ?? 10497)],
|
|
77
|
+
...(sampler.minFilter === undefined ? {} : { minFilter: filters[sampler.minFilter] }),
|
|
78
|
+
...(sampler.magFilter === undefined ? {} : { magFilter: filters[sampler.magFilter] }),
|
|
79
|
+
} }),
|
|
80
|
+
...(compression === undefined ? {} : { compression }),
|
|
81
|
+
}, channels: { ...channels }, native: structuredClone(info),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Inventory validated glTF JSON without flattening slots, decoding compressed geometry, or changing materials.
|
|
86
|
+
* IDs use the supplied model namespace plus source indices, and remain stable while its pinned bytes remain stable.
|
|
87
|
+
* @capability gltf-material-inspection inspect native material slots, packed maps, attribution and compatibility
|
|
88
|
+
*/
|
|
89
|
+
export function inspectGltfMaterials(json, assetId) {
|
|
90
|
+
if (json?.asset?.version !== "2.0" || assetId.trim() === "")
|
|
91
|
+
throw new Error("Material inspection requires glTF 2.0 and a nonempty asset id");
|
|
92
|
+
const diagnostics = [];
|
|
93
|
+
const diagnostic = (severity, code, path, message) => diagnostics.push({ severity, code, path, message });
|
|
94
|
+
const supported = new Set([...GLTF_MATERIAL_EXTENSIONS_R182, "KHR_draco_mesh_compression", "EXT_meshopt_compression", "KHR_mesh_quantization", "KHR_lights_punctual"]);
|
|
95
|
+
for (const name of new Set([...(json.extensionsUsed ?? []), ...(json.extensionsRequired ?? [])])) {
|
|
96
|
+
if (!supported.has(name))
|
|
97
|
+
diagnostic(json.extensionsRequired?.includes(name) ? "error" : "warning", "unsupported-extension", "extensionsUsed", `${name} has no native Three.js r182 loader implementation; register a compatible loader plugin or retain the source asset outside this renderer`);
|
|
98
|
+
}
|
|
99
|
+
const materials = (json.materials ?? []).map((material, index) => {
|
|
100
|
+
const textures = [];
|
|
101
|
+
const extensions = Object.keys(material.extensions ?? {});
|
|
102
|
+
for (const [name, specs] of Object.entries(maps)) {
|
|
103
|
+
const source = name === "material" ? material : name === "pbrMetallicRoughness" ? material.pbrMetallicRoughness : material.extensions?.[name];
|
|
104
|
+
for (const spec of specs) {
|
|
105
|
+
const info = source?.[spec[1]];
|
|
106
|
+
if (info !== undefined)
|
|
107
|
+
textures.push(binding(json, info, spec, `materials[${index}].${spec[1]}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (extensions.includes("KHR_materials_unlit") && extensions.some((name) => /^KHR_materials_/.test(name) && name !== "KHR_materials_unlit"))
|
|
111
|
+
diagnostic("error", "incompatible-extensions", `materials[${index}]`, "Unlit materials cannot retain a physical surface response; choose one shading model");
|
|
112
|
+
if (extensions.includes("KHR_materials_pbrSpecularGlossiness") && extensions.some((name) => /^KHR_materials_/.test(name) && name !== "KHR_materials_pbrSpecularGlossiness" && name !== "KHR_materials_emissive_strength"))
|
|
113
|
+
diagnostic("error", "incompatible-extensions", `materials[${index}]`, "Legacy specular/glossiness cannot be combined with metallic/roughness physical material extensions");
|
|
114
|
+
if (extensions.includes("KHR_materials_volume") && !extensions.includes("KHR_materials_transmission"))
|
|
115
|
+
diagnostic("warning", "missing-transmission", `materials[${index}]`, "Volume thickness and attenuation require transmission to contribute to transmitted light");
|
|
116
|
+
const normal = textures.find((texture) => texture.role === "normal");
|
|
117
|
+
const direction = textures.find((texture) => texture.role === "anisotropy");
|
|
118
|
+
if (normal !== undefined && direction !== undefined && normal.metadata.uvSet !== direction.metadata.uvSet)
|
|
119
|
+
diagnostic("warning", "different-direction-uv", `materials[${index}]`, "Normal and anisotropy textures should share their tangent-space UV set");
|
|
120
|
+
return {
|
|
121
|
+
id: `${assetId}/material/${index}`, index, ...(material.name === undefined ? {} : { name: material.name }),
|
|
122
|
+
alphaMode: material.alphaMode ?? "OPAQUE", alphaCutoff: material.alphaCutoff ?? 0.5,
|
|
123
|
+
doubleSided: material.doubleSided ?? false, extensions, textures, native: structuredClone(material),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const slots = (json.meshes ?? []).flatMap((mesh, meshIndex) => mesh.primitives.map((primitive, primitiveIndex) => {
|
|
127
|
+
const material = primitive.material === undefined ? undefined : materials[primitive.material];
|
|
128
|
+
if (primitive.material !== undefined && material === undefined)
|
|
129
|
+
throw new Error(`meshes[${meshIndex}].primitives[${primitiveIndex}]: missing material ${primitive.material}`);
|
|
130
|
+
const uvSets = Object.keys(primitive.attributes).flatMap((name) => /^TEXCOORD_\d+$/.test(name) ? [Number(name.slice(9))] : []).sort((a, b) => a - b);
|
|
131
|
+
const hasNormals = primitive.attributes.NORMAL !== undefined;
|
|
132
|
+
const hasTangents = primitive.attributes.TANGENT !== undefined;
|
|
133
|
+
const path = `meshes[${meshIndex}].primitives[${primitiveIndex}]`;
|
|
134
|
+
for (const texture of material?.textures ?? [])
|
|
135
|
+
if ((!material?.extensions.includes("KHR_materials_unlit") || texture.role === "color") && !uvSets.includes(texture.metadata.uvSet ?? 0))
|
|
136
|
+
diagnostic("error", "missing-uv", path, `${texture.role} uses missing TEXCOORD_${texture.metadata.uvSet ?? 0}`);
|
|
137
|
+
if (material?.extensions.includes("KHR_materials_anisotropy") && !(hasNormals && hasTangents) && !material.textures.some((texture) => texture.role === "normal"))
|
|
138
|
+
diagnostic("error", "missing-tangent-space", path, "Anisotropy requires NORMAL and TANGENT attributes or a normal texture with UVs from which tangent space can be derived");
|
|
139
|
+
return {
|
|
140
|
+
id: `${assetId}/mesh/${meshIndex}/primitive/${primitiveIndex}`, mesh: meshIndex,
|
|
141
|
+
...(mesh.name === undefined ? {} : { meshName: mesh.name }), primitive: primitiveIndex,
|
|
142
|
+
...(material === undefined ? {} : { material: material.index, materialId: material.id, ...(material.name === undefined ? {} : { materialName: material.name }) }),
|
|
143
|
+
nodes: (json.nodes ?? []).flatMap((node, index) => node.mesh !== meshIndex ? [] : [{ index, ...(node.name === undefined ? {} : { name: node.name }) }]),
|
|
144
|
+
uvSets, hasNormals, hasTangents,
|
|
145
|
+
};
|
|
146
|
+
}));
|
|
147
|
+
return {
|
|
148
|
+
assetId, materials, slots, diagnostics,
|
|
149
|
+
...(json.asset.copyright === undefined ? {} : { copyright: json.asset.copyright }),
|
|
150
|
+
...(json.asset.extras === undefined ? {} : { extras: structuredClone(json.asset.extras) }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Inspect the JSON chunk of complete GLB v2 bytes, including models with compressed geometry.
|
|
155
|
+
* This measures material metadata only; it never decodes geometry or resolves image URLs.
|
|
156
|
+
* @capability gltf-material-inspection inspect material slots and texture metadata directly from complete GLB bytes
|
|
157
|
+
*/
|
|
158
|
+
export function inspectGlbMaterials(bytes, assetId) {
|
|
159
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
160
|
+
if (bytes.length < 20 || view.getUint32(0, true) !== 0x46546c67 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.length)
|
|
161
|
+
throw new Error("Material inspection requires complete GLB v2 bytes");
|
|
162
|
+
let json;
|
|
163
|
+
for (let offset = 12; offset < bytes.length;) {
|
|
164
|
+
if (offset + 8 > bytes.length)
|
|
165
|
+
throw new Error("Truncated GLB material chunk header");
|
|
166
|
+
const size = view.getUint32(offset, true);
|
|
167
|
+
const type = view.getUint32(offset + 4, true);
|
|
168
|
+
const start = offset + 8;
|
|
169
|
+
if (size % 4 !== 0 || start + size > bytes.length)
|
|
170
|
+
throw new Error("Invalid GLB material chunk length");
|
|
171
|
+
if (offset === 12 && type !== 0x4e4f534a)
|
|
172
|
+
throw new Error("First GLB chunk must contain JSON");
|
|
173
|
+
if (type === 0x4e4f534a) {
|
|
174
|
+
if (json !== undefined)
|
|
175
|
+
throw new Error("Duplicate GLB JSON chunk");
|
|
176
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(start, start + size)));
|
|
177
|
+
}
|
|
178
|
+
offset = start + size;
|
|
179
|
+
}
|
|
180
|
+
if (json === undefined)
|
|
181
|
+
throw new Error("Missing GLB material JSON");
|
|
182
|
+
return inspectGltfMaterials(json, assetId);
|
|
183
|
+
}
|
package/dist/importSpec.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Supported logical kinds for user-supplied assets. */
|
|
2
2
|
export type AssetImportKind = "model" | "texture" | "material" | "sprite" | "spriteSheet" | "audio" | "font" | "hdri";
|
|
3
|
-
/** A
|
|
3
|
+
/** A game-owned import description; paths are relative POSIX paths, roles are authored labels. */
|
|
4
4
|
export interface AssetImportSpec {
|
|
5
5
|
id: string;
|
|
6
6
|
kind: AssetImportKind;
|
|
@@ -10,7 +10,7 @@ export interface AssetImportSpec {
|
|
|
10
10
|
}[];
|
|
11
11
|
meta?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
|
-
/** Result of validating an import description. */
|
|
13
|
+
/** Result of validating an import description; a successful entry is an independent JSON snapshot. */
|
|
14
14
|
export type ImportSpecValidation = {
|
|
15
15
|
ok: true;
|
|
16
16
|
entry: AssetImportSpec;
|
|
@@ -18,7 +18,16 @@ export type ImportSpecValidation = {
|
|
|
18
18
|
ok: false;
|
|
19
19
|
reason: string;
|
|
20
20
|
};
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Validate untrusted import descriptions without filesystem access or path rewriting.
|
|
23
|
+
* Requires unique relative files, printable non-empty roles and plain JSON metadata (maximum depth 64).
|
|
24
|
+
* @capability asset-import-validation validate game-owned multi-file asset descriptions before ingest
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateImportSpec(spec: unknown): ImportSpecValidation;
|
|
27
|
+
/**
|
|
28
|
+
* Classify supported container headers from complete file bytes; filenames never override bytes.
|
|
29
|
+
* Recognizes GLB/glTF, PNG/JPEG/KTX2, Ogg/WAV/MP3, TTF/OTF/WOFF/WOFF2, HDR/EXR and JSON sprite sheets.
|
|
30
|
+
* This is format detection, not decoder validation. Images classify as texture; authors choose sprite/material in the spec.
|
|
31
|
+
* @capability asset-byte-classification identify custom asset formats from bytes instead of filename extensions
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyAssetFile(bytes: Uint8Array, _filename: string): AssetImportKind | null;
|
package/dist/importSpec.js
CHANGED
|
@@ -1,58 +1,230 @@
|
|
|
1
1
|
const kinds = new Set(["model", "texture", "material", "sprite", "spriteSheet", "audio", "font", "hdri"]);
|
|
2
|
-
const
|
|
3
|
-
|
|
2
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3
|
+
function record(value) {
|
|
4
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
return false;
|
|
6
|
+
const prototype = Object.getPrototypeOf(value);
|
|
7
|
+
return (prototype === Object.prototype || prototype === null)
|
|
8
|
+
&& Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor && descriptor.enumerable);
|
|
9
|
+
}
|
|
10
|
+
function safePath(value) {
|
|
11
|
+
return value.length > 0 && value.trim() === value && !/[\\\u0000-\u001f\u007f:%?#]/.test(value)
|
|
12
|
+
&& value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
13
|
+
}
|
|
14
|
+
function jsonData(value, ancestors = new Set(), depth = 0) {
|
|
15
|
+
if (depth > 64)
|
|
16
|
+
return false;
|
|
17
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
18
|
+
return true;
|
|
19
|
+
if (typeof value === "number")
|
|
20
|
+
return Number.isFinite(value);
|
|
21
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
22
|
+
return false;
|
|
23
|
+
if (!Array.isArray(value) && !record(value))
|
|
24
|
+
return false;
|
|
25
|
+
if (Reflect.ownKeys(value).some((key) => typeof key === "symbol")
|
|
26
|
+
|| Object.values(Object.getOwnPropertyDescriptors(value)).some((descriptor) => !("value" in descriptor)))
|
|
27
|
+
return false;
|
|
28
|
+
ancestors.add(value);
|
|
29
|
+
const valid = Array.isArray(value)
|
|
30
|
+
? Object.keys(value).length === value.length && Array.from(value).every((item) => jsonData(item, ancestors, depth + 1))
|
|
31
|
+
: Object.values(value).every((item) => jsonData(item, ancestors, depth + 1));
|
|
32
|
+
ancestors.delete(value);
|
|
33
|
+
return valid;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate untrusted import descriptions without filesystem access or path rewriting.
|
|
37
|
+
* Requires unique relative files, printable non-empty roles and plain JSON metadata (maximum depth 64).
|
|
38
|
+
* @capability asset-import-validation validate game-owned multi-file asset descriptions before ingest
|
|
39
|
+
*/
|
|
4
40
|
export function validateImportSpec(spec) {
|
|
5
|
-
if (spec
|
|
41
|
+
if (!record(spec))
|
|
6
42
|
return { ok: false, reason: "spec must be an object" };
|
|
7
|
-
if (typeof spec.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(spec.id))
|
|
43
|
+
if (typeof spec.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(spec.id) || !safePath(spec.id)) {
|
|
8
44
|
return { ok: false, reason: "id must be a non-empty path-safe string" };
|
|
9
|
-
|
|
45
|
+
}
|
|
46
|
+
if (typeof spec.kind !== "string" || !kinds.has(spec.kind))
|
|
10
47
|
return { ok: false, reason: "kind is unsupported" };
|
|
11
48
|
if (!Array.isArray(spec.files) || spec.files.length === 0)
|
|
12
49
|
return { ok: false, reason: "files must contain at least one file" };
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)
|
|
23
|
-
return "texture";
|
|
24
|
-
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
|
|
25
|
-
return "texture";
|
|
26
|
-
if (bytes.length >= 12 && text(bytes.subarray(0, 4)) === "RIFF" && text(bytes.subarray(8, 12)) === "WAVE")
|
|
27
|
-
return "audio";
|
|
28
|
-
if (bytes.length >= 4 && text(bytes.subarray(0, 4)) === "OggS")
|
|
29
|
-
return "audio";
|
|
30
|
-
if (bytes.length >= 3 && bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33)
|
|
31
|
-
return "audio";
|
|
32
|
-
if (bytes.length >= 4 && ((bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00) || text(bytes.subarray(0, 4)) === "OTTO" || text(bytes.subarray(0, 4)) === "wOFF"))
|
|
33
|
-
return "font";
|
|
34
|
-
if (bytes.length >= 12 && text(bytes.subarray(0, 12)) === "\xabKTX 20\xbb\r\n\x1a\n")
|
|
35
|
-
return "texture";
|
|
36
|
-
if (bytes.length >= 4 && bytes[0] === 0x76 && bytes[1] === 0x2f && bytes[2] === 0x31 && bytes[3] === 0x01)
|
|
37
|
-
return "hdri";
|
|
38
|
-
if (lower.endsWith(".woff2"))
|
|
39
|
-
return "font";
|
|
40
|
-
if (lower.endsWith(".hdr"))
|
|
41
|
-
return "hdri";
|
|
42
|
-
if (lower.endsWith(".exr"))
|
|
43
|
-
return "hdri";
|
|
44
|
-
if (lower.endsWith(".json")) {
|
|
45
|
-
try {
|
|
46
|
-
if (typeof JSON.parse(text(bytes)).frames === "object")
|
|
47
|
-
return "spriteSheet";
|
|
50
|
+
const paths = new Set();
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const file of spec.files) {
|
|
53
|
+
if (!record(file) || typeof file.path !== "string" || !safePath(file.path))
|
|
54
|
+
return { ok: false, reason: "files must have safe non-empty relative paths" };
|
|
55
|
+
if (paths.has(file.path))
|
|
56
|
+
return { ok: false, reason: `duplicate file path: ${file.path}` };
|
|
57
|
+
if (file.role !== undefined && (typeof file.role !== "string" || file.role.trim() !== file.role || file.role.length === 0 || /[\u0000-\u001f\u007f]/.test(file.role))) {
|
|
58
|
+
return { ok: false, reason: `file role must be a non-empty printable string: ${file.path}` };
|
|
48
59
|
}
|
|
49
|
-
|
|
60
|
+
paths.add(file.path);
|
|
61
|
+
files.push({ path: file.path, ...(file.role === undefined ? {} : { role: file.role }) });
|
|
62
|
+
}
|
|
63
|
+
if (spec.meta !== undefined && (!record(spec.meta) || !jsonData(spec.meta)))
|
|
64
|
+
return { ok: false, reason: "meta must contain plain JSON data without cycles or non-finite numbers" };
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
entry: {
|
|
68
|
+
id: spec.id,
|
|
69
|
+
kind: spec.kind,
|
|
70
|
+
files,
|
|
71
|
+
...(spec.meta === undefined ? {} : { meta: JSON.parse(JSON.stringify(spec.meta)) }),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function match(bytes, signature, offset = 0) {
|
|
76
|
+
return bytes.length >= offset + signature.length && signature.every((byte, i) => bytes[offset + i] === byte);
|
|
77
|
+
}
|
|
78
|
+
function ascii(bytes, value, offset = 0) {
|
|
79
|
+
return bytes.length >= offset + value.length && [...value].every((char, i) => bytes[offset + i] === char.charCodeAt(0));
|
|
80
|
+
}
|
|
81
|
+
function json(bytes) {
|
|
82
|
+
let start = match(bytes, [0xef, 0xbb, 0xbf]) ? 3 : 0;
|
|
83
|
+
while ([0x20, 0x09, 0x0a, 0x0d].includes(bytes[start]))
|
|
84
|
+
start += 1;
|
|
85
|
+
if (bytes[start] !== 0x7b)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(decoder.decode(bytes));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function gltf(value) {
|
|
95
|
+
return record(value) && record(value.asset) && typeof value.asset.version === "string" && /^2\.\d+$/.test(value.asset.version);
|
|
96
|
+
}
|
|
97
|
+
function wav(bytes, view) {
|
|
98
|
+
if (bytes.length < 12 || !ascii(bytes, "WAVE", 8) || view.getUint32(4, true) !== bytes.length - 8)
|
|
99
|
+
return false;
|
|
100
|
+
let format = false;
|
|
101
|
+
let data = false;
|
|
102
|
+
let offset = 12;
|
|
103
|
+
while (offset + 8 <= bytes.length) {
|
|
104
|
+
const size = view.getUint32(offset + 4, true);
|
|
105
|
+
if (size > bytes.length - offset - 8)
|
|
106
|
+
return false;
|
|
107
|
+
if (ascii(bytes, "fmt ", offset))
|
|
108
|
+
format = size >= 16 && view.getUint16(offset + 8, true) > 0 && view.getUint16(offset + 10, true) > 0 && view.getUint32(offset + 12, true) > 0;
|
|
109
|
+
if (ascii(bytes, "data", offset))
|
|
110
|
+
data = size > 0;
|
|
111
|
+
offset += 8 + size + (size % 2);
|
|
112
|
+
}
|
|
113
|
+
return format && data && offset === bytes.length;
|
|
114
|
+
}
|
|
115
|
+
function exr(bytes, view) {
|
|
116
|
+
if (bytes.length < 9 || (view.getUint32(4, true) & 0xff) !== 2)
|
|
117
|
+
return false;
|
|
118
|
+
let offset = 8;
|
|
119
|
+
const attributes = new Set();
|
|
120
|
+
while (offset < bytes.length && bytes[offset] !== 0) {
|
|
121
|
+
const nameEnd = bytes.indexOf(0, offset);
|
|
122
|
+
if (nameEnd < 0)
|
|
123
|
+
return false;
|
|
124
|
+
const name = new TextDecoder().decode(bytes.subarray(offset, nameEnd));
|
|
125
|
+
const typeEnd = bytes.indexOf(0, nameEnd + 1);
|
|
126
|
+
if (typeEnd < 0 || bytes.length < typeEnd + 5)
|
|
127
|
+
return false;
|
|
128
|
+
const size = view.getUint32(typeEnd + 1, true);
|
|
129
|
+
offset = typeEnd + 5;
|
|
130
|
+
if (size > bytes.length - offset)
|
|
131
|
+
return false;
|
|
132
|
+
attributes.add(name);
|
|
133
|
+
offset += size;
|
|
134
|
+
}
|
|
135
|
+
return attributes.has("channels") && attributes.has("compression") && attributes.has("dataWindow") && bytes.length >= offset + 9;
|
|
136
|
+
}
|
|
137
|
+
function mp3Frame(bytes, start) {
|
|
138
|
+
if (bytes.length < start + 4)
|
|
139
|
+
return false;
|
|
140
|
+
const b1 = bytes[start + 1];
|
|
141
|
+
const b2 = bytes[start + 2];
|
|
142
|
+
if (bytes[start] !== 0xff || (b1 & 0xe0) !== 0xe0)
|
|
143
|
+
return false;
|
|
144
|
+
const version = (b1 >> 3) & 3;
|
|
145
|
+
const layer = (b1 >> 1) & 3;
|
|
146
|
+
const bitrateIndex = b2 >> 4;
|
|
147
|
+
const sampleIndex = (b2 >> 2) & 3;
|
|
148
|
+
if (version === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3 || (bytes[start + 3] & 3) === 2)
|
|
149
|
+
return false;
|
|
150
|
+
const rates = version === 3 ? [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] : [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160];
|
|
151
|
+
const frequency = [44100, 48000, 32000][sampleIndex] / (version === 3 ? 1 : version === 2 ? 2 : 4);
|
|
152
|
+
const length = Math.floor((version === 3 ? 144000 : 72000) * rates[bitrateIndex] / frequency) + ((b2 >> 1) & 1);
|
|
153
|
+
return length >= 4 && bytes.length - start >= length;
|
|
154
|
+
}
|
|
155
|
+
function mp3(bytes) {
|
|
156
|
+
if (!ascii(bytes, "ID3"))
|
|
157
|
+
return mp3Frame(bytes, 0);
|
|
158
|
+
if (bytes.length < 10 || bytes[3] < 2 || bytes[3] > 4 || bytes[4] === 0xff || bytes.subarray(6, 10).some((byte) => byte > 0x7f))
|
|
159
|
+
return false;
|
|
160
|
+
const version = bytes[3];
|
|
161
|
+
const flags = bytes[5];
|
|
162
|
+
if ((flags & (version === 2 ? 0x3f : version === 3 ? 0x1f : 0x0f)) !== 0)
|
|
163
|
+
return false;
|
|
164
|
+
const size = bytes[6] * 2097152 + bytes[7] * 16384 + bytes[8] * 128 + bytes[9];
|
|
165
|
+
return mp3Frame(bytes, 10 + size + (version === 4 && (flags & 0x10) !== 0 ? 10 : 0));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Classify supported container headers from complete file bytes; filenames never override bytes.
|
|
169
|
+
* Recognizes GLB/glTF, PNG/JPEG/KTX2, Ogg/WAV/MP3, TTF/OTF/WOFF/WOFF2, HDR/EXR and JSON sprite sheets.
|
|
170
|
+
* This is format detection, not decoder validation. Images classify as texture; authors choose sprite/material in the spec.
|
|
171
|
+
* @capability asset-byte-classification identify custom asset formats from bytes instead of filename extensions
|
|
172
|
+
*/
|
|
173
|
+
export function classifyAssetFile(bytes, _filename) {
|
|
174
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
175
|
+
if (ascii(bytes, "glTF")) {
|
|
176
|
+
if (bytes.length < 20 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.length || view.getUint32(16, true) !== 0x4e4f534a)
|
|
177
|
+
return null;
|
|
178
|
+
const length = view.getUint32(12, true);
|
|
179
|
+
return length > 0 && length % 4 === 0 && length <= bytes.length - 20 && gltf(json(bytes.subarray(20, 20 + length))) ? "model" : null;
|
|
50
180
|
}
|
|
51
|
-
if (
|
|
52
|
-
return "texture";
|
|
53
|
-
|
|
181
|
+
if (match(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
182
|
+
return bytes.length >= 33 && view.getUint32(8) === 13 && ascii(bytes, "IHDR", 12) && view.getUint32(16) > 0 && view.getUint32(20) > 0 ? "texture" : null;
|
|
183
|
+
}
|
|
184
|
+
if (match(bytes, [0xff, 0xd8, 0xff]))
|
|
185
|
+
return bytes.length >= 4 && bytes[3] !== 0 && bytes[3] !== 0xff ? "texture" : null;
|
|
186
|
+
if (match(bytes, [0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
187
|
+
return bytes.length >= 80 && view.getUint32(20, true) > 0 && view.getUint32(40, true) > 0 && bytes.length >= 80 + view.getUint32(40, true) * 24 ? "texture" : null;
|
|
188
|
+
}
|
|
189
|
+
if (ascii(bytes, "RIFF"))
|
|
190
|
+
return wav(bytes, view) ? "audio" : null;
|
|
191
|
+
if (ascii(bytes, "OggS")) {
|
|
192
|
+
if (bytes.length < 27 || bytes[4] !== 0 || (bytes[5] & 0xf8) !== 0 || bytes.length < 27 + bytes[26])
|
|
193
|
+
return null;
|
|
194
|
+
const payload = bytes.subarray(27, 27 + bytes[26]).reduce((sum, size) => sum + size, 0);
|
|
195
|
+
return bytes[26] > 0 && payload > 0 && bytes.length >= 27 + bytes[26] + payload ? "audio" : null;
|
|
196
|
+
}
|
|
197
|
+
if (mp3(bytes))
|
|
54
198
|
return "audio";
|
|
55
|
-
if (
|
|
199
|
+
if (match(bytes, [0, 1, 0, 0]) || ascii(bytes, "OTTO")) {
|
|
200
|
+
if (bytes.length < 12)
|
|
201
|
+
return null;
|
|
202
|
+
const tables = view.getUint16(4);
|
|
203
|
+
if (tables === 0 || bytes.length < 12 + tables * 16)
|
|
204
|
+
return null;
|
|
205
|
+
for (let i = 0; i < tables; i += 1) {
|
|
206
|
+
const offset = view.getUint32(12 + i * 16 + 8);
|
|
207
|
+
const length = view.getUint32(12 + i * 16 + 12);
|
|
208
|
+
if (offset > bytes.length || length > bytes.length - offset)
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
56
211
|
return "font";
|
|
212
|
+
}
|
|
213
|
+
if (ascii(bytes, "wOFF") || ascii(bytes, "wOF2")) {
|
|
214
|
+
const minimum = ascii(bytes, "wOF2") ? 48 : 44;
|
|
215
|
+
return bytes.length > minimum && view.getUint32(8) === bytes.length && view.getUint16(12) > 0 && view.getUint16(14) === 0 ? "font" : null;
|
|
216
|
+
}
|
|
217
|
+
if (match(bytes, [0x76, 0x2f, 0x31, 0x01]))
|
|
218
|
+
return exr(bytes, view) ? "hdri" : null;
|
|
219
|
+
if (ascii(bytes, "#?RADIANCE\n") || ascii(bytes, "#?RGBE\n") || ascii(bytes, "#?RADIANCE\r\n") || ascii(bytes, "#?RGBE\r\n")) {
|
|
220
|
+
const header = new TextDecoder().decode(bytes.subarray(0, Math.min(bytes.length, 4096)));
|
|
221
|
+
const resolution = /\r?\n\r?\n[+-]([XY]) ([1-9]\d*) [+-]([XY]) ([1-9]\d*)\r?\n/.exec(header);
|
|
222
|
+
return header.includes("FORMAT=32-bit_rle_rgbe") && resolution !== null && resolution[1] !== resolution[3] && bytes.length > resolution.index + resolution[0].length ? "hdri" : null;
|
|
223
|
+
}
|
|
224
|
+
const value = json(bytes);
|
|
225
|
+
if (gltf(value))
|
|
226
|
+
return "model";
|
|
227
|
+
if (record(value) && (record(value.frames) || Array.isArray(value.frames)) && Object.keys(value.frames).length > 0)
|
|
228
|
+
return "spriteSheet";
|
|
57
229
|
return null;
|
|
58
230
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,3 +18,4 @@ export { registryCatalog, componentInstallUrl, type RegistryCatalog, type Regist
|
|
|
18
18
|
export { modelWiringSnippet, materialWiringSnippet, componentWiringSnippet, iconWiringSnippet, spriteWiringSnippet, type ModelSnippetOptions, } from "./snippet.js";
|
|
19
19
|
export { verifyManifest, type VerifyResult } from "./verify.js";
|
|
20
20
|
export { resolveProvenance, validateAssetReferences, type AssetProvenance, type AssetProvenanceKind, type AssetReference, type ReferenceValidation, type ResolveProvenanceOptions, type ValidateAssetReferencesOptions, type ValidateAssetReferencesResult, } from "./provisioning.js";
|
|
21
|
+
export { readGlbMetrics, type GlbMetrics, type GlbTextureMetrics } from "./glbMetrics.js";
|
package/dist/index.js
CHANGED
|
@@ -18,3 +18,4 @@ export { registryCatalog, componentInstallUrl } from "./registry.js";
|
|
|
18
18
|
export { modelWiringSnippet, materialWiringSnippet, componentWiringSnippet, iconWiringSnippet, spriteWiringSnippet, } from "./snippet.js";
|
|
19
19
|
export { verifyManifest } from "./verify.js";
|
|
20
20
|
export { resolveProvenance, validateAssetReferences, } from "./provisioning.js";
|
|
21
|
+
export { readGlbMetrics } from "./glbMetrics.js";
|
package/dist/indexGen.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { GlbMetrics } from "./glbMetrics.js";
|
|
1
2
|
import type { AssetSource, CollisionMeshData, IndexEntry, ModelDims } from "./manifest.js";
|
|
2
3
|
export declare function keyFromFile(file: string): string;
|
|
3
|
-
|
|
4
|
+
/** Build a source-owned model entry, preserving optional measured bounds, collision, clips, and inventory. */
|
|
5
|
+
export declare function entryForFile(source: AssetSource, file: string, dims?: ModelDims, collisionMesh?: CollisionMeshData, clips?: readonly string[], metrics?: GlbMetrics): IndexEntry;
|
|
4
6
|
export declare function indexSourceDir(source: AssetSource, dir: string): IndexEntry[];
|
|
5
7
|
export interface ReindexResult {
|
|
6
8
|
perSource: {
|