@jgengine/assets 0.19.0 → 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 +346 -0
- 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 +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-assets/SKILL.md +24 -3
- package/skills/jgengine-assets/api.md +90 -15
- package/skills/jgengine-assets/capabilities.md +41 -0
- package/skills/jgengine-assets/references/custom-imports.md +55 -0
|
@@ -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: {
|
package/dist/indexGen.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { readGlbFileMetrics } from "./assetBudget.js";
|
|
3
4
|
import { COLLISION_MESH_ASSET_IDS } from "./collisionMeshAssets.js";
|
|
4
5
|
import { readGlbClips, readGlbCollisionMesh, readGlbDims } from "./dims.js";
|
|
5
6
|
import { sourceById } from "./sources/index.js";
|
|
6
7
|
export function keyFromFile(file) {
|
|
7
8
|
return file.replace(/\.gltf\.glb$/i, "").replace(/\.glb$/i, "");
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
/** Build a source-owned model entry, preserving optional measured bounds, collision, clips, and inventory. */
|
|
11
|
+
export function entryForFile(source, file, dims, collisionMesh, clips, metrics) {
|
|
10
12
|
return {
|
|
11
13
|
id: `${source.id}/${keyFromFile(file)}`,
|
|
12
14
|
source: source.id,
|
|
13
15
|
categories: source.categories,
|
|
14
16
|
file,
|
|
17
|
+
...(metrics === undefined ? {} : { metrics }),
|
|
15
18
|
...(dims === undefined ? {} : { dims }),
|
|
16
19
|
...(collisionMesh === undefined ? {} : { collisionMesh }),
|
|
17
20
|
...(clips === undefined ? {} : { clips }),
|
|
@@ -54,7 +57,7 @@ function measureClips(bytes) {
|
|
|
54
57
|
return undefined;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
|
-
function entryFromGlb(source, file, full) {
|
|
60
|
+
function entryFromGlb(source, file, full, resourceRoot) {
|
|
58
61
|
let bytes;
|
|
59
62
|
try {
|
|
60
63
|
bytes = readFileSync(full);
|
|
@@ -64,7 +67,14 @@ function entryFromGlb(source, file, full) {
|
|
|
64
67
|
}
|
|
65
68
|
const id = `${source.id}/${keyFromFile(file)}`;
|
|
66
69
|
const collisionMesh = COLLISION_MESH_ASSET_IDS.has(id) ? measureCollisionMesh(bytes) : undefined;
|
|
67
|
-
|
|
70
|
+
let metrics;
|
|
71
|
+
try {
|
|
72
|
+
metrics = readGlbFileMetrics(full, resourceRoot);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
metrics = undefined;
|
|
76
|
+
}
|
|
77
|
+
return entryForFile(source, file, measureDims(bytes), collisionMesh, measureClips(bytes), metrics);
|
|
68
78
|
}
|
|
69
79
|
export function indexSourceDir(source, dir) {
|
|
70
80
|
const seen = new Set();
|
|
@@ -73,7 +83,7 @@ export function indexSourceDir(source, dir) {
|
|
|
73
83
|
if (seen.has(file))
|
|
74
84
|
continue;
|
|
75
85
|
seen.add(file);
|
|
76
|
-
entries.push(entryFromGlb(source, file, full));
|
|
86
|
+
entries.push(entryFromGlb(source, file, full, dir));
|
|
77
87
|
}
|
|
78
88
|
return entries;
|
|
79
89
|
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ModelDims } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { AssetSpace } from "@jgengine/core/scene/assetSpace";
|
|
3
3
|
import type { CollisionMeshData } from "@jgengine/core/scene/collisionMesh";
|
|
4
|
+
import type { GlbMetrics } from "./glbMetrics.js";
|
|
5
|
+
import type { MaterialMapRole } from "./materials.js";
|
|
4
6
|
export type { ModelDims };
|
|
5
7
|
export type { AssetSpace };
|
|
6
8
|
export type { CollisionMeshData };
|
|
@@ -30,6 +32,8 @@ export interface AssetSource {
|
|
|
30
32
|
categories: readonly string[];
|
|
31
33
|
download: AssetDownload;
|
|
32
34
|
homepage?: string;
|
|
35
|
+
/** Map roles present in the pinned material archive, used by the catalog and checked during pull. */
|
|
36
|
+
materialMaps?: readonly MaterialMapRole[];
|
|
33
37
|
/** Direct archive URL tried as a last resort when the primary provider path fails; see `downloadPackArchive`. */
|
|
34
38
|
mirror?: string;
|
|
35
39
|
/**
|
|
@@ -41,11 +45,14 @@ export interface AssetSource {
|
|
|
41
45
|
*/
|
|
42
46
|
unpulled?: string;
|
|
43
47
|
}
|
|
48
|
+
/** A source-owned model or sprite id, with optional offline model measurements. */
|
|
44
49
|
export interface IndexEntry {
|
|
45
50
|
id: string;
|
|
46
51
|
source: string;
|
|
47
52
|
categories: readonly string[];
|
|
48
53
|
file: string;
|
|
54
|
+
/** Offline byte, stored-triangle, and image inventory measured at reindex; absent if inspection fails. */
|
|
55
|
+
metrics?: GlbMetrics;
|
|
49
56
|
/** Footprint/center/minY measured from the GLB at reindex; absent when the model could not be read. */
|
|
50
57
|
dims?: ModelDims;
|
|
51
58
|
/** Opt-in compact triangle mesh extracted at reindex (see {@link "@jgengine/core/scene/collisionMesh".CollisionMeshData}); present only for ids in `COLLISION_MESH_ASSET_IDS` whose geometry extracted. */
|
package/dist/materials.d.ts
CHANGED
|
@@ -23,12 +23,12 @@ export interface ExtractedMaterialMap {
|
|
|
23
23
|
* URLs never depend on the provider's naming or the pulled resolution.
|
|
24
24
|
*/
|
|
25
25
|
export declare function extractMaterialMaps(archive: Uint8Array): ExtractedMaterialMap[];
|
|
26
|
-
/** URLs of one material's PBR maps; `ao`/`
|
|
26
|
+
/** URLs of one material's PBR maps; `ao`/`roughness`/`ktx2` appear only when declared by its source. */
|
|
27
27
|
export interface MaterialMaps {
|
|
28
28
|
color: string;
|
|
29
29
|
normal: string;
|
|
30
|
-
roughness
|
|
31
|
-
ao
|
|
30
|
+
roughness?: string;
|
|
31
|
+
ao?: string;
|
|
32
32
|
displacement: string;
|
|
33
33
|
ktx2?: string;
|
|
34
34
|
}
|