@jgengine/assets 0.11.0 → 0.12.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 +26 -0
- package/README.md +1 -1
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +19 -2
- package/dist/cli/pull.js +7 -11
- package/dist/collisionMeshAssets.d.ts +2 -0
- package/dist/collisionMeshAssets.js +6 -0
- package/dist/dims.d.ts +7 -0
- package/dist/dims.js +154 -0
- package/dist/download.d.ts +26 -10
- package/dist/download.js +140 -43
- package/dist/generated/kaykit-adventurers.json +64 -32
- package/dist/generated/kaykit-city-builder.json +82 -41
- package/dist/generated/kaykit-dungeon.json +940 -203
- package/dist/generated/kaykit-furniture.json +106 -53
- package/dist/generated/quaternius-stylized-nature.json +136 -68
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +27 -5
- package/dist/manifest.d.ts +4 -0
- package/dist/registry-catalog.json +0 -5
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +4 -0
- package/skills/jgengine-assets/api.md +13 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,32 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
13
|
the latest and surface the migration steps.
|
|
14
14
|
|
|
15
|
+
## 0.12.0
|
|
16
|
+
|
|
17
|
+
### Migrate
|
|
18
|
+
|
|
19
|
+
- **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
|
|
20
|
+
- **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
|
|
21
|
+
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
|
|
26
|
+
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
|
|
27
|
+
- **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
|
|
28
|
+
- **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
|
|
29
|
+
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
|
|
30
|
+
- **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
|
|
31
|
+
- **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
|
|
32
|
+
- **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
|
|
33
|
+
- **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
|
|
34
|
+
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
|
|
39
|
+
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
|
|
40
|
+
|
|
15
41
|
## 0.11.0
|
|
16
42
|
|
|
17
43
|
### Migrate
|
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> GǪ`. (`a
|
|
|
75
75
|
|
|
76
76
|
1. **Mirror base override** G�� `--mirror <baseUrl>` (or the `JGENGINE_ASSETS_MIRROR` env var if `--mirror` is not passed) G�� the archive is expected at `<baseUrl>/<provider>/<source-id>.zip`, e.g. `https://my-mirror.example.com/quaternius/quaternius-stylized-nature.zip`.
|
|
77
77
|
2. **The default GitHub-release mirror** G�� `https://github.com/Noisemaker111/jgengine/releases/download/packs/<provider>-<source-id>.zip`, on this repo's own rolling `packs` release (no separate assets repo). github.com is reachable from every cloud sandbox with no network-policy change, so zero-setup sessions still pull. `.github/workflows/mirror-assets.yml` (weekly cron + manual dispatch) keeps the release in sync with `src/sources/*.ts` automatically G�� adding a catalog entry is the whole publishing step, no manual upload. Skip this hop with `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`. Model/sprite pack failures fail the job; individual ambientCG material 404s are soft (logged, non-fatal).
|
|
78
|
-
3. **The primary provider path** G�� pinned `{ url, sha256? }` when stable: **KayKit** G�� `github.com/KayKit-Game-Assets/*/archive/GǪ/main.zip`; **Quaternius** free Standard packs G�� OpenGameArt direct zips (site pages JS-gate). Else scrape. `extractGlbs` packs co-located `.gltf`+`.bin` into `.glb
|
|
78
|
+
3. **The primary provider path** G�� pinned `{ url, sha256? }` when stable: **KayKit** G�� `github.com/KayKit-Game-Assets/*/archive/GǪ/main.zip`; **Quaternius** free Standard packs G�� OpenGameArt direct zips (site pages JS-gate). Else scrape. `extractGlbs` packs co-located `.gltf`+`.bin` into `.glb`, flattens external image URIs to basenames, and returns each referenced texture so `pull` writes it beside the models — a pulled pack renders textured out of the box.
|
|
79
79
|
4. **The pack's own `mirror`** G�� an optional direct archive URL set on the `AssetSource` entry itself (`src/sources/*.ts`), tried as a last resort.
|
|
80
80
|
|
|
81
81
|
If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which path supplied them** G�� a mirror serving stale or tampered bytes is rejected and the next source in the chain is tried instead.
|
package/dist/catalogs/build.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { type AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { IndexEntry } from "../manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* A durable, hand-or-tool authored catalog entry that is not part of the generated pack index,
|
|
5
|
+
* singles, or aliases — the shape the editor writes into a promoted game's `src/game/assets.ts`
|
|
6
|
+
* when an asset is imported. `label` is human-facing only and is intentionally not registered
|
|
7
|
+
* into the catalog (it mirrors how a single asset's author/license live only in the source
|
|
8
|
+
* literal), so at runtime an extra resolves to exactly `{ url }`.
|
|
9
|
+
*/
|
|
10
|
+
export interface CatalogExtra {
|
|
11
|
+
/** Catalog id the entry resolves under (matches what a folder rescan would produce). */
|
|
12
|
+
id: string;
|
|
13
|
+
/** URL the shipped game serves the model from (e.g. `/models/imported/Ship.glb`). */
|
|
14
|
+
url: string;
|
|
15
|
+
/** Human-facing label kept only in the source literal; never registered into the catalog. */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
3
18
|
export interface BuildCatalogOptions {
|
|
4
19
|
/** URL prefix where pulled pack GLBs live (consumer's `public/models`). */
|
|
5
20
|
basePath?: string;
|
|
@@ -7,6 +22,18 @@ export interface BuildCatalogOptions {
|
|
|
7
22
|
sources?: readonly string[];
|
|
8
23
|
includeAliases?: boolean;
|
|
9
24
|
includeSingles?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Extra catalog entries registered after packs/singles and before aliases: imported assets the
|
|
27
|
+
* editor persists into a promoted game's typed catalog. Registered last-writer-wins after packs
|
|
28
|
+
* (so an extra can override a pack id) but before aliases (so an alias can target an extra).
|
|
29
|
+
*/
|
|
30
|
+
extras?: readonly CatalogExtra[];
|
|
10
31
|
}
|
|
11
32
|
export declare function entryUrl(basePath: string, entry: IndexEntry): string;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases.
|
|
35
|
+
* Registration order is packs → singles → {@link BuildCatalogOptions.extras | extras} → aliases:
|
|
36
|
+
* packs and singles come first, extras override them last-writer-wins, and aliases resolve last so
|
|
37
|
+
* they can target an extra. See {@link BuildCatalogOptions} for filtering and opt-outs.
|
|
38
|
+
*/
|
|
12
39
|
export declare function buildCatalog(options?: BuildCatalogOptions): AssetCatalog;
|
package/dist/catalogs/build.js
CHANGED
|
@@ -6,6 +6,12 @@ import { sources as declaredSources } from "../sources/index.js";
|
|
|
6
6
|
export function entryUrl(basePath, entry) {
|
|
7
7
|
return `${basePath.replace(/\/+$/, "")}/${entry.source}/${entry.file}`;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases.
|
|
11
|
+
* Registration order is packs → singles → {@link BuildCatalogOptions.extras | extras} → aliases:
|
|
12
|
+
* packs and singles come first, extras override them last-writer-wins, and aliases resolve last so
|
|
13
|
+
* they can target an extra. See {@link BuildCatalogOptions} for filtering and opt-outs.
|
|
14
|
+
*/
|
|
9
15
|
export function buildCatalog(options = {}) {
|
|
10
16
|
const basePath = options.basePath ?? "/models";
|
|
11
17
|
const includeAliases = options.includeAliases ?? true;
|
|
@@ -24,19 +30,30 @@ export function buildCatalog(options = {}) {
|
|
|
24
30
|
if (sourceFilter !== null && !sourceFilter.has(entry.source))
|
|
25
31
|
continue;
|
|
26
32
|
const url = entryUrl(basePath, entry);
|
|
27
|
-
catalog.register(entry.id,
|
|
33
|
+
catalog.register(entry.id, {
|
|
34
|
+
url,
|
|
35
|
+
...(entry.dims === undefined ? {} : { dims: entry.dims }),
|
|
36
|
+
...(entry.collisionMesh === undefined ? {} : { collisionMesh: entry.collisionMesh }),
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
39
|
if (includeSingles) {
|
|
30
40
|
for (const single of singles) {
|
|
31
41
|
catalog.register(single.id, { url: single.url });
|
|
32
42
|
}
|
|
33
43
|
}
|
|
44
|
+
for (const extra of options.extras ?? []) {
|
|
45
|
+
catalog.register(extra.id, { url: extra.url });
|
|
46
|
+
}
|
|
34
47
|
if (includeAliases) {
|
|
35
48
|
for (const alias of aliases) {
|
|
36
49
|
const ref = catalog.resolve(alias.target);
|
|
37
50
|
if (ref === null)
|
|
38
51
|
continue;
|
|
39
|
-
catalog.register(alias.key,
|
|
52
|
+
catalog.register(alias.key, {
|
|
53
|
+
url: ref.url,
|
|
54
|
+
...(ref.dims === undefined ? {} : { dims: ref.dims }),
|
|
55
|
+
...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
|
|
56
|
+
});
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
return catalog;
|
package/dist/cli/pull.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { downloadPackArchive, extractGlbs, extractSpriteFiles
|
|
6
|
+
import { downloadPackArchive, extractGlbs, extractSpriteFiles } from "../download.js";
|
|
7
7
|
import { rankAssets } from "../find.js";
|
|
8
8
|
import { generatedIndex } from "../generated/index.js";
|
|
9
9
|
import { generatedSpriteIndex } from "../generated-sprites/index.js";
|
|
@@ -196,19 +196,15 @@ async function fetchPackInto(source, outRoot, options) {
|
|
|
196
196
|
writeFileSync(join(outDir, file.file), file.bytes);
|
|
197
197
|
return { outDir, models: files.length, textures: 0, url };
|
|
198
198
|
}
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
199
|
+
const { models, images } = extractGlbs(archive);
|
|
200
|
+
if (models.length === 0)
|
|
201
201
|
fail(`no .glb files found in ${source.id} archive`);
|
|
202
202
|
mkdirSync(outDir, { recursive: true });
|
|
203
|
-
for (const glb of
|
|
203
|
+
for (const glb of models)
|
|
204
204
|
writeFileSync(join(outDir, glb.file), glb.bytes);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
209
|
-
writeFileSync(dest, texture.bytes);
|
|
210
|
-
}
|
|
211
|
-
return { outDir, models: glbs.length, textures: textures.length, url };
|
|
205
|
+
for (const image of images)
|
|
206
|
+
writeFileSync(join(outDir, image.file), image.bytes);
|
|
207
|
+
return { outDir, models: models.length, textures: images.length, url };
|
|
212
208
|
}
|
|
213
209
|
function readSingles() {
|
|
214
210
|
if (!existsSync(singlesJson))
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Catalog assets that ship a collision mesh in the generated index. Opt-in only: each entry adds ~10-30KB of quantized triangles to the index and a BVH raycast per query, so list only concave models whose fitted box lies (archways, rings, frames). */
|
|
2
|
+
export declare const COLLISION_MESH_ASSET_IDS: ReadonlySet<string>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Catalog assets that ship a collision mesh in the generated index. Opt-in only: each entry adds ~10-30KB of quantized triangles to the index and a BVH raycast per query, so list only concave models whose fitted box lies (archways, rings, frames). */
|
|
2
|
+
export const COLLISION_MESH_ASSET_IDS = new Set([
|
|
3
|
+
"kaykit-dungeon/wall_arched",
|
|
4
|
+
"kaykit-dungeon/wall_archedwindow_open",
|
|
5
|
+
"kaykit-dungeon/wall_archedwindow_gated",
|
|
6
|
+
]);
|
package/dist/dims.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { ModelDims } from "@jgengine/core/scene/assetCatalog";
|
|
2
|
+
import { type CollisionMeshData } from "@jgengine/core/scene/collisionMesh";
|
|
2
3
|
export type { ModelDims };
|
|
3
4
|
export declare function readGlbDims(bytes: Uint8Array): ModelDims | null;
|
|
5
|
+
/**
|
|
6
|
+
* Extract a model-space collision triangle soup from a GLB and compress it with {@link encodeCollisionMesh}.
|
|
7
|
+
* Walks the default scene, transforming each TRIANGLES primitive's float32 VEC3 positions by the node's world
|
|
8
|
+
* matrix, and returns `null` for a JSON-only GLB or when no triangle survives quantization.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readGlbCollisionMesh(bytes: Uint8Array): CollisionMeshData | null;
|
package/dist/dims.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { encodeCollisionMesh } from "@jgengine/core/scene/collisionMesh";
|
|
1
2
|
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2
3
|
const GLB_MAGIC = 0x46546c67;
|
|
3
4
|
const CHUNK_JSON = 0x4e4f534a;
|
|
5
|
+
const CHUNK_BIN = 0x004e4942;
|
|
4
6
|
function multiply(a, b) {
|
|
5
7
|
const out = new Array(16);
|
|
6
8
|
for (let col = 0; col < 4; col += 1) {
|
|
@@ -139,5 +141,157 @@ export function readGlbDims(bytes) {
|
|
|
139
141
|
footprint: { w: bounds.max[0] - bounds.min[0], d: bounds.max[2] - bounds.min[2] },
|
|
140
142
|
center: { x: (bounds.min[0] + bounds.max[0]) / 2, z: (bounds.min[2] + bounds.max[2]) / 2 },
|
|
141
143
|
minY: bounds.min[1],
|
|
144
|
+
maxY: bounds.max[1],
|
|
142
145
|
};
|
|
143
146
|
}
|
|
147
|
+
const COMPONENT_FLOAT = 5126;
|
|
148
|
+
const COMPONENT_U8 = 5121;
|
|
149
|
+
const COMPONENT_U16 = 5123;
|
|
150
|
+
const COMPONENT_U32 = 5125;
|
|
151
|
+
const MODE_TRIANGLES = 4;
|
|
152
|
+
/** The GLB's binary chunk, or `null` for a JSON-only GLB (no geometry buffer to read). */
|
|
153
|
+
function readBinChunk(bytes) {
|
|
154
|
+
if (bytes.byteLength < 20)
|
|
155
|
+
return null;
|
|
156
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
157
|
+
if (view.getUint32(0, true) !== GLB_MAGIC)
|
|
158
|
+
return null;
|
|
159
|
+
const jsonChunkLength = view.getUint32(12, true);
|
|
160
|
+
if (view.getUint32(16, true) !== CHUNK_JSON)
|
|
161
|
+
return null;
|
|
162
|
+
const header = 20 + jsonChunkLength;
|
|
163
|
+
if (header + 8 > bytes.byteLength)
|
|
164
|
+
return null;
|
|
165
|
+
const chunkLength = view.getUint32(header, true);
|
|
166
|
+
if (view.getUint32(header + 4, true) !== CHUNK_BIN)
|
|
167
|
+
return null;
|
|
168
|
+
const start = header + 8;
|
|
169
|
+
const end = Math.min(start + chunkLength, bytes.byteLength);
|
|
170
|
+
return bytes.subarray(start, end);
|
|
171
|
+
}
|
|
172
|
+
/** Float32 VEC3 accessor values in model space, honoring bufferView/accessor byteOffset and byteStride; `null` for a malformed or out-of-bounds accessor. */
|
|
173
|
+
function readVec3Accessor(gltf, bin, accessorIndex) {
|
|
174
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
175
|
+
if (accessor === undefined)
|
|
176
|
+
return null;
|
|
177
|
+
if (accessor.componentType !== COMPONENT_FLOAT || accessor.type !== "VEC3")
|
|
178
|
+
return null;
|
|
179
|
+
const count = accessor.count;
|
|
180
|
+
if (count === undefined || count <= 0)
|
|
181
|
+
return null;
|
|
182
|
+
if (accessor.bufferView === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
const bufferView = gltf.bufferViews?.[accessor.bufferView];
|
|
185
|
+
if (bufferView === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
const stride = bufferView.byteStride ?? 12;
|
|
188
|
+
const base = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
189
|
+
if (base < 0 || base + (count - 1) * stride + 12 > bin.byteLength)
|
|
190
|
+
return null;
|
|
191
|
+
const view = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
192
|
+
const out = new Float64Array(count * 3);
|
|
193
|
+
for (let i = 0; i < count; i += 1) {
|
|
194
|
+
const offset = base + i * stride;
|
|
195
|
+
out[i * 3] = view.getFloat32(offset, true);
|
|
196
|
+
out[i * 3 + 1] = view.getFloat32(offset + 4, true);
|
|
197
|
+
out[i * 3 + 2] = view.getFloat32(offset + 8, true);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/** Scalar index accessor values (u8/u16/u32), honoring byteOffset and byteStride; `null` for a malformed or out-of-bounds accessor. */
|
|
202
|
+
function readIndexAccessor(gltf, bin, accessorIndex) {
|
|
203
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
204
|
+
if (accessor === undefined || accessor.type !== "SCALAR")
|
|
205
|
+
return null;
|
|
206
|
+
const componentType = accessor.componentType;
|
|
207
|
+
const componentBytes = componentType === COMPONENT_U8 ? 1 : componentType === COMPONENT_U16 ? 2 : componentType === COMPONENT_U32 ? 4 : 0;
|
|
208
|
+
if (componentBytes === 0)
|
|
209
|
+
return null;
|
|
210
|
+
const count = accessor.count;
|
|
211
|
+
if (count === undefined || count <= 0)
|
|
212
|
+
return null;
|
|
213
|
+
if (accessor.bufferView === undefined)
|
|
214
|
+
return null;
|
|
215
|
+
const bufferView = gltf.bufferViews?.[accessor.bufferView];
|
|
216
|
+
if (bufferView === undefined)
|
|
217
|
+
return null;
|
|
218
|
+
const stride = bufferView.byteStride ?? componentBytes;
|
|
219
|
+
const base = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
220
|
+
if (base < 0 || base + (count - 1) * stride + componentBytes > bin.byteLength)
|
|
221
|
+
return null;
|
|
222
|
+
const view = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
223
|
+
const out = new Uint32Array(count);
|
|
224
|
+
for (let i = 0; i < count; i += 1) {
|
|
225
|
+
const offset = base + i * stride;
|
|
226
|
+
out[i] =
|
|
227
|
+
componentType === COMPONENT_U8
|
|
228
|
+
? view.getUint8(offset)
|
|
229
|
+
: componentType === COMPONENT_U16
|
|
230
|
+
? view.getUint16(offset, true)
|
|
231
|
+
: view.getUint32(offset, true);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
/** Append one triangle primitive's world-space geometry to the soup; skips non-triangle modes and malformed accessors, appending nothing on failure. */
|
|
236
|
+
function addPrimitive(gltf, bin, primitive, world, soup) {
|
|
237
|
+
if ((primitive.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES)
|
|
238
|
+
return;
|
|
239
|
+
const accessorIndex = primitive.attributes?.POSITION;
|
|
240
|
+
if (accessorIndex === undefined)
|
|
241
|
+
return;
|
|
242
|
+
const positions = readVec3Accessor(gltf, bin, accessorIndex);
|
|
243
|
+
if (positions === null)
|
|
244
|
+
return;
|
|
245
|
+
let localIndices = null;
|
|
246
|
+
if (primitive.indices !== undefined) {
|
|
247
|
+
localIndices = readIndexAccessor(gltf, bin, primitive.indices);
|
|
248
|
+
if (localIndices === null)
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const vertexBase = soup.positions.length / 3;
|
|
252
|
+
const vertexCount = positions.length / 3;
|
|
253
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
254
|
+
const [wx, wy, wz] = transformPoint(world, positions[v * 3], positions[v * 3 + 1], positions[v * 3 + 2]);
|
|
255
|
+
soup.positions.push(wx, wy, wz);
|
|
256
|
+
}
|
|
257
|
+
if (localIndices === null) {
|
|
258
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
259
|
+
soup.indices.push(vertexBase + v);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
for (let i = 0; i < localIndices.length; i += 1)
|
|
263
|
+
soup.indices.push(vertexBase + localIndices[i]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function walkCollision(gltf, bin, nodeIndex, parent, soup) {
|
|
267
|
+
const node = gltf.nodes?.[nodeIndex];
|
|
268
|
+
if (node === undefined)
|
|
269
|
+
return;
|
|
270
|
+
const world = multiply(parent, fromTRS(node));
|
|
271
|
+
if (node.mesh !== undefined) {
|
|
272
|
+
const mesh = gltf.meshes?.[node.mesh];
|
|
273
|
+
for (const primitive of mesh?.primitives ?? [])
|
|
274
|
+
addPrimitive(gltf, bin, primitive, world, soup);
|
|
275
|
+
}
|
|
276
|
+
for (const child of node.children ?? [])
|
|
277
|
+
walkCollision(gltf, bin, child, world, soup);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Extract a model-space collision triangle soup from a GLB and compress it with {@link encodeCollisionMesh}.
|
|
281
|
+
* Walks the default scene, transforming each TRIANGLES primitive's float32 VEC3 positions by the node's world
|
|
282
|
+
* matrix, and returns `null` for a JSON-only GLB or when no triangle survives quantization.
|
|
283
|
+
*/
|
|
284
|
+
export function readGlbCollisionMesh(bytes) {
|
|
285
|
+
const gltf = readJsonChunk(bytes);
|
|
286
|
+
if (gltf === null)
|
|
287
|
+
return null;
|
|
288
|
+
const bin = readBinChunk(bytes);
|
|
289
|
+
if (bin === null)
|
|
290
|
+
return null;
|
|
291
|
+
const sceneIndex = gltf.scene ?? 0;
|
|
292
|
+
const roots = gltf.scenes?.[sceneIndex]?.nodes ?? gltf.nodes?.map((_, index) => index) ?? [];
|
|
293
|
+
const soup = { positions: [], indices: [] };
|
|
294
|
+
for (const root of roots)
|
|
295
|
+
walkCollision(gltf, bin, root, IDENTITY, soup);
|
|
296
|
+
return encodeCollisionMesh(soup);
|
|
297
|
+
}
|
package/dist/download.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export interface ExtractedGlb {
|
|
|
15
15
|
file: string;
|
|
16
16
|
bytes: Uint8Array;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** An image a pack's models reference, shipped flat beside the `.glb` files. */
|
|
19
|
+
export interface ExtractedPackImage {
|
|
20
|
+
/** Flat basename, e.g. "citybits_texture.png" — models reference it relative to their own URL. */
|
|
20
21
|
file: string;
|
|
21
22
|
bytes: Uint8Array;
|
|
22
23
|
}
|
|
@@ -72,21 +73,38 @@ export interface DownloadPackResult {
|
|
|
72
73
|
* @internal
|
|
73
74
|
*/
|
|
74
75
|
export declare function downloadPackArchive(source: AssetSource, options?: DownloadPackOptions): Promise<DownloadPackResult>;
|
|
76
|
+
/**
|
|
77
|
+
* External image basenames a packed `.glb` still references (its textures must
|
|
78
|
+
* sit beside it in the pack dir). Empty for self-contained models.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function externalGlbImages(bytes: Uint8Array): string[];
|
|
75
82
|
/**
|
|
76
83
|
* Pack a `.gltf` + optional external `.bin` into a single `.glb` so reindex /
|
|
77
84
|
* the shell only ever deal in one-file models. Strips buffer `uri` fields so
|
|
78
|
-
* the binary chunk is self-contained
|
|
79
|
-
*
|
|
85
|
+
* the binary chunk is self-contained; external image URIs are flattened to
|
|
86
|
+
* basenames so the pack ships each referenced texture beside the models
|
|
87
|
+
* (loaders resolve the relative URI against the model's own URL).
|
|
80
88
|
* @internal
|
|
81
89
|
*/
|
|
82
90
|
export declare function packGltfToGlb(gltfBytes: Uint8Array, binBytes?: Uint8Array): Uint8Array;
|
|
91
|
+
/** What `extractGlbs` pulls out of a pack archive: the models plus the textures they reference. */
|
|
92
|
+
export interface ExtractedPack {
|
|
93
|
+
models: ExtractedGlb[];
|
|
94
|
+
/** Every archive image some extracted model references, flattened to basenames. */
|
|
95
|
+
images: ExtractedPackImage[];
|
|
96
|
+
}
|
|
83
97
|
/**
|
|
84
|
-
* Pull every GLB out of an archive
|
|
85
|
-
* pairs (Quaternius Standard packs on
|
|
86
|
-
*
|
|
98
|
+
* Pull every GLB out of an archive, plus the textures those models reference.
|
|
99
|
+
* Converts co-located `.gltf` + `.bin` pairs (Quaternius Standard packs on
|
|
100
|
+
* OpenGameArt, KayKit bits packs) into `.glb` so the catalog stays
|
|
101
|
+
* one-file-per-model; GLB wins when both formats share a basename. External
|
|
102
|
+
* image URIs are flattened to basenames and the matching archive images are
|
|
103
|
+
* returned for writing beside the models — a pulled pack renders textured, not
|
|
104
|
+
* as white clay (#1005).
|
|
87
105
|
* @internal
|
|
88
106
|
*/
|
|
89
|
-
export declare function extractGlbs(archive: Uint8Array):
|
|
107
|
+
export declare function extractGlbs(archive: Uint8Array): ExtractedPack;
|
|
90
108
|
/** One SVG/PNG file pulled out of a sprite/icon-pack archive by `extractSpriteFiles`. */
|
|
91
109
|
export interface ExtractedSpriteFile {
|
|
92
110
|
file: string;
|
|
@@ -97,6 +115,4 @@ export interface ExtractedSpriteFile {
|
|
|
97
115
|
*/
|
|
98
116
|
export declare function extractSpriteFiles(archive: Uint8Array): ExtractedSpriteFile[];
|
|
99
117
|
/** @internal */
|
|
100
|
-
export declare function extractTextures(archive: Uint8Array): ExtractedTexture[];
|
|
101
|
-
/** @internal */
|
|
102
118
|
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|