@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 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.
@@ -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;
@@ -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, entry.dims === undefined ? { url } : { url, dims: entry.dims });
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, ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims });
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, extractTextures } from "../download.js";
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 glbs = extractGlbs(archive);
200
- if (glbs.length === 0)
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 glbs)
203
+ for (const glb of models)
204
204
  writeFileSync(join(outDir, glb.file), glb.bytes);
205
- const textures = extractTextures(archive);
206
- for (const texture of textures) {
207
- const dest = join(outDir, texture.file);
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
+ }
@@ -15,8 +15,9 @@ export interface ExtractedGlb {
15
15
  file: string;
16
16
  bytes: Uint8Array;
17
17
  }
18
- export interface ExtractedTexture {
19
- /** Path relative to the pack output dir, e.g. "Textures/colormap.png". */
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. External image URIs stay as-is (loaders
79
- * resolve relative to the catalog URL only for GLB-embedded images).
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. Also converts co-located `.gltf` + `.bin`
85
- * pairs (Quaternius Standard packs on OpenGameArt) into `.glb` so the catalog
86
- * stays one-file-per-model. GLB wins when both formats share a basename.
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): ExtractedGlb[];
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>;