@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
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
|
}
|
package/dist/materials.js
CHANGED
|
@@ -65,10 +65,10 @@ function refFor(source, basePath) {
|
|
|
65
65
|
maps: {
|
|
66
66
|
color: `${base}/${MATERIAL_MAP_FILES.color}`,
|
|
67
67
|
normal: `${base}/${MATERIAL_MAP_FILES.normal}`,
|
|
68
|
-
roughness: `${base}/${MATERIAL_MAP_FILES.roughness}
|
|
69
|
-
ao: `${base}/${MATERIAL_MAP_FILES.ao}
|
|
68
|
+
...(source.materialMaps?.includes("roughness") ? { roughness: `${base}/${MATERIAL_MAP_FILES.roughness}` } : {}),
|
|
69
|
+
...(source.materialMaps?.includes("ao") ? { ao: `${base}/${MATERIAL_MAP_FILES.ao}` } : {}),
|
|
70
70
|
displacement: `${base}/${MATERIAL_MAP_FILES.displacement}`,
|
|
71
|
-
ktx2: `${base}/${MATERIAL_MAP_FILES.ktx2}
|
|
71
|
+
...(source.materialMaps?.includes("ktx2") ? { ktx2: `${base}/${MATERIAL_MAP_FILES.ktx2}` } : {}),
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
74
|
}
|
package/dist/snippet.js
CHANGED
|
@@ -30,7 +30,7 @@ export function materialWiringSnippet(id, basePath = "/materials") {
|
|
|
30
30
|
`export const materials = buildMaterialCatalog({ basePath: ${JSON.stringify(basePath)} });`,
|
|
31
31
|
``,
|
|
32
32
|
`const material = materials.resolve(${JSON.stringify(id)})!;`,
|
|
33
|
-
`// material.maps => { color, normal,
|
|
33
|
+
`// material.maps => { color, normal, displacement, roughness?, ao?, ktx2? } URLs under ${basePath}/${id}/`,
|
|
34
34
|
``,
|
|
35
35
|
`// onto the ground (terrain() in defineGame({ world: environment({ terrain: ... }) })):`,
|
|
36
36
|
`terrain({ detail: { material: { maps: material.maps, repeat: 4 } } })`,
|
|
@@ -6,38 +6,39 @@
|
|
|
6
6
|
* catalog is generated per family instead of hand-typing hundreds of ids.
|
|
7
7
|
*/
|
|
8
8
|
const RESOLUTION = "1K-JPG";
|
|
9
|
+
// Optional-map availability for the pinned 1K-JPG variants: https://ambientcg.com/api/v2/full_json?type=Material&include=downloadData
|
|
9
10
|
const FAMILIES = [
|
|
10
|
-
{ family: "Grass", count: 5, categories: ["ground", "grass", "nature"] },
|
|
11
|
-
{ family: "Ground", count: 25, categories: ["ground", "dirt", "mud"] },
|
|
12
|
-
{ family: "Rock", count: 25, categories: ["rock", "cliff", "stone"] },
|
|
13
|
-
{ family: "Rocks", count: 8, categories: ["rock", "scatter"] },
|
|
14
|
-
{ family: "Gravel", count: 15, categories: ["ground", "gravel"] },
|
|
15
|
-
{ family: "Snow", count: 6, categories: ["ground", "snow", "winter"] },
|
|
16
|
-
{ family: "Ice", count: 3, categories: ["ice", "winter"] },
|
|
17
|
-
{ family: "Lava", count: 4, categories: ["lava", "volcanic"] },
|
|
18
|
-
{ family: "Moss", count: 2, categories: ["moss", "nature"] },
|
|
19
|
-
{ family: "Bark", count: 8, categories: ["bark", "wood", "nature"] },
|
|
20
|
-
{ family: "Wood", count: 30, categories: ["wood"] },
|
|
21
|
-
{ family: "WoodFloor", count: 25, categories: ["wood", "floor", "interior"] },
|
|
22
|
-
{ family: "Planks", count: 12, categories: ["wood", "planks"] },
|
|
23
|
-
{ family: "Bricks", count: 30, categories: ["brick", "wall"] },
|
|
24
|
-
{ family: "PavingStones", count: 25, categories: ["paving", "stone", "path"] },
|
|
25
|
-
{ family: "Tiles", count: 30, categories: ["tile", "floor", "interior"] },
|
|
26
|
-
{ family: "Concrete", count: 25, categories: ["concrete", "urban"] },
|
|
27
|
-
{ family: "Asphalt", count: 12, categories: ["asphalt", "road", "urban"] },
|
|
28
|
-
{ family: "Metal", count: 25, categories: ["metal"] },
|
|
29
|
-
{ family: "MetalPlates", count: 10, categories: ["metal", "plates", "scifi"] },
|
|
30
|
-
{ family: "Fabric", count: 25, categories: ["fabric", "cloth"] },
|
|
31
|
-
{ family: "Leather", count: 12, categories: ["leather"] },
|
|
32
|
-
{ family: "Marble", count: 12, categories: ["marble", "stone", "interior"] },
|
|
33
|
-
{ family: "Plaster", count: 7, categories: ["plaster", "wall", "interior"] },
|
|
34
|
-
{ family: "Facade", count: 6, categories: ["facade", "building"] },
|
|
35
|
-
{ family: "Terrazzo", count: 8, categories: ["floor", "interior"] },
|
|
36
|
-
{ family: "Wicker", count: 4, categories: ["wicker"] },
|
|
37
|
-
{ family: "Rope", count: 2, categories: ["rope"] },
|
|
38
|
-
{ family: "Cardboard", count: 3, categories: ["cardboard", "prop"] },
|
|
39
|
-
{ family: "Paper", count: 3, categories: ["paper", "prop"] },
|
|
40
|
-
{ family: "RoofingTiles", count: 8, categories: ["roof", "tile", "building"] },
|
|
11
|
+
{ family: "Grass", count: 5, categories: ["ground", "grass", "nature"], ao: [1, 2, 3, 4, 5] },
|
|
12
|
+
{ family: "Ground", count: 25, categories: ["ground", "dirt", "mud"], ao: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25] },
|
|
13
|
+
{ family: "Rock", count: 25, categories: ["rock", "cliff", "stone"], ao: [2, 3, 4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] },
|
|
14
|
+
{ family: "Rocks", count: 8, categories: ["rock", "scatter"], ao: [1, 2, 3, 4, 5, 6, 7, 8] },
|
|
15
|
+
{ family: "Gravel", count: 15, withoutRoughness: [1], categories: ["ground", "gravel"], ao: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] },
|
|
16
|
+
{ family: "Snow", count: 6, categories: ["ground", "snow", "winter"], ao: [6] },
|
|
17
|
+
{ family: "Ice", count: 3, categories: ["ice", "winter"], ao: [] },
|
|
18
|
+
{ family: "Lava", count: 4, categories: ["lava", "volcanic"], ao: [] },
|
|
19
|
+
{ family: "Moss", count: 2, categories: ["moss", "nature"], ao: [1, 2] },
|
|
20
|
+
{ family: "Bark", count: 8, categories: ["bark", "wood", "nature"], ao: [2, 3, 4, 5, 6, 7, 8] },
|
|
21
|
+
{ family: "Wood", count: 30, categories: ["wood"], ao: [11] },
|
|
22
|
+
{ family: "WoodFloor", count: 25, categories: ["wood", "floor", "interior"], ao: [3, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] },
|
|
23
|
+
{ family: "Planks", count: 12, categories: ["wood", "planks"], ao: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] },
|
|
24
|
+
{ family: "Bricks", count: 30, categories: ["brick", "wall"], ao: [4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 22, 23, 24, 25, 26, 28, 30] },
|
|
25
|
+
{ family: "PavingStones", count: 25, categories: ["paving", "stone", "path"], ao: [2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] },
|
|
26
|
+
{ family: "Tiles", count: 30, categories: ["tile", "floor", "interior"], ao: [7, 8, 9, 10, 11, 17, 18, 19, 26, 27, 28] },
|
|
27
|
+
{ family: "Concrete", count: 25, withoutRoughness: [1], categories: ["concrete", "urban"], ao: [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 25] },
|
|
28
|
+
{ family: "Asphalt", count: 12, categories: ["asphalt", "road", "urban"], ao: [3, 4, 5, 6, 7, 8] },
|
|
29
|
+
{ family: "Metal", count: 25, categories: ["metal"], ao: [] },
|
|
30
|
+
{ family: "MetalPlates", count: 10, categories: ["metal", "plates", "scifi"], ao: [10] },
|
|
31
|
+
{ family: "Fabric", count: 25, categories: ["fabric", "cloth"], ao: [7, 8, 9] },
|
|
32
|
+
{ family: "Leather", count: 12, categories: ["leather"], ao: [8] },
|
|
33
|
+
{ family: "Marble", count: 12, categories: ["marble", "stone", "interior"], ao: [] },
|
|
34
|
+
{ family: "Plaster", count: 7, categories: ["plaster", "wall", "interior"], ao: [7] },
|
|
35
|
+
{ family: "Facade", count: 6, categories: ["facade", "building"], ao: [] },
|
|
36
|
+
{ family: "Terrazzo", count: 8, categories: ["floor", "interior"], ao: [] },
|
|
37
|
+
{ family: "Wicker", count: 4, categories: ["wicker"], ao: [1, 2, 3, 4] },
|
|
38
|
+
{ family: "Rope", count: 2, categories: ["rope"], ao: [] },
|
|
39
|
+
{ family: "Cardboard", count: 3, categories: ["cardboard", "prop"], ao: [] },
|
|
40
|
+
{ family: "Paper", count: 3, categories: ["paper", "prop"], ao: [] },
|
|
41
|
+
{ family: "RoofingTiles", count: 8, categories: ["roof", "tile", "building"], ao: [6, 7, 8] },
|
|
41
42
|
];
|
|
42
43
|
/** Upstream asset id (`Grass001`) for a material source id (`ambientcg-grass001`). */
|
|
43
44
|
export function ambientcgAssetId(source) {
|
|
@@ -56,6 +57,11 @@ function materialSource(family, index) {
|
|
|
56
57
|
license: "CC0-1.0",
|
|
57
58
|
author: "ambientCG",
|
|
58
59
|
categories: family.categories,
|
|
60
|
+
materialMaps: [
|
|
61
|
+
"color", "normal", "displacement",
|
|
62
|
+
...(family.withoutRoughness?.includes(index) ? [] : ["roughness"]),
|
|
63
|
+
...(family.ao.includes(index) ? ["ao"] : []),
|
|
64
|
+
],
|
|
59
65
|
download: { url: `https://ambientcg.com/get?file=${assetId}_${RESOLUTION}.zip` },
|
|
60
66
|
homepage: `https://ambientcg.com/view?id=${assetId}`,
|
|
61
67
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { EditorPrefab } from "@jgengine/core/editor/types";
|
|
2
|
+
import { type StaticPrefabBox } from "@jgengine/core/editor/staticPrefab";
|
|
3
|
+
import type { ModelAssetRef, ModelDims } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import { type CollisionMeshData } from "@jgengine/core/scene/collisionMesh";
|
|
5
|
+
/** A verified external source image and its complete runtime URL. URI matches the source GLB exactly. */
|
|
6
|
+
export interface StaticPrefabTextureSource {
|
|
7
|
+
uri: string;
|
|
8
|
+
path: string;
|
|
9
|
+
sha256: string;
|
|
10
|
+
bytes: number;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
/** Original static model bytes; paths are absolute and every external image must have a pin. */
|
|
14
|
+
export interface StaticPrefabSource {
|
|
15
|
+
catalogId: string;
|
|
16
|
+
path: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
bytes: number;
|
|
19
|
+
textures?: readonly StaticPrefabTextureSource[];
|
|
20
|
+
}
|
|
21
|
+
/** Deterministic inventory and provenance for an editor-authored static artifact. */
|
|
22
|
+
export interface StaticPrefabBakeReport {
|
|
23
|
+
assetId: string;
|
|
24
|
+
prefabId: string;
|
|
25
|
+
sha256: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
/** GLB and unique external images; shared textures still contribute real encoded bytes. */
|
|
28
|
+
byteLength: number;
|
|
29
|
+
triangles: number;
|
|
30
|
+
submissions: number;
|
|
31
|
+
/** Optimization merges source mesh primitives; original mesh/primitive selectors are not portable. */
|
|
32
|
+
materialLayout: "merged-by-source-material";
|
|
33
|
+
bounds: StaticPrefabBox;
|
|
34
|
+
sourcePrefabSha256: string;
|
|
35
|
+
staticBakeSha256: string;
|
|
36
|
+
sources: readonly {
|
|
37
|
+
catalogId: string;
|
|
38
|
+
sha256: string;
|
|
39
|
+
bytes: number;
|
|
40
|
+
copyright?: string;
|
|
41
|
+
textures?: readonly Omit<StaticPrefabTextureSource, "path">[];
|
|
42
|
+
}[];
|
|
43
|
+
textures: readonly Omit<StaticPrefabTextureSource, "path">[];
|
|
44
|
+
}
|
|
45
|
+
/** GLB bytes and catalog-ready metadata; origin anchoring preserves the editable fragment's local frame. */
|
|
46
|
+
export interface StaticPrefabBakeResult {
|
|
47
|
+
bytes: Uint8Array;
|
|
48
|
+
dims: ModelDims;
|
|
49
|
+
space: NonNullable<ModelAssetRef["space"]>;
|
|
50
|
+
anchor: "origin";
|
|
51
|
+
collisionMesh: CollisionMeshData;
|
|
52
|
+
report: StaticPrefabBakeReport;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Bake pinned static source art in an editor prefab's local frame. Node-only; never mutates authored data.
|
|
56
|
+
* @capability static-prefab-export export editable prefab compositions with source materials and collision metadata
|
|
57
|
+
*/
|
|
58
|
+
export declare function bakeStaticPrefab(prefab: EditorPrefab, sources: readonly StaticPrefabSource[]): Promise<StaticPrefabBakeResult>;
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { Document, GLB_BUFFER, NodeIO, PropertyType, Root } from "@gltf-transform/core";
|
|
5
|
+
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
6
|
+
import { dedup, flatten, getBounds, join, mergeDocuments, unpartition, weld } from "@gltf-transform/functions";
|
|
7
|
+
import { parseStaticPrefabBake } from "@jgengine/core/editor/staticPrefab";
|
|
8
|
+
import { encodeCollisionMesh } from "@jgengine/core/scene/collisionMesh";
|
|
9
|
+
import { markerCatalogId } from "@jgengine/core/world/authoredObjects";
|
|
10
|
+
import { readGlbMetrics } from "./glbMetrics.js";
|
|
11
|
+
function sha256(bytes) {
|
|
12
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
async function readPin(pin) {
|
|
15
|
+
if (!isAbsolute(pin.path))
|
|
16
|
+
throw new Error(`Static prefab source path must be absolute: ${pin.path}`);
|
|
17
|
+
if (!Number.isSafeInteger(pin.bytes) || pin.bytes <= 0 || !/^[a-f0-9]{64}$/.test(pin.sha256))
|
|
18
|
+
throw new Error(`Invalid byte/hash pin for ${pin.path}`);
|
|
19
|
+
let bytes;
|
|
20
|
+
try {
|
|
21
|
+
bytes = await readFile(pin.path);
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
throw new Error(`Provision or restore pinned static prefab source ${pin.path}`, { cause });
|
|
25
|
+
}
|
|
26
|
+
if (bytes.length !== pin.bytes || sha256(bytes) !== pin.sha256)
|
|
27
|
+
throw new Error(`Provision or verify ${pin.path}: static prefab source does not match its byte/hash pin`);
|
|
28
|
+
return bytes;
|
|
29
|
+
}
|
|
30
|
+
function markerTransform(marker) {
|
|
31
|
+
if (marker.kind !== "prop")
|
|
32
|
+
throw new Error(`Static prefab marker ${marker.id} requires kind prop; interactive or custom kind ${marker.kind} is unsupported`);
|
|
33
|
+
const catalogId = markerCatalogId(marker);
|
|
34
|
+
if (catalogId === null)
|
|
35
|
+
throw new Error(`Static prefab marker ${marker.id} requires a pinned catalogId`);
|
|
36
|
+
const unsupported = ["scale", "scaleX", "scaleY", "scaleZ", "rotation", "rotationX", "rotationZ", "quaternion", "matrix", "offsetY", "yOffset", "targetHeight", "anchor", "animation", "interactive", "interaction", "behavior", "on", "action", "triggers", "triggerRadius", "generatorId", "params", "material", "materialId", "materials", "materialAssignments", "surface", "maps", "tint"];
|
|
37
|
+
for (const key of unsupported) {
|
|
38
|
+
if (marker.meta?.[key] !== undefined || marker[key] !== undefined)
|
|
39
|
+
throw new Error(`Static prefab marker ${marker.id}: unsupported ${key}; keep this part outside the static bake`);
|
|
40
|
+
}
|
|
41
|
+
const lift = marker.meta?.verticalOffset ?? 0;
|
|
42
|
+
const yaw = marker.rotationY ?? 0;
|
|
43
|
+
const translation = [marker.position.x, marker.position.y, marker.position.z];
|
|
44
|
+
if (!translation.every(Number.isFinite) || typeof lift !== "number" || !Number.isFinite(lift) || !Number.isFinite(yaw))
|
|
45
|
+
throw new Error(`Static prefab marker ${marker.id} requires finite position, verticalOffset, and rotationY`);
|
|
46
|
+
translation[1] += lift;
|
|
47
|
+
if (!Number.isFinite(translation[1]))
|
|
48
|
+
throw new Error(`Static prefab marker ${marker.id} has an overflowing verticalOffset`);
|
|
49
|
+
return { catalogId, translation, yaw };
|
|
50
|
+
}
|
|
51
|
+
function decodeGlb(bytes) {
|
|
52
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
53
|
+
if (bytes.length < 20 || view.getUint32(0, true) !== 0x46546c67 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.length)
|
|
54
|
+
throw new Error("Static prefab sources must be valid GLB v2 files");
|
|
55
|
+
const jsonLength = view.getUint32(12, true);
|
|
56
|
+
if (view.getUint32(16, true) !== 0x4e4f534a || jsonLength % 4 !== 0 || 20 + jsonLength > bytes.length)
|
|
57
|
+
throw new Error("Invalid static prefab GLB JSON chunk");
|
|
58
|
+
const json = JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + jsonLength)));
|
|
59
|
+
const offset = 20 + jsonLength;
|
|
60
|
+
const resources = {};
|
|
61
|
+
if (offset < bytes.length) {
|
|
62
|
+
if (offset + 8 > bytes.length || view.getUint32(offset + 4, true) !== 0x004e4942 || view.getUint32(offset, true) % 4 !== 0 || offset + 8 + view.getUint32(offset, true) !== bytes.length)
|
|
63
|
+
throw new Error("Invalid static prefab GLB BIN chunk");
|
|
64
|
+
resources[GLB_BUFFER] = bytes.subarray(offset + 8);
|
|
65
|
+
}
|
|
66
|
+
return { json, resources };
|
|
67
|
+
}
|
|
68
|
+
function validateStaticSource(json, path) {
|
|
69
|
+
if ((json.animations?.length ?? 0) > 0 || (json.skins?.length ?? 0) > 0 || json.nodes?.some((node) => node.skin !== undefined))
|
|
70
|
+
throw new Error(`${path}: animations and skins cannot be baked into a static prefab`);
|
|
71
|
+
if (json.nodes?.some((node) => node.weights !== undefined) || json.meshes?.some((mesh) => mesh.weights !== undefined || mesh.primitives.some((primitive) => (primitive.targets?.length ?? 0) > 0)))
|
|
72
|
+
throw new Error(`${path}: morph targets cannot be baked into a static prefab`);
|
|
73
|
+
if (json.scenes?.length !== 1 || (json.cameras?.length ?? 0) > 0)
|
|
74
|
+
throw new Error(`${path}: static prefab sources require exactly one scene and no cameras`);
|
|
75
|
+
const supported = new Set(ALL_EXTENSIONS.map((extension) => extension.EXTENSION_NAME));
|
|
76
|
+
for (const extension of new Set([...(json.extensionsUsed ?? []), ...(json.extensionsRequired ?? [])])) {
|
|
77
|
+
if (!supported.has(extension) || !/^(KHR_materials_|KHR_texture_transform$|KHR_texture_basisu$|EXT_texture_webp$|EXT_texture_avif$)/.test(extension))
|
|
78
|
+
throw new Error(`${path}: unsupported static prefab extension ${extension}`);
|
|
79
|
+
}
|
|
80
|
+
for (const node of json.nodes ?? []) {
|
|
81
|
+
for (const vector of [node.matrix, node.translation, node.rotation, node.scale]) {
|
|
82
|
+
if (vector !== undefined && !vector.every(Number.isFinite))
|
|
83
|
+
throw new Error(`${path}: non-finite source transform`);
|
|
84
|
+
}
|
|
85
|
+
if (node.scale?.some((value) => value === 0))
|
|
86
|
+
throw new Error(`${path}: zero-scale source transform`);
|
|
87
|
+
if (node.matrix !== undefined) {
|
|
88
|
+
const m = node.matrix;
|
|
89
|
+
const columns = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
|
|
90
|
+
if (m[3] !== 0 || m[7] !== 0 || m[11] !== 0 || m[15] !== 1 || columns.some((column) => Math.hypot(...column) === 0) || columns.some((a, i) => columns.some((b, j) => i < j && Math.abs(a.reduce((sum, value, k) => sum + value * b[k], 0)) > 1e-6 * Math.hypot(...a) * Math.hypot(...b))))
|
|
91
|
+
throw new Error(`${path}: sheared or non-affine source transform`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const mesh of json.meshes ?? []) {
|
|
95
|
+
for (const primitive of mesh.primitives) {
|
|
96
|
+
if ((primitive.mode ?? 4) !== 4 || primitive.attributes.POSITION === undefined || Object.keys(primitive.attributes).some((name) => /^(JOINTS|WEIGHTS)_/.test(name)))
|
|
97
|
+
throw new Error(`${path}: static prefabs require unskinned triangle primitives with POSITION`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function encodeGlb(output, document) {
|
|
102
|
+
const json = output.json;
|
|
103
|
+
const buffer = json.buffers?.[0];
|
|
104
|
+
if (json.buffers?.length !== 1 || buffer?.uri === undefined)
|
|
105
|
+
throw new Error("Static prefab output requires one binary buffer");
|
|
106
|
+
let binary = output.resources[buffer.uri];
|
|
107
|
+
if (binary === undefined)
|
|
108
|
+
throw new Error("Static prefab output buffer is missing");
|
|
109
|
+
const textures = document.getRoot().listTextures();
|
|
110
|
+
for (let i = 0; i < (json.images?.length ?? 0); i += 1) {
|
|
111
|
+
const image = json.images[i];
|
|
112
|
+
const runtimeUrl = textures[i].getURI();
|
|
113
|
+
if (runtimeUrl !== "") {
|
|
114
|
+
// GLTFTransform's GLTF writer strips image directories; the pinned runtime URI is authoritative.
|
|
115
|
+
image.uri = runtimeUrl;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const encoded = image.uri === undefined ? undefined : output.resources[image.uri];
|
|
119
|
+
if (encoded === undefined)
|
|
120
|
+
throw new Error("Static prefab embedded image is missing");
|
|
121
|
+
const offset = Math.ceil(binary.length / 4) * 4;
|
|
122
|
+
const combined = new Uint8Array(offset + encoded.length);
|
|
123
|
+
combined.set(binary);
|
|
124
|
+
combined.set(encoded, offset);
|
|
125
|
+
binary = combined;
|
|
126
|
+
image.bufferView = (json.bufferViews ??= []).length;
|
|
127
|
+
json.bufferViews.push({ buffer: 0, byteOffset: offset, byteLength: encoded.length });
|
|
128
|
+
delete image.uri;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
buffer.byteLength = binary.length;
|
|
132
|
+
delete buffer.uri;
|
|
133
|
+
const text = new TextEncoder().encode(JSON.stringify(json));
|
|
134
|
+
const jsonSize = Math.ceil(text.length / 4) * 4;
|
|
135
|
+
const binSize = Math.ceil(binary.length / 4) * 4;
|
|
136
|
+
const bytes = new Uint8Array(28 + jsonSize + binSize);
|
|
137
|
+
const view = new DataView(bytes.buffer);
|
|
138
|
+
for (const [offset, value] of [[0, 0x46546c67], [4, 2], [8, bytes.length], [12, jsonSize], [16, 0x4e4f534a], [20 + jsonSize, binSize], [24 + jsonSize, 0x004e4942]])
|
|
139
|
+
view.setUint32(offset, value, true);
|
|
140
|
+
bytes.fill(32, 20, 20 + jsonSize);
|
|
141
|
+
bytes.set(text, 20);
|
|
142
|
+
bytes.set(binary, 28 + jsonSize);
|
|
143
|
+
return bytes;
|
|
144
|
+
}
|
|
145
|
+
function dedupPinnedTextures(document) {
|
|
146
|
+
const owners = new Map();
|
|
147
|
+
for (const texture of document.getRoot().listTextures()) {
|
|
148
|
+
const image = texture.getImage();
|
|
149
|
+
if (image === null)
|
|
150
|
+
throw new Error("Static prefab texture has no verified image bytes");
|
|
151
|
+
// Generic dedup ignores URIs, which merges separately owned but byte-identical source images.
|
|
152
|
+
const key = JSON.stringify([texture.getURI(), texture.getMimeType(), sha256(image), texture.getName(), texture.getExtras()]);
|
|
153
|
+
const existing = owners.get(key);
|
|
154
|
+
if (existing === undefined) {
|
|
155
|
+
owners.set(key, texture);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
for (const parent of texture.listParents())
|
|
159
|
+
if (!(parent instanceof Root))
|
|
160
|
+
parent.swap(texture, existing);
|
|
161
|
+
texture.dispose();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function textureProvenance(pin) {
|
|
166
|
+
return { uri: pin.uri, url: pin.url, sha256: pin.sha256, bytes: pin.bytes };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Bake pinned static source art in an editor prefab's local frame. Node-only; never mutates authored data.
|
|
170
|
+
* @capability static-prefab-export export editable prefab compositions with source materials and collision metadata
|
|
171
|
+
*/
|
|
172
|
+
export async function bakeStaticPrefab(prefab, sources) {
|
|
173
|
+
return bakeStaticPrefabSnapshot(structuredClone(prefab), structuredClone(sources));
|
|
174
|
+
}
|
|
175
|
+
async function bakeStaticPrefabSnapshot(prefab, sources) {
|
|
176
|
+
if (prefab.staticBake === undefined)
|
|
177
|
+
throw new Error(`Prefab ${prefab.id} has no authored staticBake settings`);
|
|
178
|
+
const config = parseStaticPrefabBake(prefab.staticBake);
|
|
179
|
+
const fragment = prefab.fragment;
|
|
180
|
+
if (fragment.markers.length === 0 || fragment.volumes.length > 0 || fragment.paths.length > 0 || fragment.annotations.length > 0)
|
|
181
|
+
throw new Error(`Static prefab ${prefab.id} requires model markers only; retain gameplay regions, paths, and notes outside the bake`);
|
|
182
|
+
const ids = new Set();
|
|
183
|
+
const parts = fragment.markers.map((marker) => {
|
|
184
|
+
if (ids.has(marker.id))
|
|
185
|
+
throw new Error(`Static prefab ${prefab.id} has duplicate marker id ${marker.id}`);
|
|
186
|
+
ids.add(marker.id);
|
|
187
|
+
return markerTransform(marker);
|
|
188
|
+
});
|
|
189
|
+
const byId = new Map();
|
|
190
|
+
for (const source of sources) {
|
|
191
|
+
if (byId.has(source.catalogId))
|
|
192
|
+
throw new Error(`Duplicate static prefab source ${source.catalogId}`);
|
|
193
|
+
byId.set(source.catalogId, source);
|
|
194
|
+
}
|
|
195
|
+
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
196
|
+
const document = new Document();
|
|
197
|
+
const scene = document.createScene(prefab.name);
|
|
198
|
+
document.getRoot().setDefaultScene(scene);
|
|
199
|
+
const loaded = new Map();
|
|
200
|
+
const usedSources = [];
|
|
201
|
+
const usedTextures = new Map();
|
|
202
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
203
|
+
const part = parts[i];
|
|
204
|
+
let sourceDocument = loaded.get(part.catalogId);
|
|
205
|
+
if (sourceDocument === undefined) {
|
|
206
|
+
const source = byId.get(part.catalogId);
|
|
207
|
+
if (source === undefined)
|
|
208
|
+
throw new Error(`Static prefab marker ${fragment.markers[i].id}: no pinned source for ${part.catalogId}`);
|
|
209
|
+
const bytes = await readPin(source);
|
|
210
|
+
const input = decodeGlb(bytes);
|
|
211
|
+
validateStaticSource(input.json, source.path);
|
|
212
|
+
const textures = new Map();
|
|
213
|
+
const verifiedTextures = new Map();
|
|
214
|
+
for (const texture of source.textures ?? []) {
|
|
215
|
+
if (textures.has(texture.uri))
|
|
216
|
+
throw new Error(`${source.path}: duplicate texture pin URI ${texture.uri}`);
|
|
217
|
+
if (!texture.url.startsWith("/") && !/^https?:\/\//.test(texture.url))
|
|
218
|
+
throw new Error(`${source.path}: texture requires a full runtime URL: ${texture.url}`);
|
|
219
|
+
textures.set(texture.uri, texture);
|
|
220
|
+
}
|
|
221
|
+
for (const image of input.json.images ?? []) {
|
|
222
|
+
if (image.uri === undefined || image.uri.startsWith("data:"))
|
|
223
|
+
continue;
|
|
224
|
+
const pin = textures.get(image.uri);
|
|
225
|
+
if (pin === undefined)
|
|
226
|
+
throw new Error(`${source.path}: unpinned external texture ${image.uri}`);
|
|
227
|
+
const previous = usedTextures.get(pin.url);
|
|
228
|
+
if (previous !== undefined && (previous.pin.sha256 !== pin.sha256 || previous.pin.bytes !== pin.bytes))
|
|
229
|
+
throw new Error(`Conflicting static prefab texture pins for ${pin.url}`);
|
|
230
|
+
const data = await readPin(pin);
|
|
231
|
+
input.resources[image.uri] = data;
|
|
232
|
+
verifiedTextures.set(image.uri, pin);
|
|
233
|
+
usedTextures.set(pin.url, { pin: { ...pin }, data });
|
|
234
|
+
}
|
|
235
|
+
readGlbMetrics(bytes, (uri) => {
|
|
236
|
+
const resource = input.resources[uri];
|
|
237
|
+
if (resource === undefined)
|
|
238
|
+
throw new Error(`${source.path}: unpinned external resource ${uri}`);
|
|
239
|
+
return resource;
|
|
240
|
+
});
|
|
241
|
+
sourceDocument = await io.readJSON(input);
|
|
242
|
+
for (const texture of sourceDocument.getRoot().listTextures()) {
|
|
243
|
+
const pin = textures.get(texture.getURI());
|
|
244
|
+
if (pin !== undefined)
|
|
245
|
+
texture.setURI(pin.url);
|
|
246
|
+
}
|
|
247
|
+
loaded.set(part.catalogId, sourceDocument);
|
|
248
|
+
usedSources.push({ catalogId: source.catalogId, sha256: source.sha256, bytes: source.bytes, ...(input.json.asset.copyright === undefined ? {} : { copyright: input.json.asset.copyright }), ...(verifiedTextures.size === 0 ? {} : { textures: [...verifiedTextures.values()].map(textureProvenance) }) });
|
|
249
|
+
}
|
|
250
|
+
const mapping = mergeDocuments(document, sourceDocument);
|
|
251
|
+
const pivot = document.createNode(fragment.markers[i].id).setTranslation(part.translation).setRotation([0, Math.sin(part.yaw / 2), 0, Math.cos(part.yaw / 2)]);
|
|
252
|
+
for (const sourceScene of sourceDocument.getRoot().listScenes()) {
|
|
253
|
+
for (const node of sourceScene.listChildren())
|
|
254
|
+
pivot.addChild(mapping.get(node));
|
|
255
|
+
mapping.get(sourceScene).dispose();
|
|
256
|
+
}
|
|
257
|
+
scene.addChild(pivot);
|
|
258
|
+
}
|
|
259
|
+
dedupPinnedTextures(document);
|
|
260
|
+
await document.transform(dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.MESH, PropertyType.MATERIAL], keepUniqueNames: true }), flatten(), join({ keepNamed: false }), weld(), unpartition());
|
|
261
|
+
const positions = [];
|
|
262
|
+
const indices = [];
|
|
263
|
+
let submissions = 0;
|
|
264
|
+
scene.traverse((node) => {
|
|
265
|
+
const matrix = node.getWorldMatrix();
|
|
266
|
+
for (const primitive of node.getMesh()?.listPrimitives() ?? []) {
|
|
267
|
+
const position = primitive.getAttribute("POSITION");
|
|
268
|
+
const base = positions.length / 3;
|
|
269
|
+
const vertex = [0, 0, 0];
|
|
270
|
+
for (let i = 0; i < position.getCount(); i += 1) {
|
|
271
|
+
position.getElement(i, vertex);
|
|
272
|
+
for (let axis = 0; axis < 3; axis += 1)
|
|
273
|
+
positions.push(matrix[axis] * vertex[0] + matrix[axis + 4] * vertex[1] + matrix[axis + 8] * vertex[2] + matrix[axis + 12]);
|
|
274
|
+
}
|
|
275
|
+
const sourceIndices = primitive.getIndices();
|
|
276
|
+
for (let i = 0; i < (sourceIndices?.getCount() ?? position.getCount()); i += 1)
|
|
277
|
+
indices.push(base + (sourceIndices?.getScalar(i) ?? i));
|
|
278
|
+
submissions += 1;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
const collisionMesh = encodeCollisionMesh({ positions, indices });
|
|
282
|
+
if (collisionMesh === null)
|
|
283
|
+
throw new Error(`Static prefab ${prefab.id} has no nondegenerate collision triangles`);
|
|
284
|
+
const bounds = getBounds(scene);
|
|
285
|
+
if (![...bounds.min, ...bounds.max].every(Number.isFinite))
|
|
286
|
+
throw new Error(`Static prefab ${prefab.id} has invalid bounds`);
|
|
287
|
+
if (config.collisionBoxes === undefined && bounds.min.some((value, axis) => value >= bounds.max[axis]))
|
|
288
|
+
throw new Error(`Static prefab ${prefab.id} has degenerate bounds; author explicit collisionBoxes`);
|
|
289
|
+
collisionMesh.boxes = config.collisionBoxes ?? [{ min: [...bounds.min], max: [...bounds.max] }];
|
|
290
|
+
const dims = {
|
|
291
|
+
footprint: { w: bounds.max[0] - bounds.min[0], d: bounds.max[2] - bounds.min[2] },
|
|
292
|
+
center: { x: (bounds.max[0] + bounds.min[0]) / 2, z: (bounds.max[2] + bounds.min[2]) / 2 },
|
|
293
|
+
minY: bounds.min[1], maxY: bounds.max[1],
|
|
294
|
+
};
|
|
295
|
+
const output = await io.writeJSON(document);
|
|
296
|
+
const copyrights = [...new Set(usedSources.flatMap((source) => source.copyright === undefined ? [] : [source.copyright]))];
|
|
297
|
+
if (copyrights.length > 0)
|
|
298
|
+
output.json.asset.copyright = copyrights.join("\n");
|
|
299
|
+
const bytes = encodeGlb(output, document);
|
|
300
|
+
const metrics = readGlbMetrics(bytes, (url) => {
|
|
301
|
+
const texture = usedTextures.get(url);
|
|
302
|
+
if (texture === undefined)
|
|
303
|
+
throw new Error(`Static prefab output has an unverified texture ${url}`);
|
|
304
|
+
return texture.data;
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
bytes, dims, space: { anchor: { x: 0, z: 0 }, unitScale: 1 }, anchor: "origin", collisionMesh,
|
|
308
|
+
report: {
|
|
309
|
+
assetId: config.assetId, prefabId: prefab.id, sha256: sha256(bytes), bytes: bytes.length, byteLength: metrics.byteLength,
|
|
310
|
+
triangles: metrics.triangles, submissions, materialLayout: "merged-by-source-material", bounds: { min: [...bounds.min], max: [...bounds.max] },
|
|
311
|
+
sourcePrefabSha256: sha256(JSON.stringify(fragment)), staticBakeSha256: sha256(JSON.stringify(config)),
|
|
312
|
+
sources: usedSources, textures: [...usedTextures.values()].map(({ pin }) => textureProvenance(pin)),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
package/dist/verify.js
CHANGED
|
@@ -3,7 +3,7 @@ import { generatedIndex } from "./generated/index.js";
|
|
|
3
3
|
import { materialAliases } from "./materials.js";
|
|
4
4
|
import { singles } from "./singles.js";
|
|
5
5
|
import { sources } from "./sources/index.js";
|
|
6
|
-
/** Providers allowed repo-wide; Kenney.nl is permanently barred — see
|
|
6
|
+
/** Providers allowed repo-wide; Kenney.nl is permanently barred — see AGENTS.md "Never use Kenney assets". */
|
|
7
7
|
const ALLOWED_PROVIDERS = ["quaternius", "kaykit", "ambientcg", "gameicons"];
|
|
8
8
|
function containsKenney(value) {
|
|
9
9
|
return value.toLowerCase().includes("kenney");
|
|
@@ -16,7 +16,7 @@ export function verifyData(input) {
|
|
|
16
16
|
if (source.author.trim().length === 0)
|
|
17
17
|
errors.push(`source ${source.id}: missing author`);
|
|
18
18
|
if (!ALLOWED_PROVIDERS.includes(source.provider)) {
|
|
19
|
-
errors.push(`source ${source.id}: provider "${source.provider}" is not allowlisted (Never Kenney — see
|
|
19
|
+
errors.push(`source ${source.id}: provider "${source.provider}" is not allowlisted (Never Kenney — see AGENTS.md)`);
|
|
20
20
|
}
|
|
21
21
|
if (containsKenney(source.id))
|
|
22
22
|
errors.push(`source ${source.id}: id contains banned "kenney" substring`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Self-generating, license-verified index of thousands of CC0 3D models for JGengine. Sources fetch from providers' own CDNs at pull time; the npm tarball ships only the typed index and CLI (no GLB bytes).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
38
|
+
"@gltf-transform/core": "4.2.1",
|
|
39
|
+
"@gltf-transform/extensions": "4.2.1",
|
|
40
|
+
"@gltf-transform/functions": "4.2.1",
|
|
41
|
+
"@jgengine/core": "^0.21.0",
|
|
39
42
|
"fflate": "^0.8.2"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|